45 // journal

Over-the-air updates: what the app stores allow after CodePush

guide · 2026-11 · 5 min read · app stores · react native · clients

journal/ota-update-rules
Over-the-air updates: what the app stores allow after CodePush

fig.01 · what may ship over the air, and what still needs a binary

A payment bug lands in your app on a Friday. The fix is four lines of JavaScript, but the store review queue does not care, and your users will hit the bug all weekend. Over-the-air updates exist for exactly this: the app downloads a new JS bundle and assets, applies them on next launch, and the store binary never changes. Both stores allow it, the rules have real teeth, and the tooling landscape shifted when CodePush died. Worth knowing the actual lines before you ship the mechanism.

The Apple line

The reviewer-facing rule is guideline 2.5.2: "Apps should be self-contained in their bundles, and may not read or write data outside the designated container area, nor may they download, install, or execute code which introduces or changes features or functionality of the app, including other apps." Read alone, that bans OTA outright. The carve-out is not in the guidelines at all: it lives in the Developer Program License Agreement, §3.3.1(B), outside the review document entirely. It permits interpreted code where the download does not change the app's primary purpose or advertised features, does not bypass signing, sandbox, or other OS security features, and does not create "a store or storefront for other Applications." JavaScript running inside your app is interpreted code, which is why React Native, Expo, and Capacitor updates are legal.

Two more rules frame the same boundary. Guideline 4.7 opens with "Apps may offer certain software that is not embedded in the binary, specifically HTML5 and JavaScript mini apps and mini games, streaming games, chatbots, and plug-ins," Apple's explicit acknowledgment that non-embedded JS is a normal thing apps do. And as of the guidelines revision dated October 16, 2026, hidden features got their own guideline: 2.5.19, under which attempting to hide undocumented features is grounds for removal and developer-account termination. An OTA channel that wakes up functionality the reviewer never saw is exactly what that clause exists for.

The Google Play line

Play states the same boundary more plainly, in its Device and Network Abuse policy: an app "may not modify, replace, or update itself using any method other than Google Play's update mechanism," and may not download executable code "such as dex, JAR, .so files" from outside Play. Then the sentence OTA depends on: "This restriction does not apply to code that runs in a virtual machine or an interpreter where either provides indirect access to Android APIs (such as JavaScript in a webview or browser)." Your JS bundle is fine; a downloaded .dex file is not. The same policy adds that interpreted languages loaded at runtime "must not allow potential violations of Google Play policies," meaning the update channel inherits liability for whatever it delivers.

What CodePush's death changed

For years the default implementation was Microsoft's CodePush on App Center. Visual Studio App Center retired on March 31, 2025: after that date you could not sign in or make API calls, and CodePush, Build, Test, and Distribute all died together. Only Analytics and Diagnostics got extensions, first to June 30, 2026, then to the end of March 2027 in an April 2026 update. Microsoft's standalone code-push-server was archived on GitHub weeks later, though its README survives: it is "fully compatible with CodePush React Native Client SDK" and deployable on your own infrastructure.

The successors split by framework:

EXPO
Expo Update (formerly EAS Update): the mature path
RN
revopush, codepush-api compatible, ~16k weekly dl;
community servers: shm-open/code-push-server, lisong's

original; or self-host microsoft's repo
CAPACITOR
capawesome live updates: ota for the web layer only,
native binary untouched
SELF-HOST
expo updates protocol is an open spec; the code-push
repos deploy standalone; both mean running infra

What an update may actually change

Expo's own framing is the cleanest: OTA can carry JS, styling, images, and content; a new binary is required for native modules, permissions, SDK upgrades, and config that touches native code. Both stores converge on the same test, stated differently: does this update change what the app fundamentally is or does, versus how it does it. Bug fixes, copy, layout, business logic, content: over the air. New permissions, new native capability, features a reviewer never saw, anything that changes the "intended and advertised purpose": a binary, through review. And one trap worth naming: guideline 3.1.1 bans turning on paid functionality through your own mechanisms, so an OTA flip that enables premium features without in-app purchase is a second violation stacked on the first.

Our position

OTA is a patch channel, not a release channel. We wire it for the crash fix, the stale copy, the broken deeplink: changes small enough that describing them in a review note would be easy. Anything that would make us hesitate to describe it goes in a binary. Operationally that means three habits: the update payload is signed and the app verifies it before applying (a hijacked update channel is a supply-chain hole), releases roll out staged with a kill switch, and the OTA floor stays matched to the store binary so an old install never receives a bundle built for newer native code. The stores' version of this test is a smell test, and the working rule is simple: if you would not want to explain the diff to a reviewer, do not send it over the air.

The takeaway

ALLOWED
js, logic, styling, images, content, copy, bug fixes
the bundle changes how, never what
BINARY
native modules, permissions, sdk upgrades, new
features, anything changing advertised purpose
APPLE
guideline 2.5.2 bans downloaded code that changes
features; license agreement 3.3.1(b) permits

interpreted code within purpose, security, and

no-storefront bounds; 2.5.19 kills hidden features
PLAY
no self-update outside play; interpreted code in a
vm/interpreter exempt; runtime js may not enable

policy violations
CODEPUSH
dead with app center, march 31 2025; Expo Update,
revopush, capawesome, or self-host are the paths
OPERATE
sign every bundle, stage rollouts, keep a kill
switch, match the ota floor to the store binary

Update plumbing like this is part of the release machinery we run for apps like CashTaq in our custom software work, alongside the store rules in both app stores force a yearly update. If your OTA channel predates CodePush's death, we can port it.

// next step

We work this way on client projects too.

Tell us what you are making. We reply with a plan, a timeline, and a price.

← the journal