On Expo/EAS → EAS Update via the expo-updates client (channels + phased rollouts + rollback; diffing makes updates dramatically smaller since SDK 55/56). Hard self-host requirement → hot-updater, or the open expo-updates protocol with your own server. Anything still pointed at the retired CodePush service is silently broken — migrate.
Expo app → EAS Update (first-party; channels/rollouts/rollback built in)
self-hosted / no-vendor requirement → hot-updater, else XPREM when you want to keep the expo-updates client (its protocol behind your own bucket, with rollouts/rollback/A-B)
self-hosted AND you want a dashboard + CI integration rather than a protocol to operate → Codemagic Patch (0.x — evaluate with its local Docker Compose stack first)
huge fleet where full-bundle pushes cost real money (a 20MB Hermes bundle × 1M installs ≈ $1,700/push at CloudFront list) → EAS Update bsdiff diffing on Expo, else Delta (Zepto, self-hosted)
legacy CodePush app → code-push-server (standalone; keeps it alive while you migrate — the hosted service is gone)
the change touches native code → n/a: OTA cannot ship it; that's a store release (see RB-E-RN-VERSIONS)
RN embedded in a native app (brownfield) → EAS Update (Expo Updates works in isolated brownfield since SDK 55; see RB-E-BROWNFIELD)
Options & tradeoffs
the field considered — and why each one isn’t the default here
option
tradeoff
evidence
EAS Update (expo-updates client)
The hosted default: channels, phased rollouts, republish-to-rollback. SDK 55 added bsdiff bytecode/bundle diffing (est. ~75% smaller update downloads; default in SDK 56). expo-updates 57.0.23 (2026-09-18; 58.0.8 on next for SDK 58 beta; verified vs npm).
4.9M/wk · ships in 3/14
expo-updates + custom server
The Expo Updates protocol is open: self-host the server and keep the battle-tested client; more ops work than EAS. XPREM (next row) is the ready-made server.
XPREM (ex expo-open-ota)
Go implementation of the Expo Updates protocol: multi-app, progressive rollouts + instant rollback, A/B branch splitting, S3/GCS/Azure/R2 backends, no Expo account or token; open core, RBAC/SSO commercial. Renamed from expo-open-ota at v3. v3.2.4 (2026-09-23, verified vs GitHub releases). See note: XPREM.
hot-updater
Self-hosted open-source OTA (gronxb) positioned as the CodePush successor; younger than expo-updates. 0.36.15 (2026-09-22, verified vs npm); v1 in release candidates on GitHub since 2026-09-08. See note: HOT-UPDATER V1.
51k/wk
Codemagic Patch (@codemagic/react-native-patch)
Self-hosted OTA from the CodePush lineage with a dashboard, CLI, Expo config plugin and a CI vendor behind it — its edge is operability, not protocol. Still early: SDK 0.5.0 and server v0.5.0 (2026-09-23, verified vs npm + GitHub releases); evaluate, do not standardize. See note: CODEMAGIC PATCH.
code-push-server (standalone)
Microsoft's open-sourced server, compatible with the legacy react-native-code-push client — a self-host stopgap for existing CodePush apps, not a bet for new ones.
Delta (Zepto, OSS)
Self-hosted BINARY DELTA patching at quick-commerce scale (bsdiff patches to your S3, Lambda + CloudFront picks the patch per device). Zepto's own receipts: 80% adoption in 24h, 15M update requests/day, P90 patch download 305ms. @zepto-labs/react-native-delta 1.0.0 (2026-07-01, verified vs npm). See note: DELTA.
react-native-code-push (RETIRED service)
App Center and the hosted CodePush service shut down 2025-03-31; the client npm is dormant at 9.0.1 (2024-12-19, verified vs npm). Not recommended: migrate.
version and deprecation lines this decision tracks — for YOUR repo's sequenced plan: the doctor or npx -y @heart-it/react-brain migrate .
react-native-code-push → hot-updater (self-hosted successor) or expo-updates + EAS Update [dead · effort M]
the hosted CodePush service shut down 2025-03-31 and the client npm has been dormant since 2024-12 — anything still pointed at the service is silently broken; standalone code-push-server is a stopgap, not a destination
npm weekly downloads (from the corpus's last signals run): expo-updates 4.9M · react-native-code-push 26k · hot-updater 51k
Verified notes
OTA is the rollback story, not a convenience: republishing a known-good update is the fastest production mitigation React Native has, so run updates like releases — channels, staged percentage rollouts, a monitored bake period, abort vs republish-rollback as distinct mitigations. EAS Update is the default because those pieces are built in and its client now diffs bytecode. The hard boundary: store policy allows updating interpreted JS/assets within guidelines; native changes always require a store release.
CODEPUSH IS GONE (verified vs the microsoft/react-native-code-push repo): Microsoft retired App Center AND the hosted CodePush service on 2025-03-31, archived the repo and open-sourced the standalone code-push-server as the escape hatch → anything still pointed at the service stops updating without crashing, i.e. it is silently broken.
EAS UPDATE DIFFING (verified vs the SDK 55 changelog): binary bsdiff patches for Hermes bytecode — the client applies diffs against previously installed bytecode instead of re-downloading whole bundles, an estimated ~75% download reduction; opt-in in SDK 55, default in SDK 56.
HOT-UPDATER V1 (2026-09-24, verified vs the gronxb/hot-updater v1.0.0-rc.0 release): v1 moves to hybrid delivery — the server serves shared, cacheable Release Catalogs and the client decides which update to apply; in the project's own Cloudflare test, p95 update-check latency fell from 283.2 ms (v0) to 70.0 ms and cache hit rate rose from 0% to 96.9%. It is an RC and the release warns of breaking changes → stay on 0.36.x for production until 1.0.0 ships.
XPREM (verified vs the mercuretechnologies/xprem repo and the expo-open-ota v3 post): the concrete server for the expo-updates + custom server row. Stateless (bucket-only) or PostgreSQL control-plane mode. Renamed from expo-open-ota at v3 — the old name is still all over the ecosystem, so search both.
CODEMAGIC PATCH (2026-09-24, verified vs the codemagic-patch GitHub releases and npm): Codemagic ran a CodePush fork for ~18 months after Microsoft retired the service, then rebuilt it as a self-hosted OTA service — server + release worker, RN client SDK with an Expo config plugin, a `cmpatch` CLI, a web dashboard, and a one-command Docker Compose stack (Postgres + MinIO + dashboard) for local evaluation; positioned on CDN-first delivery, binary diffs and native fingerprinting. Server v0.3.0 (2026-09-07) added a CLI that installs, upgrades, backs up and restores a self-hosted server with guided Cloudflare/CloudFront/no-CDN setup, CloudFront as a delivery option, and structured failure detail on metric failures; breaking: an existing Cloudflare install with the old two-hour edge TTL rule must drop it to get the new cache policy. v0.4.0 (2026-09-14) extended the install wizard to external storage and DNS (R2, S3, GCS, CDN delivery verification) and added a localhost-only HTTP mode. v0.5.0 (2026-09-23) added GitLab sign-in and adoption charts and renamed SDK lifecycle events (`Ready`, `Applied`; SDK 0.4.x events are stored under the new names) → the upgrade runs a migration that rebuilds a `metric_event` index under an exclusive lock, so expect a longer upgrade on servers with a large metrics history. The SDK went 0.1.6 (first published 2026-07-29) → 0.5.0 in eight weeks: still an evaluation, not a standard.
DELTA (2026-07-17, RN Rewind #49; repo verified; npm package verified 2026-09-24): Zepto open-sourced Delta as the self-hosted binary-delta answer to full-bundle economics — their motivation math: a one-character fix × 1M installs = ~20TB ≈ $1,700 at CloudFront. delta-cli computes bsdiff patches between releases to your S3; delta-server (Lambdas behind CloudFront) picks the patch per device from an appId/jsVersion/bundleVersion manifest; the device applies bspatch and swaps on next launch. Adoption numbers are Zepto's own production telemetry. The SDK is published as @zepto-labs/react-native-delta (1.0.0, 2026-07-01), which this entry's detect rows do not yet cover.
CROSS-REFERENCES: RB-E-RN-VERSIONS owns store releases and the native-change boundary; RB-E-BROWNFIELD owns embedding constraints; RB-E-OBSERVABILITY owns the monitoring a bake period depends on (EAS Observe marks every OTA update).
HISTORY: 2026-09-24 CORRECTED: Delta was described as "GitHub-install, not yet on npm" — it has been on npm as @zepto-labs/react-native-delta since 2026-07-01; hot-updater 0.35 → 0.36 and v1 RC recorded; Codemagic Patch 0.1.6 → 0.5.0. 2026-09-10 REOPENED: Codemagic Patch server 0.3.0 (firsthand); SWSH E2E-gate-every-OTA-push reading. 2026-07-17 GAP CLOSED: Delta lane (Zepto binary-delta patching). 2026-07-09 GAP CLOSED: entry created — OTA delivery was a corpus gap surfaced by Native Weekly's 2026 run (bundle diffing, runtime channel switching, iteration speed, issues 12–14).
Canonical reading
Editorial annotations on why each piece matters — the articles themselves are the originals; read them there.
Fills the gap this entry's staged-rollout guidance leaves open — how to validate the update ITSELF before it ships. SWSH gates every EAS Update behind Meridian, an in-house end-to-end framework built on Appium and WebDriverIO and driven by Vitest, running critical flows against the updated bundle on iOS/App Clip/Android simulators in CI. The reasoning generalizes even though Meridian does not: an OTA update drops new JS onto a native binary already on users' devices, and ordinary pre-merge CI never exercises that pairing, so nothing confirms compatibility before real users do. Context for the scale claim: one React Native codebase covering iOS, an iOS App Clip, Android and web, ~300 builds a month on the team's own hardware, native releases about weekly.
The operational canon for this entry: staged percentage rollouts as exposure control, what to monitor while an update bakes, and aborts vs republish-rollbacks as distinct mitigations — 'sometimes it helps to go a little slower.' Vendor-authored but the staged-rollout discipline is platform-agnostic. (Body verified via the page's embedded content JSON — the blog is a JS shell.)
The reference for the hosted-default choice: how updates map to channels, phased rollouts to a percentage of users, and rollback-by-republish ('much like a new commit'). Read before designing a release-channel scheme.
Watching
Curated talks, podcasts & videos — the A/V companion to the reading list.
The scale half of this entry's delta-patching lane: why 'instant' OTA is an illusion at fleet size — full-bundle economics, adoption curves, and delivery latency — the problem statement Zepto's Delta and EAS bsdiff diffing both answer. (Verified via YouTube oEmbed: title + author.)
The reviewed long-form essay behind this entry — the why, not a how-to. Also on GitHub.
About over-the-air (OTA) JS updates & release channels (React Native)
Diataxis: Explanation. This page builds understanding of OTA delivery — the reasoning
behind the picks. It is not a setup guide. Store releases and native versioning are owned by
RB-E-RN-VERSIONS; brownfield constraints by RB-E-BROWNFIELD; the monitoring an update bake
relies on by RB-E-OBSERVABILITY. Read this for the why.
The one principle that organises everything: OTA is your rollback story, not a convenience
The seductive framing of OTA is "ship JS fixes without waiting for the store." The correct
framing is the inverse: republishing a known-good update is the fastest production mitigation
React Native has, and everything about how you run OTA should serve that. So run updates like
releases — channels, staged percentage rollouts as exposure control, a monitored bake period,
and abort vs republish-rollback as distinct mitigations learned before you need one. Respect
the hard boundary: OTA can update interpreted JS and assets within store guidelines, but a
native change can never ship OTA — that is a store release (RB-E-RN-VERSIONS). And know the
history: the hosted CodePush service died 2025-03-31, so "still pointed at CodePush" is the
silent failure mode of this category.
The default, and why
On Expo/EAS → EAS Update via the expo-updates client (channels + phased rollouts +
rollback, and diffing makes updates dramatically smaller since SDK 55/56). Hard self-host
requirement → hot-updater, or the open expo-updates protocol with your own server.
Anything still pointed at the retired CodePush service is silently broken — migrate.
EAS Update is the hosted default because the release-engineering pieces are built in: updates
map to channels, roll out phased to a percentage of users, and roll back by republishing
— "much like a new commit." The client got materially better in SDK 55: bsdiff diffing
applies patches against previously installed Hermes bytecode instead of re-downloading whole
bundles — an estimated ~75% smaller update downloads, opt-in in SDK 55 and default in
SDK 56. The self-host escape hatches are real: the Expo Updates protocol is open (bring your
own server, keep the battle-tested client), and hot-updater exists precisely as the CodePush
successor for no-vendor shops.
The landscape, and when each piece earns its place
EAS Update (expo-updates client) — the hosted default. Channels, phased rollouts,
republish-to-rollback; SDK 55 added bsdiff diffing (est. ~75% smaller downloads; default in
SDK 56). The first-party pick for any Expo app.
expo-updates + custom server — the protocol is open, so you can self-host the server and
keep the battle-tested client. More ops work than EAS; earns its place under a no-vendor rule.
hot-updater — self-hosted open-source OTA (gronxb), explicitly positioned as the CodePush
successor. Actively maintained (0.36.x as of 2026-09; v1 in release candidates, which warn of
breaking changes) but younger than expo-updates — the self-host pick when you don't want the Expo
client at all.
XPREM (ex expo-open-ota) — the ready-made server for the row above: a Go implementation of the
Expo Updates protocol with rollouts, instant rollback and A/B branch splitting on your own bucket.
Take it when you self-host but want to keep the expo-updates client.
Codemagic Patch — self-hosted OTA from the CodePush lineage whose edge is operability (a
dashboard, CLI and a CI vendor) rather than protocol. Still 0.x (0.5.0 as of 2026-09-23): evaluate
it, don't standardize on it.
Delta (Zepto) — self-hosted binary-delta patching for fleets where full-bundle pushes cost real
money (a 20MB Hermes bundle × 1M installs ≈ $1,700 per push at CloudFront list). On Expo, EAS
Update's bsdiff diffing answers the same economics first-party.
code-push-server (standalone) — Microsoft's open-sourced server, compatible with the legacy
react-native-code-push client. A self-host stopgap that keeps an existing CodePush app alive
while you migrate — not a bet for new ones.
react-native-code-push (RETIRED service) — App Center and the hosted CodePush service shut
down 2025-03-31; the repo is archived and the client npm has been dormant since 2024-12.
Migrate (urgency: dead) to hot-updater or expo-updates + EAS Update.
Tradeoffs and failure modes to name out loud
Still pointed at the dead service. The hosted CodePush shutdown doesn't crash your app —
it just stops updating it. Anything still targeting the retired service is silently broken;
standalone code-push-server is a stopgap, not a destination.
OTA run as a hotfix cannon, not a release. The production playbook is staged percentage
rollouts and a monitored bake period — "sometimes it helps to go a little slower." Shipping to
100% instantly forfeits the exposure control that makes OTA safe.
Conflating abort with rollback. They are distinct mitigations — halting an in-progress
rollout vs republishing a known-good update — and the time to learn the difference is before
the incident, not during it.
Trusting pre-merge CI to validate the update. An OTA update drops new JS onto a native binary
already on users' devices; ordinary CI never exercises that pairing. Gate each push behind an E2E
run against the updated bundle on the build it lands on (SWSH's pipeline is the worked example).
Expecting OTA to carry a native change. Store policy allows updating interpreted
JS/assets within guidelines; native changes always require a store release
(RB-E-RN-VERSIONS). Every OTA pipeline needs the "does this touch native?" gate.
Self-hosting as identity, not requirement. Both self-host paths cost real ops work, and
hot-updater is younger than expo-updates. Take them for a hard no-vendor requirement.
How it interacts with the rest of the stack
Native versioning (RB-E-RN-VERSIONS). The hard boundary: the moment a change touches
native code, OTA is out and the store-release machinery owned there is in — two halves of one
release story.
Brownfield (RB-E-BROWNFIELD). RN embedded in a native app can still get OTA — Expo
Updates works in isolated brownfield since SDK 55; the embedding constraints live there.
Observability (RB-E-OBSERVABILITY). A "monitored bake period" is only as good as the
monitoring behind it; the tooling that tells you whether to abort or roll back is owned there.
In one paragraph
OTA is your rollback story: republishing a known-good update is the fastest production
mitigation React Native has, so run updates like releases — channels, staged percentage
rollouts, a monitored bake period, and abort vs republish-rollback as distinct mitigations.
Default to EAS Update via the expo-updates client (SDK 55's bsdiff diffing, an estimated
~75% smaller downloads, default in SDK 56); with a hard self-host requirement take
hot-updater or the open expo-updates protocol on your own server. Native changes never ship
OTA — that's a store release (RB-E-RN-VERSIONS) — and the hosted CodePush service died
2025-03-31, so anything still pointed at it is silently broken and must migrate.
See also: RB-E-RN-VERSIONS (store releases; the native-change boundary), RB-E-BROWNFIELD
(Expo Updates in isolated brownfield since SDK 55), RB-E-OBSERVABILITY (the monitoring a bake
period depends on).