React Native OTA updates without the wait
Ship JS-layer fixes and UI changes to your React Native app in seconds, not days. Ubriot publishes OTA updates to named channels and lets you roll back instantly. Before you publish, it warns you if an update would target an older runtime than your users are on.
- #412 iOSBuilding, Building
- #409 AndroidOn Google Play, Done
- #410 iOSNeeds you, Needs you
- #411 iOSSigning, Building
- #406 iOSOn TestFlight, Done
The short answer
You can ship React Native OTA updates without EAS Update by pointing your app at a different update server. Expo apps keep expo-updates and set expo.updates.url to a server that speaks the expo-updates protocol, such as Ubriot. Bare React Native apps can use ubriot-updates-rn, with no Expo required.
Ubriot hosts OTA updates on every plan, from $29 a month after a 14-day trial, with named channels, rollback by repointing a channel, and no per-MAU or bandwidth charge. Other routes include Bitrise CodePush, Codemagic Patch (self-hosted) and the open-source CodePush server.
Migrate from EAS Update · OTA for bare React Native · Ubriot vs Expo EAS
Who Ubriot suits
- Expo apps that want to keep expo-updates but stop paying per update MAU
- Bare React Native apps, including teams moving off hosted CodePush
- Teams who want OTA, builds and store submission in one place
Who Ubriot does not suit
- Teams who want EAS-native tooling and Expo's own hosted services: EAS Update is the first-party option
- Apps where OTA download size matters most: EAS Update offers bundle diffing and Ubriot does not
- Teams who want to run their own OTA server: Codemagic Patch and the open-source CodePush server are self-hosted
- Anyone who needs a free plan: Ubriot has no free tier, only a 14-day trial
What Ubriot OTA updates give your React Native team
- Publish updates without App Store review
- Named channels (production, staging, etc.)
- Runtime version compatibility checks
- Instant rollback from the dashboard
- CLI command: ubriot update
- Update history and bundle storage
- Per-channel update targeting
- Works with Expo (expo-updates) and bare React Native (ubriot-updates-rn)
How React Native OTA updates work with Ubriot
Set up once, then ship updates with one command.
Step 1
Set a runtime version
The runtime version tells Ubriot which native binary a JS bundle is compatible with. Updates only land on matching binaries.
Step 2
Set up the update URL
Point your update client, expo-updates for Expo apps or ubriot-updates-rn for bare React Native, at your Ubriot update endpoint. The app checks for a new bundle on launch.
Step 3
Run ubriot update
Bundle your JS layer and publish it to a channel, production, staging, or any name you choose. Takes seconds.
Step 4
The app receives the update
On next launch, the app downloads and applies the new bundle. No App Store review, no waiting.
Step 5
Roll back instantly if needed
If a bad update ships, roll back to any previous bundle from the dashboard. Users get the previous version on next launch.
Expo or bare React Native, your choice
Same channels, same rollback, same ubriot update command. Pick the client that matches your app.
Expo
Add expo-updates, set updates.url to your Ubriot endpoint, and publish with ubriot update.
Bare React Native
Install ubriot-updates-rn, no Expo required. Same backend, same CLI.
npm install ubriot-updates-rnCommon pitfalls
These mistakes cause silent OTA failures, good to know before you ship.
reloadAsync() must be compiled into the binary, it cannot arrive via OTA
With checkAutomatically: NEVER, your binary must call checkForUpdateAsync() → fetchUpdateAsync() → reloadAsync(). Miss it and updates download but never apply, and an OTA can't add it back. Verify the sequence is compiled in before shipping.
Run ubriot update from inside your app directory, not the monorepo root
In a yarn workspaces monorepo, yarn hoists packages to the root. When you run ubriot update from the root, npx expo export resolves modules from the wrong node_modules and fails with module-not-found errors. Always cd into your app folder (e.g. products/my-app/mobile) before running ubriot update.
runtimeVersion links OTAs to binaries, never change it casually
An OTA is only delivered to devices whose installed binary declares the same runtimeVersion. Changing the value in app.json means old binaries stop receiving new OTAs instantly, and new OTAs can never reach anyone still on the old binary. Only change runtimeVersion when a native module change makes old JS bundles genuinely incompatible with the new binary.
Apple closes a version train once a build is approved
If you submit a binary and Apple approves it, that version string (CFBundleShortVersionString) is closed. Submitting another binary under the same version returns a 409 Invalid Pre-Release Train error. Bump your version field in app.json before every binary build that goes to TestFlight or the App Store.
Common questions
- How do I ship React Native OTA updates without EAS Update?
- Point your update client at a different update server. Expo apps keep expo-updates and change expo.updates.url to a server that speaks the expo-updates protocol, such as Ubriot. Bare React Native apps can use ubriot-updates-rn. Other options include Bitrise CodePush, Codemagic Patch (self-hosted) and the open-source CodePush server.
- Does Ubriot charge per monthly active user for OTA updates?
- No. OTA updates are included in every Ubriot plan ($29, $99 or $299 a month) with no per-MAU, bandwidth or storage charge. There is no free tier; every plan starts with a 14-day trial.
- Can I roll back a bad OTA update?
- Yes. Point the channel back to an earlier update group from the dashboard or CLI, and devices pick up that update on their next launch.
- Does Ubriot support bundle diffing?
- No. Ubriot serves the full JavaScript bundle for each update. EAS Update offers bundle diffing from Expo SDK 55, so if download size is your main concern, EAS Update may suit you better.
Ship React Native OTA updates today
Fix bugs and ship UI changes in seconds with ubriot update.