Migrating from Expo Updates
Migrating from Expo Updates
This guide covers moving from Expo Updates / EAS Update to self-hosted Codemagic Patch. You can keep EAS Build for native binaries if you want; only the update delivery layer changes. Start against the local evaluation stack with your own app before provisioning domains. For trade-offs, see How Patch compares.
Patch requires a development build (not Expo Go) and a new native binary with the Patch SDK embedded. Plan a store or internal build before OTA can flow through Patch.
Concept mapping
| Expo Updates | Codemagic Patch |
|---|---|
| EAS-hosted update server | Self-hosted Patch API + storage |
Update channel (preview, production, …) | Deployment (Staging, Production, …) |
runtimeVersion (EAS) | targetBinaryVersion |
eas update | cmpatch release-react |
Updates.checkForUpdateAsync() / fetchUpdateAsync() | sync() or checkForUpdate() + downloadUpdate() + installUpdate() |
Updates.reloadAsync() | restartApp() (or install mode IMMEDIATE) |
expo-updates config in app.config | @codemagic/react-native-patch config plugin |
| EAS project credentials | GitHub OAuth + cm_pat_… tokens on your Patch instance |
1. Stand up Patch
For a proof of concept on your own Expo app, start with the Local quickstart (./scripts/local-eval/up.sh). That runs the real API, storage, and dashboard on your laptop without domains or OAuth. Use Install when you want a shared Staging/Production server.
| Local eval | Running on VM | |
|---|---|---|
API (apiUrl) | http://localhost:3000 | https://updates.example.com |
Download base (downloadBaseUrl) | http://localhost:9100/codemagic-patch | https://storage.updates.example.com/codemagic-patch |
Create apps for your product (skip the seeded demo-app-* apps unless you are only trying the bundled on-device demo):
cmpatch login --server-url http://localhost:3000
cmpatch app create --server-url http://localhost:3000 --name MyApp-iOS
cmpatch app create --server-url http://localhost:3000 --name MyApp-Android
cmpatch deployment list --server-url http://localhost:3000 --app MyApp-iOS --format table
Map each EAS channel you use today to a Patch deployment (e.g. EAS preview → Patch Staging, EAS production → Patch Production). Copy each deployment key into the Expo config plugin in the next step.
Local eval is enough to answer "does Patch work on our app?" It is not a production host: authentication is disabled and ports bind to localhost. Physical devices and Android emulators need a LAN IP (or adb reverse); see Try it with your own app.
2. Replace the client integration
Remove Expo Updates from the project:
yarn remove expo-updates
Remove expo-updates from plugins in app.config and delete updates / runtimeVersion blocks that pointed at EAS (unless you still need runtimeVersion for another tool. Patch does not read it).
Add Patch:
yarn add @codemagic/react-native-patch
Configure the Expo config plugin with deployment key, apiUrl, and downloadBaseUrl per platform. For local eval:
{
"expo": {
"plugins": [
[
"@codemagic/react-native-patch",
{
"ios": {
"deploymentKey": "<from deployment list>",
"downloadBaseUrl": "http://localhost:9100/codemagic-patch",
"apiUrl": "http://localhost:3000"
},
"android": {
"deploymentKey": "<from deployment list>",
"downloadBaseUrl": "http://localhost:9100/codemagic-patch",
"apiUrl": "http://localhost:3000"
}
}
]
]
}
}
Then regenerate native projects:
npx expo prebuild --clean
cd ios && pod install && cd ..
Build a development or release binary (not Expo Go) and install it on a simulator or emulator. Replace update logic in JS:
// Before (Expo Updates)
import * as Updates from "expo-updates";
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
await Updates.reloadAsync();
}
// After (Patch)
import { sync } from "@codemagic/react-native-patch";
void sync();
Tune when checks run in Checking for updates and install timing in Applying updates.
3. Ship OTAs with Patch instead of EAS Update
For a local proof of concept, run release-react from your machine against the eval stack (no CI token required):
cmpatch release-react \
--server-url http://localhost:3000 \
--platform ios \
--deployment Staging \
--release-notes "Local PoC" \
--yes
Confirm the update on the binary you built in step 2 (check / relaunch, same loop as the on-device demo). That is enough to decide whether a production install is worth it.
When you cut over for real, install the CLI against your hosted API and create a CI token:
npm install -g @codemagic/patch-cli
cmpatch config set server-url https://updates.example.com
cmpatch token create --name ci
Replace eas update (or EAS Update workflow steps) with the same release-react command, pointed at your production server URL. release-react bundles JS, resolves a target binary version, and uploads in one step, similar in role to eas update plus local bundling. See CI integration.
You can keep EAS Build (or any CI) to produce the native .ipa / .apk.
4. Cut over
- Install Patch on your domains (or keep using local only until the PoC is done).
- Point the Expo plugin at the production API and download URLs; ship a development or store build with those keys (Staging first).
- Publish an OTA with
cmpatch release-reacttargeting the binary version users install. - Confirm updates on real devices; then promote the same workflow to Production.
- Turn off EAS Update for the project once no binaries depend on it.
Checklist
- Local eval (or hosted Install) running; apps and deployments created
-
expo-updatesremoved; Patch plugin configured inapp.config(local URLs for PoC) -
expo prebuildrun; development/release binary installed (not Expo Go) -
sync()(or manual API) replaces Expo Updates calls - Local
cmpatch release-reactlands an update on that binary - Hosted Install done before any shared/Staging cutover
- CI publishes with
cmpatchinstead ofeas update - New native binary in users' hands; Staging validated before Production