Every conversation about over-the-air updates on iOS arrives at the same question: "Doesn't Apple ban this?"

Short answer: yes, conditionally, it's allowed. Apple's Developer Program License Agreement (§3.3.1(B)) lets an app download and run interpreted code — never native executable code — provided the update doesn't change the app's primary purpose, doesn't bypass iOS signing or sandbox protections, and (for App Store apps) doesn't create a storefront for other code. App Review Guideline 2.5.2 is the reviewer-facing rule that enforces it.

The longer answer is written down in two documents, was shaped by one famous ban, and has a decade of public precedent behind it. What follows is a close reading of both documents and the enforcement record. It is not legal advice — and whichever tool is involved, the developer always owns compliance.

Guideline 2.5.2 and §3.3.1(B): the two rules that matter

App Review Guideline 2.5.2 is the reviewer-facing rule:

"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 sounds like a total ban. The carve-out lives in the contract every developer signed — the Apple Developer Program License Agreement, §3.3.1(B) in the current numbering (veterans still call it "3.3.2," its home before the agreement was reorganized in June 2022; Apple last revised the clause's wording in October 2025). The structure: an app may not download or install executable code — but interpreted code may be downloaded and run, provided it:

  • (a) does not change the app's primary purpose by providing features or functionality inconsistent with its intended and advertised purpose,

  • (b) does not bypass signing, sandbox, or other security features of the OS, and

  • (c) for apps distributed on the App Store, does not create a store or storefront for other apps or code.

That clause is the legal foundation under every OTA tool on iOS: CodePush ran on it for a decade, Expo's EAS Update runs on it today, and so does Patch. The conditions are worth taking one at a time.

Condition (a): the app must stay the same app

Fix bugs, tune copy, rework a flow, iterate features in a meditation app: consistent with advertised purpose. Ship an update that turns a flashlight app into a poker client: precisely what this clause exists to stop — review approved one app and users got another. (Guideline 2.3.1 attacks the same bait-and-switch from the "hidden features" angle, and it's the one that gets developer accounts terminated, not just builds rejected.)

Condition (b): stay inside the sandbox

The payload runs inside an interpreter inside the sandboxed process, with no new entitlements and no tampering with signed code. This is the condition native hot-patching failed in 2017 — more below.

Condition (c): no storefronts of code

Updating an app's own logic is fine. A marketplace of downloadable third-party mini-apps is a different product category with its own rules (guideline 4.7 explicitly permits HTML5/JavaScript mini-apps and games, streaming games, chatbots, plug-ins, and retro-game emulators — under its own conditions, 4.7.1–4.7.5).

The 2017 JSPatch ban: how Apple drew the line on OTA updates

The history explains the text.

2010: Apple briefly bans apps not originally written in C, C++, Objective-C — or JavaScript as executed by WebKit. That's the Flash cross-compiler war, and the telling detail is the exception: interpreted JavaScript run by Apple's own engine was blessed from the start. The ban on cross-compilers was relaxed within months; the JS-only blessing stayed for years.

2015: Microsoft ships CodePush. React Native apps update their JavaScript bundles in production, in the open. The JS runs in JavaScriptCore — Apple's own interpreter — so it sits squarely inside even the old, narrow text.

March 2017: the purge. JSPatch — a framework that downloaded JavaScript instructions which reached into the Objective-C runtime and redefined arbitrary native methods — had spread to thousands of apps. Apple mass-emails developers (the wording, reported at the time: "Your app, extension, and/or linked framework appears to contain code designed explicitly with the capability to change your app's behavior or functionality after App Review approval…") and begins rejecting apps that contain it. Rollout.io, a commercial product on the same mechanism, is swept up too. The security rationale wasn't theoretical: FireEye had shown a year earlier that JSPatch payloads delivered over unauthenticated channels handed a network attacker full access to private APIs through the JS–ObjC bridge.

June 2017: three months after the purge, Apple rewrites the clause into the engine-agnostic, condition-based text in force today — dropping the old WebKit/JavaScriptCore-only restriction. That ordering matters: Apple broadened the rule right after demonstrating it would enforce the line. The line was never "interpreted vs. not" alone; it was what the downloaded payload is allowed to touch.

Since then: Expo built a mainstream business on the pattern with EAS Update. Shorebird brought it to Flutter, including on iOS. The agreement moved the clause in June 2022 (3.3.2 → 3.3.1(B)) and revised its wording in October 2025 — reordering the conditions and scoping the storefront condition to App Store distribution — without touching the core carve-out.

The 2017 episode remains the most instructive data point, because Apple drew the line between two kinds of OTA in one enforcement action. Bundle-interpreting tools (CodePush) kept operating publicly through and after the purge — though individual apps using them have fielded 2.5.2 questions and occasional rejections over the years, typically resolved on resubmission or appeal (the CodePush issue tracker has examples). Runtime-patching tools died in a week.

What Apple bans: native code downloads, JIT, and runtime patching

The hard floor — none of this is gray:

MechanismStatusWhy
JS bundles run by a shipped interpreter (CodePush, EAS Update)Permitted under §3.3.1(B) conditionsInterpreted code; signed binary unchanged
WASM run by an on-device interpreter (Patch / WasmKit)Same pattern; no direct enforcement precedent yet (see below)Interpreted, no JIT, inside the sandbox
Downloading dylibs / frameworks / executablesBannedCode signing blocks it at the kernel
JIT compilation inside the appBannedNo writable+executable memory for third-party apps
Runtime patching of native methods (JSPatch, Rollout.io)BannedThe 2017 enforcement; fails condition (b)
Bait-and-switch feature unlocksBannedGuideline 2.3.1; account-level termination

On the JIT row, one precision note: JavaScript in a WKWebView does JIT — but in Apple's separate, brokered WebContent process, not the app's. Engines running in the app's process (JavaScriptCore via JSContext, Hermes) are interpreter-only, and the sole carve-out — EU alternative browser engines under BrowserEngineKit since iOS 17.4 — requires an Apple entitlement ordinary apps won't have.

Can Apple still reject the app? The honest gray zone

Three things worth saying plainly.

Review is policy plus judgment, not just text. A reviewer can ask what an update mechanism does, and Apple's interpretation of its own agreement is the one that counts. Any vendor selling "Apple-approved OTA updates" is overstating what can be promised — no such certification exists, for Expo, for Patch, for anyone. What exists is a written carve-out, a decade of public precedent, and — since 2017 — no category-wide enforcement against bundle-interpreting tools, while runtime patching got purged in a week.

The decade of precedent is JavaScript precedent. Worth being exact about: no WebAssembly OTA tool — Patch included — has CodePush's enforcement history yet. The argument that WASM sits in the same bucket is condition-based rather than hand-waved: Hermes bytecode is also a precompiled binary artifact, shipped OTA for years, treated as interpreted because an interpreter in the reviewed binary executes it. WASM bytecode run by a pure interpreter is the same shape. But Apple, not any vendor, gets the final word on that reading.

Plan for the reversal you hope never comes. The 2017 survivors' question is the right one: what if Apple changes its mind? For any tool built on this pattern the exit should be mechanical — patches disabled server-side immediately, the shipped binary keeps running its built-in code with no patch applied, and removing the SDK is one ordinary App Store release, not a rewrite. Whatever tool you adopt — Patch, Expo, Shorebird, or anything else on this pattern — should be able to give that answer before you need it.

If you got a "Guideline 2.5.2 - Performance - Software Requirements" rejection

That's the header App Review attaches when it believes an app downloads or executes code that changes features or functionality. It is not an automatic ban on OTA mechanisms: the productive response in the Resolution Center is to explain precisely what the mechanism does — downloaded interpreted code, run by an interpreter shipped in the reviewed binary, per §3.3.1(B) — and that updates stay within the app's advertised purpose. (In practice enforcement looks like a Resolution Center message or a scan-triggered email, not a conversation — the 2017 emails specifically named dlopen(), dlsym(), and method_exchangeImplementations() reached from remote sources. An interpreted-update design has no need for any of those APIs.)

Where WebAssembly sits

Patch is the concrete WASM case: it compiles changed Swift to WebAssembly and executes it on-device with WasmKit, a WebAssembly interpreter written in Swift. By Patch's own description, that means no JIT, no new executable pages, no native code downloaded, and a signed binary that never changes (a mechanism walk-through is here). Mechanically that's the same category as shipping JavaScript to JavaScriptCore or Hermes bytecode to Hermes: data, interpreted by code that shipped in the reviewed binary.

The conditions bind the developer, not just the tooling. A WASM patch that turns a todo app into a sportsbook violates condition (a) exactly as a JavaScript bundle would. A sensible checklist for any OTA mechanism, in any ecosystem:

  • Updates stay within the app's advertised purpose; major new features still go through a store release.

  • Nothing downloads further native executable code; nothing leaves the sandbox.

  • Updates are served over TLS and integrity-checked before activation.

  • Rollback is fast and tested, and there's an audit trail of what shipped when.

  • App Review can be answered in one sentence: "the app downloads interpreted code, run by an interpreter in the binary, per §3.3.1(B)."

FAQ

Is CodePush allowed by Apple?

The mechanism — downloading a JavaScript bundle run by an interpreter in the signed binary — is the pattern §3.3.1(B) permits, and it operated publicly from 2015 until CodePush's retirement in March 2025. Individual apps still fielded occasional 2.5.2 rejections, usually resolved on appeal; the developer always owns compliance.

Why did Apple ban JSPatch in 2017?

JSPatch downloaded instructions that redefined arbitrary reviewed native methods through the Objective-C runtime — behavior-changing native patching, with a documented man-in-the-middle risk. That fails the security condition of the interpreted-code clause; bundle-interpreting tools were left standing in the same purge.

Can iOS apps run WebAssembly?

Yes — interpreted. There's no JIT for third-party apps, so a WASM runtime on iOS must be a pure interpreter (that's what WasmKit is). Downloaded WASM then sits in the same DPLA category as downloaded JavaScript: interpreted code, subject to the same three conditions.

That's the rulebook: interpreters yes, native patching no, the carve-out in some form since 2010 and in its condition-based form since 2017 — and the app has to remain the app users were promised.


Related: How OTA updates work on iOS · Compiling Swift to WebAssembly