Configuring Over-the-Air Updates in React Native with Expo Updates
Operate Expo updates with explicit runtime compatibility, channels, staged rollout, adoption monitoring, and a recovery strategy for failed JavaScript bundles. This revised guide emphasizes supported APIs, production tradeoffs, and an upgrade-friendly path without tying the advice to a calendar year.

Modern implementation baseline
Operate Expo updates with explicit runtime compatibility, channels, staged rollout, adoption monitoring, and a recovery strategy for failed JavaScript bundles.
Let Expo resolve compatible native package versions, use config plugins for native configuration, and validate the final experience in a release build. Updates, icons, splash screens, and runtime policies are release engineering concerns rather than cosmetic afterthoughts.
Recommended approach
- Never publish native-incompatible code to an existing runtime.
- Promote the same tested update across channels.
- Monitor failed installs and keep a known-good rollback target.
Production checklist
- Separate preview and production channels.
- Test release builds for startup, assets, permissions, and updates.
- Monitor update adoption and failed launches before broad rollout.
Authoritative references
By the end, you’ll have a React Native app ready to roll out updates seamlessly to iOS and Android users, with separate setups for staging and production environments. Let’s dive in!
Prerequisites
Before we get started, make sure you have these tools ready:
- Node.js (v20.x or later, LTS recommended): Grab it from nodejs.org.
- npm (v10.x or later, comes with Node.js).
- Expo CLI: Our go-to for easy React Native setup.
- Android Studio: Use the current stable release with compatible Android SDK and NDK tooling.
- Xcode (16+): For iOS development (macOS only). You’ll need Xcode 16+ (iOS 18 SDK) to meet App Store requirements.
- A code editor like VS Code.
- Git: For version control and managing your code.
Got everything? Awesome—let’s set up your project!
Step 1: Create Your React Native Project
Let’s kick things off by setting up a fresh React Native project with Expo.
-
Create a TypeScript Expo project:
npx create-expo-app@latest MyApp --template blank-typescript
cd MyApp -
Install Expo Updates with compatible native dependencies:
npx expo install expo-updates -
Configure the project and environment channels:
eas update:configure -
Test the project:
npx expo startPress
afor Android orifor iOS (macOS only) to launch the app on an emulator or device. You should see a simple welcome screen.
Summary: You have configured expo-updates with a runtime policy and environment-specific channels.
Step 2: Set Up Expo Updates
Now that your project is ready, let’s configure Expo Updates to handle OTA updates. This step connects your app to Expo’s cloud service, so you can push updates directly to users.
Project Configuration
-
Update
app.jsonto enable OTA updates:{
"expo": {
"updates": {
"enabled": true,
"url": "https://u.expo.dev/your-project-id",
"checkAutomatically": "ON_LOAD",
"fallbackToCacheTimeout": 0
},
"ios": {
"bundleIdentifier": "com.myapp.app"
},
"android": {
"package": "com.myapp.app"
}
}
}You’ll get your
project-idin Step 3 when we set up EAS (Expo Application Services). -
For bare workflows, link
expo-updates:- iOS: Run
npx pod-installto set up CocoaPods dependencies. - Android: Add the following to
android/app/build.gradle:apply from: "../../node_modules/expo-updates/expo-updates.gradle"
- iOS: Run
-
Add update-checking logic to
src/App.tsx:import { useEffect } from 'react';
import { View, Text } from 'react-native';
import * as Updates from 'expo-updates';
export default function App() {
useEffect(() => {
const checkForUpdates = async () => {
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
await Updates.reloadAsync();
}
} catch (e) {
console.error('Update check failed:', e);
}
};
checkForUpdates();
}, []);
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Welcome to MyApp!</Text>
</View>
);
}This code checks for new updates when the app starts, downloads them, and reloads the app to apply them.
iOS Configuration
- Open
ios/MyApp.xcworkspacein Xcode and confirmExpoUpdatesis linked in the Pods target. - Add the
EXUpdatesURLkey toInfo.plistwith the valuehttps://u.expo.dev/your-project-id. - Ensure the app’s bundle identifier matches
app.json.
Android Configuration
- Update
android/app/src/main/res/values/strings.xml:<resources>
<string name="expo_updates_url">https://u.expo.dev/your-project-id</string>
</resources> - Ensure
MainApplication.javaincludesexpo-updates:import expo.modules.updates.UpdatesPackage;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
protected List<ReactPackage> getPackages() {
return Arrays.asList(
new MainReactPackage(),
new UpdatesPackage()
);
}
};
}
Summary: You’ve configured Expo Updates in your app, setting up the foundation for OTA updates. Next, we’ll connect to EAS to manage those updates.
Step 3: Connect to EAS for OTA Updates
With your app configured, let’s hook it up to Expo Application Services (EAS), the cloud platform that powers OTA updates by hosting your JavaScript bundles.
-
Log in to Expo:
eas loginFollow the browser prompt to authenticate your account.
-
Set up EAS for your project:
eas build:configureThis creates an
eas.jsonfile:{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {}
}
} -
Initialize OTA updates:
eas update:configureThis generates a project ID, which you’ll add to
app.jsonunderupdates.url. -
Build your app to test OTA compatibility:
eas build --profile preview --platform allThis creates iOS and Android binaries ready for OTA updates.
Summary: You’ve connected your app to EAS, setting up the cloud infrastructure to deliver OTA updates. Now, let’s try pushing your first update!
Step 4: Deploy Your First OTA Update
Now that your app is linked to EAS, let’s deploy an update to see OTA in action.
-
Make a small tweak to your app, like updating the welcome message in
src/App.tsx:<Text>Welcome to MyApp! (Updated)</Text> -
Publish the update to the production branch:
eas update --branch productionThis uploads your new JavaScript bundle to EAS.
-
Test the update:
- Run your app in preview or production mode (avoid Expo Go, as it may interfere with updates).
- Close and reopen the app to download the update.
- Check that the updated message appears.
-
For a staging environment, publish to a separate branch:
eas update --branch stagingUse the EAS dashboard to manage branches or roll back updates if needed.
Summary: You’ve successfully deployed an OTA update, and your app now reflects the latest changes without an app store submission.
Step 5: Set Up Staging and Production Environments
To keep things organized, let’s configure separate OTA updates for staging and production environments, so you can test changes safely before going live.
-
Create a dynamic
app.config.tsfor environment-specific settings:import { ConfigContext, ExpoConfig } from 'expo/config';
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: process.env.APP_ENV === 'staging' ? 'MyApp Staging' : 'MyApp',
slug: 'myapp',
updates: {
enabled: true,
url: `https://u.expo.dev/your-project-id`,
checkAutomatically: 'ON_LOAD',
fallbackToCacheTimeout: 0,
},
ios: {
bundleIdentifier: process.env.APP_ENV === 'staging' ? 'com.myapp.staging' : 'com.myapp.app',
},
android: {
package: process.env.APP_ENV === 'staging' ? 'com.myapp.staging' : 'com.myapp.app',
},
}); -
Build for each environment:
- Staging:
eas build --profile preview --platform all - Production:
eas build --profile production --platform all
- Staging:
-
Publish updates for each environment:
APP_ENV=staging eas update --branch staging
APP_ENV=production eas update --branch production -
Test the environments:
- Install the staging build and verify the staging branch updates.
- Install the production build and confirm the production branch updates.
Summary: You’ve set up separate staging and production environments, allowing safe testing of updates before they reach all users.
Step 6: Enhance Your OTA Workflow
With your OTA system up and running, let’s explore ways to make it even better.
- Rollbacks: If an update goes wrong, revert it using the EAS dashboard or CLI:
eas update:rollback --branch production - A/B Testing: Create branches like
experiment-aandexperiment-bto test new features with different user groups. - Analytics: Add
expo-analyticsto track how users interact with updates and catch errors early. - Optimize Updates: Keep update sizes small by excluding unused assets and using code-splitting techniques.
Summary: Enhance your OTA setup with rollbacks, A/B testing, analytics, and optimization for a smooth and reliable update process.
Troubleshooting Tips
- Updates Not Loading: Ensure you’re not in debug mode, as it skips OTA updates. Test with preview or production builds.
- iOS Issues: Verify the
EXUpdatesURLinInfo.plistmatches your EAS project ID. - Android Problems: Check
expo-updateslinking inandroid/app/build.gradleand confirmexpo_updates_urlis set instrings.xml. - EAS Setup Errors: If
eas update:configurefails, manually checkeas.jsonand your project ID.
Conclusion
You’ve built a React Native app with Expo Updates, runtime compatibility, and staged channels for safely delivering eligible JavaScript and asset changes. With separate staging and production environments, you’re ready to test and deploy like a pro. Want to speed things up? Explore prebuilt templates with OTA support at Instamobile or Dopebase. For more advanced features, check out Expo’s documentation or join the React Native community at reactnative.dev.
Additional Resources
- dopebase.com - Mobile app development resources and templates
- instamobile.io - Production-ready React Native templates
- instaflutter.com - Flutter templates for cross-platform development
- reactnative.dev - Official React Native documentation
- docs.expo.dev - Expo SDK and EAS documentation