Expo
Use FireOTA in an Expo app: one config plugin, works with expo prebuild, EAS Build and expo-router.
7 sections Edit this page on GitHub
Requirements
- Expo SDK 50 or newer, with development or release builds (
npx expo run:android, EAS Build). Expo Go cannot load native modules, so it can't run FireOTA. - Works with Continuous Native Generation: you don't need to commit
android/andios/. The config plugin re-applies FireOTA's native change on everyexpo prebuildand EAS Build. - No
expo-updates. Both replace the JavaScript bundle at launch; the plugin stops the build if it is installed. See migrating from EAS Update, or keepexpo-updatesand use Expo Updates without EAS instead of the SDK. - Tested with Expo SDK 58 (React Native 0.88, expo-router): prebuild, OTA bundling and the Android release compile are verified. On-device OTA runs and iOS (same plugin) are still being tested.
Install
Run this in your Expo project. fireota init detects Expo and wires everything for you:
npx expo install @fireota/react-native npm install --save-dev @fireota/cli npx fireota init --app YOUR_APP_ID --server-url https://fireota.example.com
fireota.config.js— app ID, server URL and channel (no secrets).app.json— adds the"@fireota/react-native"config plugin.- Your root component — with expo-router the root layout (
app/_layout.tsx), otherwiseregisterRootComponent(App)— is wrapped withwithFireOTA, which checks for updates and shows the update prompt. package.json— anota:publishscript;.gitignore— update packages and the signing key.
app.config.js or app.config.ts? init doesn't edit dynamic configs — add "@fireota/react-native" to plugins yourself.Set it up by hand
Prefer to see every change? These are the only two edits FireOTA needs in an Expo app.
- 1Add the plugin to
app.json. The runtime version is yourexpo.version(Expo writes it into the native project), so bump it whenever native code changes:json{ "expo": { "version": "1.0.0", "plugins": [ "expo-router", "@fireota/react-native" ] } } - 2Wrap your root component. With expo-router:Without expo-router:tsx
// src/app/_layout.tsx (or app/_layout.tsx) import { Stack } from "expo-router"; import { withFireOTA } from "@fireota/react-native"; import fireotaConfig from "../../fireota.config"; function RootLayout() { return <Stack />; } export default withFireOTA(RootLayout, fireotaConfig);tsx// index.ts (apps without expo-router) import { registerRootComponent } from "expo"; import { withFireOTA } from "@fireota/react-native"; import fireotaConfig from "./fireota.config"; import App from "./App"; registerRootComponent(withFireOTA(App, fireotaConfig));
On prebuild, the plugin makes release builds load the latest verified OTA bundle: on Android it passes jsBundleFilePath to ExpoReactHostFactory (older SDKs: overrides getJSBundleFile()); on iOS bundleURL() returns FireOTA's bundle. Debug builds keep using Metro.
Build and publish
Make one release build so the app contains the SDK and the plugin's change:
# Local release build (Android): prebuild runs the FireOTA plugin npx expo run:android --variant release # Or EAS Build: the plugin runs on EAS's servers too eas build --platform all --profile production
After that, ship JavaScript changes as updates. The CLI bundles Expo apps with expo export:embed and the same entry point your native build uses (expo-router included), then compiles Hermes bytecode unless "jsEngine": "jsc":
# Bundles with Expo (expo export:embed + Hermes), exactly like the store build FIREOTA_URL=https://fireota.example.com FIREOTA_API_KEY=fota_sk_... \ npm run ota:publish -- --message "Fix checkout crash"
Keep expo-updates, skip EAS UpdateNew
Already using expo-updates? You can keep it and point it at FireOTA instead of EAS Update. You don't need the FireOTA SDK or any native change. Bundles are stored in your own Firebase Storage, and you still get channels, rollouts and rollbacks.
Migrating from EAS Update (expo-updates)
- 1Remove
expo-updatesand its config:bashnpm uninstall expo-updates # remove "updates" and "runtimeVersion" from app.json, then rebuild npx expo prebuild --clean
- 2Run
npx fireota init …as above and ship a new store build. Users on old builds keep receiving EAS updates until they install it. - 3Map EAS channels to FireOTA channels (production, beta, …). Keep FireOTA runtime versions equal to
expo.version.
["@fireota/react-native", { "allowExpoUpdates": true }] and set "updates": { "enabled": false }, or the two systems will fight over which bundle launches.Troubleshooting
- Build fails with “expo-updates is installed” — remove it (see above); FireOTA cannot run next to it.
- Prebuild warns “could not find the expected code” — your
MainApplicationorAppDelegatewas customised. Apply the change from Android setup / iOS setup in your own config plugin. - Update downloads but never applies — you're running a debug build or Expo Go; test with
npx expo run:android --variant release. - “No compatible update” —
expo.versionof the installed build must equal the update's runtime version.
Was this page helpful?