react-brain
Browse decisions
React & language foundations 3App architecture 11UI 13Platform & native 9Build, test, observe, secure 6AI in React apps 3stack composerdecision recordscensusstaleness benchnewsletterarchitecturechangelogmethodologyroadmap

entriestooling-ops · verified 2026-09-24 · react-native

Over-the-air (OTA) JS updates & release channels (React Native)

reviewedconfidence: mediumthis tier holds 71% on the public scorecard (7/20 graded · 0 overturned) →

related decisions: rn-versions · brownfield · observability

cited by: brownfield · observability

re-verified 7× — 2026-09-24 · 2026-09-10 · 2026-08-18 · 2026-08-04 · 2026-07-17 · 2026-07-13 +1 · changelog

recommendation

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

optiontradeoffevidence
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 serverThe 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-updaterSelf-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.

evidence: npm weekly downloads (signals snapshot) · “ships in n/D” = adoption across the production-app census, honest denominators

Migration lines

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

github.com/microsoft/react-native-code-push

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.

Inside SWSH's automated mobile release pipeline with ExpoExpo / SWSH

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 production playbook for OTA updatesExpo

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.)

EAS Update introduction (docs)Expo

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 OTA Illusion: What No One Tells You About Instant Updates At Scale (RUC 2025)Adam Mruk (Callstack) — React Universe Conf 2025

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.)

Sources

The full explanation

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).

Related in tooling-ops: build · testing · dx · observability · security