OTA updates

Push JS updates without a new build.

6 minmedium

OTA = over-the-air updates. Ship a JS bundle change to every user without re-submitting to the store. Your apps update overnight while users sleep. This is the Expo superpower most teams underuse.

What can update OTA, what can't

Your app has two layers:

  • Native code — installed via APK/IPA. Changing it requires a new store submission.
  • JS bundle — your React Native code, components, styles. Updates OTA.

What that means in practice:

ChangeOTA?
UI / styles / new screensYes
Bug fixes in JS / TSYes
API contract changes (calling new endpoints)Yes
Adding a new npm dep that's pure-JSYes
Adding a native module (camera, BLE, etc.)No — store re-submit
Changing app icon or splash screenNo — store re-submit
Bumping Expo SDKNo — store re-submit

Shipping an OTA

Terminal
# from apps/mobile/
$eas update --branch production --message "Fix dashboard refresh bug"

Bundles your JS, uploads to Expo. Apps configured for production branch check for updates on next launch and download the new bundle.

Branches map to release channels

Set up at least two branches:

  • preview — internal testers / beta users
  • production — store users

Push to preview first, validate, then push to production. Same idea as canary / staging / prod for backends.

What the user experiences

  1. User opens the app
  2. Expo checks for new bundle (~200ms HTTP call)
  3. If new bundle available, downloads it in the background
  4. Next launch uses the new bundle

First-launch users see the bundled bundle (from the store). On their second launch, they see your update.

Forcing an immediate reload

For urgent fixes, you can ask Expo to fetch + reload on this session, not next launch:

import * as Updates from 'expo-updates'
useEffect(() => {
Updates.checkForUpdateAsync().then((update) => {
if (update.isAvailable) {
Updates.fetchUpdateAsync().then(() => Updates.reloadAsync())
}
})
}, [])

Use sparingly — reloading mid-session is jarring. Better for important security fixes.

Don't OTA something that needs a native change. If your JS expects a new native module that's not in the installed binary, the app crashes on first use. Match OTA bundle compatibility to the installed runtime version.

Rolling back

Pushed a bad update? Republish the previous version's bundle:

Terminal
$eas update --branch production --republish --group <old-group-id>

Every eas update creates a "group" you can re-publish later. Roll back to any earlier group, users get it on next launch.

Quick check

You shipped a UI tweak via `eas update`. A user emails saying their app crashed after opening. Diff: you removed `react-native-camera` from the JS imports, but the binary on their phone still has the native module. Why did it crash?

Try it

Ship your first OTA:

  1. Make a small visible change to your app (e.g., change the home-screen title from "Users" to "Team").
  2. eas update --branch preview --message "rename users to team"
  3. On your phone (where you installed the preview build), force- quit the app and reopen it.
  4. You should see the new title without re-installing.

Paste a before/after screenshot in notes.md. That's chapter 5's assignment done.

You finished Building Mobile with Go API 🎉

Five chapters, ~13 lessons. You can now scaffold a mobile + API monorepo, sync types end-to-end, build login + secure storage + refresh, register and receive push notifications, ship to the stores via EAS, and OTA updates over the air.

From here: pair this with --triple --mobile in the Multi-Platform course to add web + admin to the mix, or move to Building Web with Next.js to add a marketing site + dashboard.

Spot a typo? Have an idea?

Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.

Suggest an improvement on GitHub