expo-updates with EAS Update.
Concepts
An installed app only receives an update when both the channel and the runtime version match. A binary built as version
1.0.5 only loads updates published with runtime 1.0.5.
The app does not call the expo-updates API from its own code. It relies on the default behaviour: check for an update on launch, download it in the background, and apply it on the next cold start. A trainee therefore needs two launches to see a new update.
Publishing
To the current version
update:preview and update:production are aliases for the same script. scripts/ota-push.sh does three things:
- Checks that the profile is
productionorpreview. Without a message it usesOTA <profile> <UTC date and time>. - Runs
node scripts/sync-eas-ota-env.js <profile>. This readsbuild.<profile>.envfromeas.jsonand pushesAPP_ENVand everyEXPO_PUBLIC_*key to the matching EAS environment witheas env:set. Other keys are skipped as native-build-only. This step exists because an update bundle is built on your machine with EAS environment variables, not with the build profile’senvblock. Without the sync an update could ship with a different API URL than the binary it lands on. - Runs
eas update --branch <profile> --environment <profile> --message "<message>".
expo.version is in app.json at that moment.
To every version still in use
Trainees stay on old store versions for a long time. A fix published only to the newest runtime reaches only people who already updated from the store.scripts/ota-all-runtimes.sh publishes the current bundle once per runtime:
- Refuses to run when
app.jsonhas uncommitted changes. - Takes the runtime list from the arguments, or uses the default list written in the script.
- For each runtime, rewrites
expo.versioninapp.jsonto that value and runspnpm ota:production "<message> (runtime X)". - Restores
app.jsonwithgit checkoutin an exit trap, so an interrupted run does not leave the wrong version behind. A wrong version left inapp.jsonwould mis-target the next update and the next store build.
Verify every publish
After a multi-runtime publish, check that each update group was recorded under the runtime its label says:(runtime X) suffix in its message. If a runtime is missing, publish it again on its own:
The native module trap
Publishing one bundle to every runtime deliberately defeats the protection theappVersion policy gives. The runtime version says “this JavaScript matches this native code”. When you send today’s JavaScript to a binary built two months ago, that promise is only true if the JavaScript does not need anything the old binary lacks.
What goes wrong
- A dependency with native code is added, for example a new Expo module, and imported at the top of a file that runs at startup.
- A new store build includes it. Old installs do not.
- An update is published to all runtimes.
- On an old binary, loading the module throws while the bundle is being evaluated. The app never renders.
expo-updatestreats the launch as failed and falls back to the embedded bundle. - Every later update fails the same way. Those installs are frozen on the JavaScript their store build shipped with.
A try/catch around a dynamic import is not enough
It is natural to write this and assume it is safe:
catch, and a release build closes. This is how photo uploads crashed the app on binaries built before the image manipulation module was added.
The safe pattern
Probe for the native module first. Import only when it is there.src/lib/media/shrinkImage.ts. shrinkImage returns null when the loader does, and the caller uploads the original image. The reference implementations are src/lib/media/shrinkImage.ts and src/features/movement/lib/cardioNotification.ts. The second one probes CardioNotification, the local Android module, so an update on a binary built before the module simply shows no notification.
requireOptionalNativeModule finds Expo modules. It cannot see a Nitro module such as the HealthKit library. Guard those with a platform and version check instead, and test on an old binary.
Rules
- Never import a native module at module scope in a file that runs at startup.
- Use the name the native side registers (for example
ExpoImageManipulator), not the npm package name. - The feature must degrade when the probe returns nothing: hide the button, skip the step, or use a JavaScript fallback.
- Adding a native module means store builds for both platforms.
- Until every runtime in the OTA list has the module, the guard must stay.
- When installs have moved off an old version, remove it from the runtime list in
scripts/ota-all-runtimes.sh.
Diagnosing a platform-only bug
Before you debug the feature, check whether the device is running the code you think it is.- Compare store build dates with the date the feature was added:
eas build:list --platform androidand the same forios. A store build older than the feature, combined with a working update channel, means the update may be failing to launch. - Run
eas update:list --branch productionand look for a rollback entry on the reporter’s runtime that no later publish has replaced. - Remember the two-launch rule. A device that has just downloaded the fix still shows the old behaviour until it is relaunched.
Rollback
- It applies per runtime. Rolling back one runtime does not touch the others.
- It pins that runtime to its embedded bundle until a newer update is published for the same runtime. The embedded bundle can be weeks old. A rollback is a way to stop a crash, not a way to return to yesterday’s update.
What an OTA update can and cannot change
See Mobile builds for when a store build is required.
Publish checklist
1
Confirm the change is JavaScript only
Check the diff of
package.json, app.json, plugins/, modules/ and patches/. If any of them changed in a way that affects native code, this is a store build, not an update.2
Confirm every native import is guarded
Search the diff for new imports of packages with native code.
3
Confirm the API is ready
The update will call the production API. Every endpoint and field it needs must already be deployed.
4
Run the checks
5
Make sure app.json is clean and on the right version
The multi-runtime script refuses to run otherwise.
6
Publish to preview first when the change is risky
7
Publish to production
Publish to the current version, then to the older runtimes that still have installs.
8
Verify
Check
eas update:list --branch production. Open a production build on each platform, relaunch it twice, and confirm the change is there.