App store policies for OTA updates

Over-the-air updates are not a grey area. Both stores address them explicitly, both allow them, and both draw the line in the same place: you may replace interpreted code; you may not replace native code, and you may not change what the app is.

This page quotes the actual rules, says which document each one lives in, and turns them into a checklist you can run before a release. It is written from the official sources linked below — not from summaries — because the numbering people repeat from memory is usually a decade out of date. It is guidance, not legal advice.

The rule people cite as "3.3.2", and where it actually lives today

Almost every discussion of React Native OTA cites "Apple guideline 3.3.2". That number comes from the 2010-era iOS Developer Program License Agreement, and it is no longer where the rule is. Two documents matter today, and neither of them numbers it 3.3.2:

  • The Apple Developer Program License Agreement — the contract you accepted as a developer. The rule is §3.3.1(B), "Executable Code". (§3.3.2 in the current agreement is "Regulatory Compliance", about FDA/FAA/FTC clearances — nothing to do with updates.)
  • The App Store Review Guidelines — what a reviewer checks. The rule is §2.5.2.

Cite the right one and a conversation with review support gets much shorter.

What Apple's licence agreement actually says

From the Apple Developer Program License Agreement, §3.3.1(B) Executable Code (version LYL255, 18 August 2026):

Except as set forth in the next paragraph, an Application may not download or install executable code. Interpreted code may be downloaded to an Application but only so long as such code: (a) does not change the primary purpose of the Application by providing features or functionality that are inconsistent with the intended and advertised purpose of the Application (b) does not bypass signing, sandbox, or other security features of the OS; and (c) for Applications distributed on the App Store, does not create a store or storefront for other Applications.

Read that carefully, because it is more permissive than its reputation:

  • Downloading interpreted code is allowed outright. A JavaScript bundle is interpreted code. There is no requirement to ask permission, disclose the mechanism at review, or restrict updates to bug fixes.
  • The three conditions are the whole test. Not "the update must be minor". Not "the update must not add features". Adding a feature that fits the app's advertised purpose passes (a) cleanly.
  • Condition (b) is why code signing matters and why you must never ship a mechanism that downloads and executes native code.
  • Condition (c) is aimed at app-stores-inside-apps. Ordinary products never come near it.

The current agreement is always at developer.apple.com/support/terms — read §3.3.1(B) there rather than trusting any quote, including this one, a year from now.

What the App Store Review Guidelines say

From the App Store Review Guidelines, §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. Educational apps designed to teach, develop, or allow students to test executable code may, in limited circumstances, download code provided that such code is not used for other purposes. Such apps must make the source code provided by the app completely viewable and editable by the user.

Taken alone, "changes features or functionality" reads as a total ban — and this is the sentence that scares teams off OTA entirely. In practice it is read against the licence agreement's explicit permission for interpreted code, and the App Store has hosted React Native, Cordova and Flutter apps using OTA updates for a decade, with EAS Update and CodePush operating in the open the whole time. The operative constraint remains §3.3.1(B): stay inside your app's advertised purpose.

The conservative reading of the two together, and the one to design against: an OTA update should be something you would have been happy to ship as a normal store release. If you would not have submitted it, do not push it.

What Google Play says

The relevant policy is Device and Network Abuse, under Full Policy:

An app distributed via Google Play may not modify, replace, or update itself using any method other than Google Play's update mechanism. Likewise, an app may not download executable code (such as dex, JAR, .so files) from a source other than Google Play. 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).

The final sentence is the exemption OTA relies on: JavaScript running in the JS engine, with indirect access to Android APIs through the React Native bridge, is not the "executable code" the policy restricts. .dex, .jar and .so files are — never ship those over the air.

There is a second clause, added in October 2021, that applies directly:

Apps or third-party code, like SDKs, with interpreted languages (JavaScript, Python, Lua, etc.) loaded at run time (for example, not packaged with the app) must not allow potential violations of Google Play policies.

Meaning: an OTA update inherits every Play policy the binary is bound by. Your update pipeline is not a hole in the rules — if the delivered bundle would violate a policy, you have violated it. Practically: no new data collection the Data safety form does not declare, no ads behaviour your declaration contradicts, no content the rating does not cover.

What you can change, and what you cannot

Otapush ships a JavaScript bundle plus its assets. That boundary is not a product limitation — it is the same boundary the store rules draw.

Safe to change over the air:

  • The JavaScript bundle: business logic, screens, navigation, state, API calls, bug fixes.
  • Assets referenced by the bundle: images, fonts, JSON, Lottie files.
  • Text and localisation, styles and layout, feature flags and remote configuration read by JS.
  • New screens and new features within the app's advertised purpose.

Never over the air — these require a store release:

  • Native code. Any Swift, Objective-C, Kotlin, Java or C++ change; a new native module; a new third-party SDK with a native part; an Expo SDK upgrade. Otapush cannot ship it, and no OTA mechanism may.
  • Native configuration. Permissions, entitlements, Info.plist and AndroidManifest.xml entries, URL schemes, background modes, app icons and splash screens — all baked at build time.
  • Store metadata. App name, description, screenshots, age rating, privacy labels and Data safety declarations. These live in App Store Connect and Play Console, not in your bundle.
  • The app's primary purpose. A calculator that becomes a chat client fails Apple §3.3.1(B)(a) whether the change arrives by OTA or by store release — but by OTA it also looks like you were routing around review.
  • New data collection or new permission use. If the update starts reading location, contacts or the microphone, your privacy declarations must change first, and that is a store submission.

The runtime version is your enforcement mechanism

The rule "no native changes" is not left to discipline. Otapush matches runtimeVersion exactly: an update published for 1.0.0 is never served to a 1.1.0 binary, and never to 0.9.0. Bump the runtime version whenever the native side changes, and JavaScript that assumes a native module simply cannot reach a binary that lacks it. See Expo setup for how the runtime version is derived and when to bump it.

Safe rollout checklist

Run this before every OTA push. It is short on purpose.

  1. Native untouched? No package.json dependency with a native part added or upgraded, no Expo SDK bump, no config-plugin change. If any of those moved, this is a store release — and the runtime version must change with it.
  2. Would you have submitted this to review? If the honest answer is "no, that is why we are pushing it OTA", stop.
  3. Do the privacy declarations still hold? New endpoint, new analytics event, new device signal — check the App Privacy labels and the Play Data safety form before pushing, not after.
  4. Staged? Publish to staging, install a staging build on a real device of each platform, launch it twice (the first launch downloads, the second runs the new bundle). Simulators do not exercise the download path the way devices do.
  5. Promoted, not republished. Move the exact artefact you tested from staging to prod with otapush promote — rebuilding for prod means shipping something you never tested. See the CLI reference.
  6. Message written. One line per publish saying what changed and why. During an incident at 03:00 it is the only thing that makes the update list readable.
  7. Watched. Check the app's stats after the push: check counts keep rising, install counts follow. If checks climb and installs stay flat, devices are downloading and failing to launch — roll back now, diagnose afterwards. Note that the failure counter is not populated today (see Protocol), so a stalled install count is your real signal, alongside your crash reporter.
  8. Rollback rehearsed. Know before you need it: otapush rollback returns the channel to the previous update, and rolling back the only update on a channel sends every device back to the bundle compiled into the binary.

If your app is rejected

Rejection over an update mechanism is uncommon and usually specific. What has actually worked:

  • Read which rule was cited. A citation of Apple §2.5.2 or Google's Device and Network Abuse is about the mechanism; anything else — privacy, content, payments — is about what the app does, and the OTA pipeline is a red herring.
  • State plainly what you ship. In the Review Notes: the app downloads only a JavaScript bundle and its assets, interpreted by the React Native runtime already in the binary; no executable native code is downloaded; the update server is your own. This is the same architecture as EAS Update and CodePush, which is a fact a reviewer can verify independently.
  • Point at the licence clause, not the folklore. §3.3.1(B) permits downloading interpreted code under three conditions, and name how you meet each: the update does not change the app's advertised purpose, does not bypass OS signing or sandboxing, and does not create a storefront.
  • On Google Play, name the interpreter exemption. Quote the sentence about code that runs in a VM or interpreter, and state that no .dex, .jar or .so is ever downloaded.
  • If the rejection is about content in an update, fix the content. The mechanism is not what was rejected, and arguing about it wastes a review cycle.
  • Never argue that the store cannot see OTA updates. It is not true, and it is the argument that turns a routine rejection into a developer-account problem.

Who is responsible for what

With Otapush you operate the pipeline: your server, your keys, your audit trail. That helps in a compliance review — you can show exactly which bundle was served to which device and when — and it means store-policy compliance is entirely yours. Otapush cannot inspect what is in your bundle, and no update server can.

Questions and answers

Are OTA updates against App Store rules?

No. The Apple Developer Program License Agreement, §3.3.1(B), explicitly permits downloading interpreted code, subject to three conditions: the update does not change the app's advertised primary purpose, does not bypass OS signing or sandboxing, and does not create a storefront for other apps. A JavaScript bundle delivered by Otapush is interpreted code.

Can Apple reject an app for using its own update server?

Not for the server itself. The rules constrain what you download and what it does — interpreted code, inside the app's advertised purpose — not who hosts it. Self-hosting is no different from EAS Update or CodePush as far as the licence text is concerned.

Can I add a new feature over the air, or only fix bugs?

You can add features, as long as they fit the app's intended and advertised purpose. "OTA is only for bug fixes" is a widely repeated rule that appears in neither store's policy. What actually fails the test is a change that makes the app something other than what was reviewed.

Does Google Play allow this?

Yes. Device and Network Abuse forbids an app from updating itself outside Play's mechanism and from downloading executable code, then exempts "code that runs in a virtual machine or an interpreter" — which is exactly how a JavaScript bundle runs under React Native. Downloading .dex, .jar or .so files remains prohibited.

Do I need to disclose OTA updates in my privacy labels?

The mechanism itself is not a disclosure. What travels because of it can be: Otapush records a device identifier per protocol request for MAU — monthly active users, counted as unique devices per calendar month — so if that identifier is one your app supplies and considers personal, declare it accordingly. What matters far more is that an update must never start collecting data your existing declarations do not cover.

What if an OTA update breaks the app for everyone?

Roll back, and it costs one command. otapush rollback returns the channel to the previous update; devices pick it up on their next check. If the broken update was the channel's first, Otapush sends a rollBackToEmbedded directive and devices revert to the bundle compiled into the binary. Neither path needs the store.

Can OTA updates change the app icon, name, or permissions?

No. All three are compiled into the binary or held in store metadata. Icons and splash screens are build-time resources, permissions live in Info.plist and AndroidManifest.xml, and the app name is store metadata. Each requires a new submission.

Is a code-signed update required by the stores?

No store requires it. It is worth doing anyway: signing is what makes condition (b) of Apple §3.3.1(B) — not bypassing OS security features — an argument you can demonstrate rather than assert, and it stops an attacker who compromises your update server from executing code inside your app. Otapush generates the keypair for you and signs by default; see Protocol.

Does using OTA affect app review times?

No. Review examines the binary you submit. Updates you push afterwards do not queue for review — which is precisely why the rules constrain what they may contain.