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:
community servers: shm-open/code-push-server, lisong's
original; or self-host microsoft's repo
native binary untouched
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
the bundle changes how, never what
features, anything changing advertised purpose
features; license agreement 3.3.1(b) permits
interpreted code within purpose, security, and
no-storefront bounds; 2.5.19 kills hidden features
vm/interpreter exempt; runtime js may not enable
policy violations
revopush, capawesome, or self-host are the paths
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.
// sources
// 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.
