Push JS and asset changes straight to installed apps. Roll out to a percentage of devices, watch adoption live, and undo a bad release for everyone — including the users already running it.
No app-store review · Works with bare React Native and Expo · Runs on your own infrastructure
v1
shipped in the binary
v2
delivered over the air
Seconds
from publish to device
0
app-store reviews to ship a JS fix
1
command to build and release
100%
of a bad release recoverable
How it works
One command builds your bundle with Metro, zips it with its assets, and uploads it. Expo and bare React Native are auto-detected.
Devices ask what applies to their binary version and rollout bucket. They get a signed, time-limited URL and pull the bundle straight from storage.
The SDK checks the SHA-256 before unzipping, swaps the bundle in on restart, and reverts automatically if the new build never confirms it booted.
$ yarn ota:prod \
--platform ios --rollout 20
› expo export:embed --platform ios --dev false
Packaged 214 files → 2.1 MB
✔ Released #9 to production▊
The CLI runs your project's own bundler, packages the output with its assets, uploads it, and records the release. Add --format json and the same command drops straight into CI.
Control
Serve a release to 5% of devices, watch the failure rate, then widen to 100% without republishing. Bucketing is deterministic per device and per release, so nobody flips between 'update available' and 'no update' — and each release samples a fresh slice instead of always hitting the same unlucky users.
20% of the fleet served — the same devices on every check, never reshuffled.
A rollback republishes the last good bundle as a new mandatory release and disables the bad one. That matters: flipping a pointer would only spare devices that hadn't updated yet. Everyone on the bad build gets the fix on their next check.
v6
healthy
v7
crashes
v8
v6 restored
Every freshly installed bundle is treated as unproven. If the app doesn't call notifyAppReady() before the next launch, the native layer reverts to the previous bundle, blacklists the bad hash, and reports it back so it shows up in your dashboard.
Adoption per release over time, install successes and failures, failure rate per release, and a live view of how much of each channel has moved to the release you're serving right now.
Features
Target an exact version or a semver range. A bundle never reaches a native binary it was not built for.
Out-of-range binaries wait for a store update instead — a bundle never reaches a native version it wasn't built for.
Organizations own apps, members and keys — the same model whether you run it for one team or many.
Acme Corp
Organizations own apps, members and keys — the same model for one team or many.
Post-publish actions logged with before/after — rollout changes, disables, key rotations, humans and CI alike.
Every post-publish action logged with before/after — humans and CI keys attributed the same way.
yarn ota:prod builds with Metro and publishes both platforms in a single step. Detects Expo vs bare React Native automatically.
Production, staging, QA — any number of channels per project, each with its own SDK key and release history. A staging build cannot receive production updates.
Force a critical fix to install immediately instead of waiting for the next natural restart.
Change rollout percentage, disable or re-enable a release, edit notes — all without republishing the bundle.
Restore any earlier release as a new mandatory one. The stored bundle is reused, so it is immediate.
On next restart, on next resume after a minimum background time, or immediately with a JS reload.
Subscribe to byte-level progress to drive your own update banner or progress bar.
withOtaUpdate wires up check-on-launch and check-on-resume. useOtaUpdate powers custom update UI.
Every release with its target range, rollout, size, author, and rollback provenance.
Who published what, how many mandatory releases, rollbacks and disables, attributed to humans and CI keys alike.
Scoped keys for pipelines, hashed at rest, shown once. Barred from managing members or other keys.
Owner, admin, developer and viewer. Viewers never see a plaintext SDK key or any billing detail.
Every CLI command supports --format json, so release automation is a one-liner in any pipeline.
Client
Bare projects add one override to their React Native host. Expo projects add a config plugin and run prebuild — the native wiring is registered automatically, including on the new bridgeless architecture.
Android
Kotlin native module
iOS
Objective-C native module
Expo
Config plugin
Android is verified end to end on release builds. The iOS module ships and links, and is pending device verification.
Checks on launch and on resume, and confirms the running bundle so rollback protection is armed.
import { withOtaUpdate } from '@otaupdate/react-native';
function App() {
return <YourApp />;
}
export default withOtaUpdate(App);Security
Bundles live in a private bucket and are only reachable through short-lived presigned URLs generated per request.
The SHA-256 published with the release is checked before the archive is unpacked, so a corrupted or tampered payload never becomes a runnable bundle.
Passwords are bcrypt-hashed. API keys are stored as SHA-256 and shown exactly once. SDK keys are hashed for lookup and encrypted for display.
The public update-check endpoint is rate limited per IP, and never caches a response carrying a signed URL.
API keys act inside a single organization and cannot mint other keys or change membership.
SDK keys, API keys and passwords are redacted from the request log.
Deployment
API, PostgreSQL, S3-compatible storage and this dashboard. Migrations run on boot and secrets are generated on first start, so a clean machine needs no preparation.
$ docker compose up -d
✔ postgres · minio · api · dashboard
ready on :8080▊
Bundle downloads bypass the API entirely, so the hot path stays a single indexed lookup.
Bundles download straight from storage — they never touch the API.
Run one command, embed a channel key, and ship your first bundle in a few minutes.