Over-the-air updates
expo-updates lets the app download new JavaScript and assets at launch without a store release. It checks https://u.expo.dev/<projectId> on every launch (checkAutomatically: ALWAYS, fallbackToCacheTimeout: 0), so a published update is picked up on the next cold start after download.
Channels and runtime versions
| Build profile | Channel |
|---|---|
| development | development |
| preview | preview |
| production | production |
runtimeVersion.policy is appVersion: an update only applies to builds with the same version in app.json. After npm run version and a new store build, updates must be published for the new version; older installs keep receiving updates for theirs until you stop publishing them.
eas update --channel production --message "Fix verse finder empty state"
eas update --channel preview --message "..."
What can and cannot go OTA
| Change | OTA? |
|---|---|
| JavaScript, styles, locales, curriculum JSON, Bible metadata JSON | yes |
| New or upgraded native module, config plugin, permission, Google client id, splash/icon | no: native build and store release |
| Backend API change | yes for the app side, but deploy the backend first and keep it compatible with the previous app version |
The mobile CLAUDE.md says Google sign-in "needs a new dev-client / EAS build, not just an OTA update"; the same applies to anything listed in the second row.
Checking what users have
eas update:list --channel production shows published updates; eas build:list shows builds and their runtime versions. The app's Constants.expoConfig is read in App.js but not surfaced; adding the update id to Settings would help support.
Rolling back
Publish the previous good commit as a new update (eas update --channel production from that commit), or eas update:republish. Users get it on their next launch.