OTA Updates
Over-the-air (OTA) updates ship JavaScript and asset changes to installed apps without going through the App Store / Play. Expo Application Services (EAS) is the modern path on React Native — push a JS-only change, devices fetch it on next launch. Native code (any new plugin, native config) still needs a binary release.
EAS Update with channels, rollout, and runtimeVersion
EXAMPLE
# 1) Install + configure expo-updates
# npx expo install expo-updates
# Update app.config.ts
# export default {
# ...
# updates: { url: 'https://u.expo.dev/<project-id>' },
# runtimeVersion: { policy: 'appVersion' }, // 1.2.3 binaries get 1.2.3 OTA bundles
# ios: { ... }, android: { ... },
# };
# 2) Push your first OTA
# npx eas update --branch production --message 'Fix checkout copy'
# Devices on the 'production' channel pick this up on next launch.
# 3) Channels — environment-scoped distribution
# eas.json
# {
# "build": {
# "production": { "channel": "production" },
# "preview": { "channel": "preview" },
# "development":{ "channel": "development" }
# }
# }
# Promote an update from preview to production WITHOUT rebuilding the binary:
# npx eas update:edit --branch preview --rollout-percentage 10
# Test on 10% of installs first, then bump.
# 4) Force-apply latest update on next launch (in JS)
import * as Updates from 'expo-updates';
async function checkForUpdates() {
if (__DEV__) return;
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
// Show a banner OR reload silently
await Updates.reloadAsync();
}
} catch (err) {
console.warn('OTA check failed', err);
}
}
// Call at app start; debounce to once per session.
// 5) runtimeVersion — the safety net
# What it controls:
# - The binary AND the OTA bundle agree on the native API surface
# - An OTA bundle built for runtimeVersion '1.3.0' WILL NOT load on a binary
# whose runtimeVersion is '1.2.0' (missing native module = crash)
#
# Policies:
# - 'appVersion': derived from app version (1.3.0); bump = new runtime
# - 'sdkVersion': one runtime per Expo SDK
# - explicit: '1': manual; you bump when you change native code
# 6) When OTA is NOT enough — REBUILD the binary
# - Added a new native module (any react-native-* with native code)
# - Changed Info.plist / AndroidManifest permissions
# - Bumped Expo SDK / React Native version
# - Toggled architecture (Hermes on/off, new arch on/off)
# The Updates.checkForUpdateAsync call will refuse to install a bundle whose
# runtimeVersion does not match the binary — failing safe.
# 7) Rollouts — staged percentage
# npx eas update --branch production --message 'New checkout' --rollout-percentage 5
# npx eas update:edit --branch production --rollout-percentage 25
# npx eas update:edit --branch production --rollout-percentage 100
# Monitor Crashlytics during each step.
# 8) Roll back instantly
# npx eas update:rollback-to-embedded --branch production
# Reverts the channel to the binary's built-in bundle; devices fetch nothing new on next launch.
# 9) View what is live per channel
# npx eas update:list --branch production
# npx eas channel:view production
# 10) CI / release flow
# - Tests pass on the protected branch
# - Build the binary on a tag (or when runtimeVersion bumps)
# - Ship JS-only updates via 'eas update' on every merge to main:
# - name: ship-OTA
# run: npx eas-cli update --branch production --auto
# env: { EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} }
# 11) Privacy + compliance
# - App Store review reviews the BINARY, not the OTA bundle
# - You may not ship code that materially changes app behaviour or content
# policy compliance via OTA (Apple guideline 4.3.4)
# - Keep OTA to bug fixes, copy changes, and feature flag toggles
# 12) When NOT to ship an OTA on a Friday
# - You changed a deep navigation flow
# - The change relies on a recent binary that not all users have
# - Your monitoring is quiet over the weekend
# (Treat OTAs like any other deploy: stage, monitor, roll back fast.)
Why it matters
`runtimeVersion: { policy: "appVersion" }` is the single setting that keeps OTA safe. Bump the app version when you add native code, and old binaries automatically refuse the new bundle — no crash, no "missing native module" reports from users on yesterdays build. Skip this and your worst OTA mistake becomes a fleet-wide crash loop.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
# Ship a JS-only update without re-submitting to the stores. eas update --branch production --message "hotfix login screen"Try it Yourself »
Discussion
Loading…