Self-hostable · CodePush-style OTA for React Native

Ship JavaScript fixes in seconds, not store-review days.

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

sha-256 verified✓

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

Three moving parts, no surprises

01

Publish

One command builds your bundle with Metro, zips it with its assets, and uploads it. Expo and bare React Native are auto-detected.

02

Serve

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.

03

Verify

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.

zsh

$ yarn ota:prod \

--platform ios --rollout 20

› expo export:embed --platform ios --dev false

Packaged 214 files → 2.1 MB

✔ Released #9 to production▊

Publishing is one line

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

Ship carefully, undo instantly

Staged rollouts that actually stage

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.

  • Percentage rollout, patchable any time
  • Sticky per-device assignment
  • Excluded devices still get the newest release they qualify for

20% of the fleet served — the same devices on every check, never reshuffled.

Rollback that rescues the users already broken

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.

  • Reuses the stored bundle — nothing re-uploads
  • Ships as mandatory at 100%
  • Full provenance kept in release history
●

v6

healthy

→
!

v7

crashes

→
↩

v8

v6 restored

A bad bundle can't brick the app

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.

  • SHA-256 checked before anything is unpacked
  • Automatic revert on a boot that never confirms
  • Zip-slip protection when extracting
  • ↓downloaded
  • ⇅sha-256 verified
  • ▶booted — unproven
  • ✓notifyAppReady() — kept
  • ↩no confirmation → reverted on next launch

See where your fleet actually is

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.

  • Adoption by release, per channel
  • Install failure rate surfaced per release
  • Per-developer release activity and audit trail
v9v8v7

Features

Everything the platform does

Binary version targeting

Target an exact version or a semver range. A bundle never reaches a native binary it was not built for.

target 1.4.x – 1.5.x→
1.3.2—
1.4.0✓
1.4.8✓
1.5.1✓
1.5.4✓
2.0.0—

Out-of-range binaries wait for a store update instead — a bundle never reaches a native version it wasn't built for.

Multi-tenant by default

Organizations own apps, members and keys — the same model whether you run it for one team or many.

Acme Corp

MyApp-iOSMyApp-Android
OwnerAdminDeveloperViewer

Organizations own apps, members and keys — the same model for one team or many.

Every action, attributed

Post-publish actions logged with before/after — rollout changes, disables, key rotations, humans and CI alike.

  • jane@acme.co·rollout 20% → 100%
  • ci-release (API key)·published v12
  • bob@acme.co·rotated staging SDK key

Every post-publish action logged with before/after — humans and CI keys attributed the same way.

One-command release

yarn ota:prod builds with Metro and publishes both platforms in a single step. Detects Expo vs bare React Native automatically.

Channels

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.

Mandatory updates

Force a critical fix to install immediately instead of waiting for the next natural restart.

Rollout patching

Change rollout percentage, disable or re-enable a release, edit notes — all without republishing the bundle.

Instant rollback

Restore any earlier release as a new mandatory one. The stored bundle is reused, so it is immediate.

Three install modes

On next restart, on next resume after a minimum background time, or immediately with a JS reload.

Download progress

Subscribe to byte-level progress to drive your own update banner or progress bar.

React hooks & HOC

withOtaUpdate wires up check-on-launch and check-on-resume. useOtaUpdate powers custom update UI.

Release history

Every release with its target range, rollout, size, author, and rollback provenance.

Per-developer analytics

Who published what, how many mandatory releases, rollbacks and disables, attributed to humans and CI keys alike.

API keys for CI

Scoped keys for pipelines, hashed at rest, shown once. Barred from managing members or other keys.

Roles

Owner, admin, developer and viewer. Viewers never see a plaintext SDK key or any billing detail.

JSON output

Every CLI command supports --format json, so release automation is a one-liner in any pipeline.

Client

Drops into the app you already have

Bare React Native and Expo

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.

Two lines in your app

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

Built for shipping code to real users

Signed, expiring download URLs

Bundles live in a private bucket and are only reachable through short-lived presigned URLs generated per request.

Integrity verified on device

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.

Secrets hashed at rest

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.

Rate limiting on the hot path

The public update-check endpoint is rate limited per IP, and never caches a response carrying a signed URL.

Least privilege for CI

API keys act inside a single organization and cannot mint other keys or change membership.

Secrets never logged

SDK keys, API keys and passwords are redacted from the request log.

Deployment

Your infrastructure, your data

One command to run the whole stack

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▊

  • Stateless API — scale it horizontally
  • AWS S3, MinIO, R2 or local disk
  • Config entirely through environment variables
  • Health check and Prometheus metrics built in

How the pieces fit

Bundle downloads bypass the API entirely, so the hot path stays a single indexed lookup.

CLI / CI
→
API
←
Dashboard
↓ metadata·↓ signed URL
PostgreSQL
S3 bucket
↑
React Native app

Bundles download straight from storage — they never touch the API.

Your next hotfix doesn't need a release train.

Run one command, embed a channel key, and ship your first bundle in a few minutes.