# Patch Docs — full text > Over-the-air code updates for native Swift iOS apps. Compile changed Swift to WebAssembly and ship it without App Store review. Generated from src/content/docs (scripts/gen-llms.mjs). --- # Patch documentation URL: https://docs.patchrelease.com/ Section: Start here Description: Patch ships over-the-air code updates to native Swift iOS apps: patchcli compiles changed Swift to WebAssembly, PatchSDK runs it on-device, no App Store review. **Patch** ships over-the-air code updates to native Swift iOS apps. You write ordinary Swift; the build engine works out which parts can run as WebAssembly, compiles those, and ships them as a small module that runs on-device in [WasmKit](https://github.com/swiftwasm/WasmKit). Your signed App Store binary never changes — only the interpreted layer updates. ```bash brew install patch-release/tap/patchcli cd MyApp && patchcli init ``` - [Quick Start](https://docs.patchrelease.com/quickstart/): Install the CLI and ship your first patch in about ten minutes. - [How it works](https://docs.patchrelease.com/how-it-works/): The Swift→WebAssembly pipeline and the on-device runtime. - [What it can & can't update](https://docs.patchrelease.com/coverage/): Measured coverage, and the limits that are permanent. - [The fingerprint](https://docs.patchrelease.com/fingerprint/): Why a patch stops applying after you change native code. ## If you've used CodePush or Expo Patch is the equivalent for **native Swift and SwiftUI** — no JavaScript bridge, no web views, no cross-platform runtime. CodePush and Expo/EAS Update patch a JavaScript bundle; Shorebird patches Dart. None of them touch native Swift. The side-by-side table is on [How Patch compares](/compare/), and the marketing site covers each one individually: [vs CodePush](https://patchrelease.com/codepush-alternative), [vs EAS Update](https://patchrelease.com/expo-eas-update-alternative), [vs Shorebird](https://patchrelease.com/shorebird-alternative). **Is this allowed?** Yes — under Apple's Developer Program License Agreement §3.3.1(B) (formerly §3.3.2), the interpreted-code provision. Shorebird's FAQ cites the same clause by number for Flutter. The signed binary is never modified and only interpreted code updates. See [Apple compliance](/apple-compliance/). ## Open source - [The packages](https://docs.patchrelease.com/open-source/): The SDK (MIT) and the engine (Apache-2.0) — both in patch-swift, what's in each, and how to build from source. - [Running it yourself](https://docs.patchrelease.com/self-hosting/): Reference architectures for GCP, AWS and Azure, and what static hosting can't do. ## Related - [FAQ](/faq/) — short answers about Apple's rules, coverage, rollback and pricing - [Glossary](/glossary/) — OTA update, native shell, fingerprint, host bridge, PMOD - [How Patch compares](/compare/) — Patch, CodePush, EAS Update and Shorebird - [Patch vs CodePush](https://patchrelease.com/codepush-alternative) - [Patch vs Expo EAS Update](https://patchrelease.com/expo-eas-update-alternative) - [Patch vs Shorebird](https://patchrelease.com/shorebird-alternative) - [OTA update tools compared](https://patchrelease.com/ota-update-tools-compared) --- # Quick Start URL: https://docs.patchrelease.com/quickstart/ Section: Start here Description: Install patchcli, run patchcli init to register the app and wire the SDK, then ship your first OTA patch with patchcli release — about ten minutes end to end. Setting Patch up takes three commands: `brew install patch-release/tap/patchcli` installs the CLI, `patchcli setup` installs the Swift-to-WebAssembly toolchain, and `patchcli init` registers your app, adds the PatchSDK package, wires the startup code and makes your SwiftUI views patchable. After that you write ordinary Swift and ship it with `patchcli release`. The whole path below is about ten minutes. ## 1 · Install the CLI + toolchain (once per machine) Patch compiles Swift to WebAssembly with the swift.org toolchain plus the WebAssembly Swift SDK. The Apple/Xcode toolchain cannot target WebAssembly. ```bash title="install — zsh" # 1 · the Patch CLI $ brew install patch-release/tap/patchcli # 2 · the Swift→WebAssembly toolchain — ONE command installs + checksum-verifies # the swift.org toolchain + WebAssembly SDK (Xcode's can't target wasm) $ patchcli setup (downloads the pinned swift.org toolchain + WASM SDK from swift.org) ``` ## 2 · Set up your app — one command From your app's directory, run `patchcli init`. One command does the whole setup, skipping anything already done (it's safe to re-run): 1. **Detects** the Xcode project, build target, bundle id, and app icon. 2. **Registers the app** — your browser opens `app.patchrelease.com/cli-connect`; sign in (or create a free account) and click **Register app**. The CLI receives the app key **and a publish token** automatically within seconds and writes `app_key`/`publish_token`/`app_id`/`workspace_id` into `.Patch.yml` — no copying credentials. 3. **Adds the PatchSDK Swift package** to the Xcode project (exactly what Xcode's "Add Package Dependencies…" would write; a backup is kept at `project.pbxproj.patch-backup`), then resolves packages. 4. **Proposes a diff** inserting the Patch startup code into your `@main` App struct — applied only after you confirm (`y/n`). 5. **Makes your SwiftUI views patchable** — routes each view `body` through Patch and generates the per-view route thunks (they build in Debug and optimized Release/archive builds), so a future OTA patch re-renders your views with **no `PatchView` wrapping and no changes to your views**. This runs **automatically on every `patchcli release` / `push` / `build`**, so views you add later are picked up with no extra step. (You can still run `patchcli prepare --check` in CI to verify, or `--no-prepare` to opt out.) `init` then **builds the prepared project** (`prepare --verify`): any view whose prepared code doesn't compile is kept native automatically, and `init` ends with a compatibility summary naming each native view and why. `init` builds Debug; add `--verify-release` to also build the configuration the app archives with (usually Release — one more full optimized build, often several minutes). Skip the build with `--no-verify`. Generated code lives in `Patch/Generated/`, not in your view files — see [where prepare puts generated code](/cli/#where-prepare-puts-generated-code). `patchcli unprepare` removes all of it. ```bash title="~/MyApp — zsh" $ patchcli init Patch init ========== Detected Xcode project: MyApp.xcodeproj (target MyApp · com.example.myapp) Opening browser to register the app… ✓ app key + publish token received Wrote: /Users/you/MyApp/.Patch.yml Adding PatchSDK package… ✓ resolved (backup: project.pbxproj.patch-backup) Proposed change to MyApp.swift — apply? (y/n) y ✓ startup code inserted ✓ MyApp is set up — build & run, then ship with: patchcli release ``` The startup code it inserts (the same integration you'd write by hand — `configure` once, `start()` activates the best cached module immediately, offline-safe, then checks for an update in the background). `init` bakes in your `appID` and the current native-shell `fingerprint`; with only the `appKey` the SDK (≥ 1.0.3) still works — the backend resolves the key, and updates are served best-effort until a fingerprint is reported: ```swift title="MyApp.swift" @main struct MyApp: App { init() { Patch.configure(.init( appKey: "pak_live_…", // your key, from .Patch.yml appID: "3f2b…-uuid", // baked in by patchcli init fingerprint: "3f2b…")) // this build's native shell Task { await Patch.shared.start() } } var body: some Scene { WindowGroup { ContentView() } } } ``` :::note[Two credentials, and only one of them is secret] `app_key` (`pak_…`) is compiled into your binary, so anyone with a published build can extract it. That's fine — it identifies your app when a device asks for an update, and it authorizes nothing. Publishing uses a **separate** credential, the publish token (`ppt_…`), which never goes near your app. `patchcli init` stores it in `.Patch.yml`; `patchcli login` gets you another. In CI, put a publish token in `PATCH_API_KEY`. Sending an app key where a publish token belongs is refused with a message telling you to run `patchcli login`. See [SECURITY.md](https://github.com/patch-release/patch-swift/blob/main/SECURITY.md). ::: Anything `patchcli init` can't do safely — no bundle id detected, an unusual project layout, a UIKit `AppDelegate` app, a declined confirmation, or no network — falls back to printing exact manual steps. See [Manual setup](#manual-setup-the-fallback) for the same steps, and the [CLI reference](/cli/) for flags like `--manual`, `--no-open`, and `--yes`. ## 3 · Write a patchable Swift function No annotations needed — just write pure logic. Patch detects that this function is WASM-safe (it only touches `Decimal` and value types) and lifts it into the module. The call site stays exactly the same. ```swift title="Pricing.swift" enum Pricing { // Pure logic → compiled to WASM, updatable over the air. static func orderTotal(items: [LineItem], promo: String?) -> Decimal { var subtotal = items.reduce(Decimal(0)) { $0 + $1.price * Decimal($1.qty) } if promo == "LAUNCH20" { subtotal *= 0.80 } var total = Decimal() NSDecimalRound(&total, &subtotal, 2, .bankers) return total } } ``` ## 4 · Register your App Store build's fingerprint After your next App Store build ships, run this once. Pushes are gated on the [native-shell fingerprint](/fingerprint/), so the backend only ever serves modules compatible with installed apps. Re-run it after each App Store release to re-baseline. ```bash title="~/MyApp — zsh" $ patchcli fingerprint register ✓ Registered native-shell fingerprint 3f2b… for MyApp ``` ## 5 · Release it `patchcli release` builds the module and pushes it to the backend in one command (build → fingerprint gate → upload → activate). Edit `Pricing.orderTotal`, then release the fix to 10% of devices to start. ```bash title="~/MyApp — zsh" $ patchcli release --rollout 10 --message "Fix LAUNCH20 promo math" building… → .Patch/build/module.wasm (38.2 KB) Fingerprint: ✓ compatible ✓ Pushed module 2026.06.03.142210 Rollout: 10% Active: true ``` Re-launch the app: the SDK fetches, verifies, and hot-swaps the new module. Watch adoption with `patchcli status`, widen the rollout, or `patchcli rollback` if anything looks wrong. That's the whole loop. **Local vs CI** Running `patchcli release` from your machine is great for the first end-to-end test. For production, wire it into CI so deploys are reproducible and credentials never live on a laptop — see [CI/CD](/cicd/). ## Manual setup (the fallback) Everything `patchcli init` automates can be done by hand — and `init` prints these exact steps whenever it can't do something safely (or when you run `patchcli init --manual`): 1. **Add the SDK package** — in Xcode, **File → Add Package Dependencies…** with `https://github.com/patch-release/patch-swift` (product `PatchSDK`, from `1.8.0`). See [SDK reference](/sdk/) for the `Package.swift` form. 2. **Create the app** in the [dashboard](https://app.patchrelease.com) to get its `pak_…` app key. 3. **Paste the key** — set `app_key` (and optionally `app_id`/`workspace_id`) in [`.Patch.yml`](/cli/#configuration-patchyml). 4. **Get a publish token** — run `patchcli login`, or create one in the dashboard and set `publish_token` in `.Patch.yml`. Without it, `push`/`release` cannot authenticate. 4. **Add the startup code** — the `import PatchSDK` + `Patch.configure` / `Patch.shared.start()` snippet from step 2 above, in your `@main` App struct's `init()`. ## Related - [FAQ](/faq/) — Apple's rules, what can be patched, rollback, pricing - [Set up with AI](/ai-setup/) — hand the same flow to a coding assistant - [The compatibility fingerprint](/fingerprint/) — the one gate to understand first - [Troubleshooting](/troubleshooting/) — when a step does not go to plan - [Update an iOS app without App Store review](https://patchrelease.com/blog/update-ios-app-without-app-store-review) --- # How it works URL: https://docs.patchrelease.com/how-it-works/ Section: Start here Description: Patch splits one Swift codebase into a WebAssembly module of updatable code and a native shell that holds everything else and calls into it on-device. Patch turns one Swift codebase into two cooperating halves: a **WASM module** with the safe, updatable code, and a **native shell** that holds everything else and calls into it. You write ordinary Swift — the build engine decides what's safe to ship over the air. At a glance: | Stage | What happens | | --- | --- | | **Swift source** | Your ordinary `.swift` files. No annotations. | | **Partition** | Patch sorts safe, updatable code from must-stay-native code. | | **WASM module** | Safe code compiled to a real `.wasm`, kept as small as possible. | | **Device** | WasmKit runs the module; the SDK hot-swaps updates. | ## What's updatable Patch updates **logic, and your SwiftUI views** (structure, state, and interaction). Full `async`/`await` concurrency runs in the WASM module, and the SwiftUI lowering covers the declarative view body, state, and user interaction; only the lowest-level platform rendering and must-stay-native APIs remain in the shell. Every function lands in one of four buckets, in safety order: | Bucket | Meaning | | --- | --- | | **WASM** | pure logic → runs OTA | | **BRIDGED** | uses a host bridge → runs OTA | | **SPLIT** | mixed → safe part lifted to WASM | | **NATIVE** | stays in the App Store binary | - **WASM-eligible** — pure logic plus WASM-safe Foundation value types (`Decimal`, `Date`, `UUID`, `JSONEncoder`/`Codable`, formatters, `URL`, regex). Ships as WASM. - **Bridged** — touches only APIs Patch has a host bridge for: networking, `UserDefaults`, notifications, navigation, keychain, date/locale, logging, analytics, file storage, connectivity, biometrics, in-app review, pasteboard, haptics, device info, share sheet, open-URL, location, calendar, contacts, app badge, mail compose, in-app purchase, speech synthesis, document picker, system sound, photo picker, secure random, app-group storage, screen control, audio playback/recording, motion, maps, network/remote images, Spotlight, background tasks, file download, accessibility, app shortcuts, Handoff, Watch connectivity, NFC, process info, video playback, speech recognition, now-playing, image filters, PDF/QR generation, and camera — **51 bridges** in all. Runs OTA, calling the native shell for the host part. - **Mixed** — part native, part safe. Patch lifts the safe parts over the air and keeps the rest in the shell, wiring the two together automatically. - **Native** — touches a must-stay-native API (low-level UIKit/AppKit rendering, AVFoundation, CoreLocation, HealthKit, Core Data, file system, threads/locks, ObjC runtime, unsafe pointers). Stays in the App Store binary. When in doubt, Patch keeps code native — so coverage is the only thing that's ever at stake, never safety. For measured, per-app coverage on a real validation corpus, see [What Patch can & can't update](/coverage/). **Safety first** Updates can only ever swap out logic Patch already proved is safe to run as WASM. If a generated module fails to compile, the offending code is auto-demoted to native and the build retries — a broken module is never produced. On-device, if a module is missing, corrupt, or fails to activate, the app runs the native fallback baked into the App Store binary. Updates never break the app. ## Compatibility fingerprint & packaging tiers Patch computes a deterministic SHA-256 **native-shell fingerprint** over everything that fixes the binary's layout — SDK + WasmKit version, bridge definitions, native `.swift` files, Info.plist, entitlements, linked frameworks, deployment target, and compiler version. If any of these change, the OTA module is incompatible with installed apps: `push`/`release` refuse and tell you to ship through the App Store and re-register. The OTA-updatable code is deliberately excluded from the fingerprint, so a pure-logic patch is always compatible. Patch also picks the smallest viable **packaging tier** per module, so OTA patches stay tiny while keeping full Foundation semantics by borrowing the native shell's real Foundation: | Tier | Size | When it's used | | --- | --- | --- | | **T0 · Embedded Swift** | tens of KB | The default. No Foundation/ICU in the module — Foundation values (Decimal, JSON, Date) are satisfied by host bridges into the shell's real Foundation. | | **T1 · Stdlib only** | ~1.1 MB | For code Embedded rejects but that needs no Foundation (e.g. `any P` existentials). Standard library, no `import Foundation`. | | **T2 · Full Foundation** | ~11.7 MB | The fallback when a module needs in-module Foundation no bridge covers (`Mirror`, in-module `Codable` synthesis, `NSRegularExpression`, formatters). | Patch uses the lightest tier a module can run on, only stepping up when the code genuinely needs more. Any heavier base ships once; every update after that is a tiny compressed **binary diff** against the version already on the device — typically a few hundred bytes to tens of KB. ## Related - [What Patch can & can't update](/coverage/) — the measured coverage map - [The compatibility fingerprint](/fingerprint/) — why a patch stops applying - [Glossary](/glossary/) — PMOD, WasmKit, Embedded Swift, host bridge, thunk - [Compiling Swift to WebAssembly](https://patchrelease.com/blog/swift-to-webassembly) - [SwiftUI over the wire](https://patchrelease.com/blog/swiftui-over-the-wire) - [WasmKit: a WebAssembly runtime in Swift](https://patchrelease.com/blog/wasmkit-webassembly-runtime-swift) --- # Set up with AI URL: https://docs.patchrelease.com/ai-setup/ Section: Start here Description: Copy-paste prompts that have Claude Code, Cursor or Copilot install patchcli, run patchcli init, and wire PatchSDK into your Xcode project for you. An AI coding assistant can install Patch for you: hand it one of the prompts on this page and it runs the documented onboarding itself. The prompts drive the same [Quick Start](/quickstart/) flow — install `patchcli`, run `patchcli init` (which registers the app, adds the PatchSDK package, injects `Patch.configure(…)`, and makes your SwiftUI views patchable), then ship an update with `patchcli release`. There is nothing AI-specific to install; the assistant is only driving the CLI. Claude Code, Cursor, Windsurf and GitHub Copilot all work. **Open your iOS project first** Point your assistant at the directory that contains your `.xcodeproj` / `.xcworkspace` before pasting, so it runs in the right place. `patchcli init` is safe to re-run — it skips any step that's already done. ## Prompt — assistant with terminal access Use this if your assistant can run shell commands (Claude Code, Cursor agent mode, Windsurf Cascade, etc.). Copy it, paste it into the chat, and let it run the CLI. ```text title="paste into your AI assistant" Please set up Patch (OTA over-the-air code updates for native Swift iOS apps) in this project. The official docs are at https://docs.patchrelease.com (LLM-readable: https://docs.patchrelease.com/llms.txt and https://docs.patchrelease.com/llms-full.txt). Follow the real onboarding flow: 1. Install the CLI with Homebrew: brew install patch-release/tap/patchcli Then install the Swift→WebAssembly toolchain it needs (one command, verifies checksums): patchcli setup 2. From this project's root directory, run: patchcli init This single command will: detect the Xcode project/target/bundle id; register the app via a browser link flow (it opens app.patchrelease.com/cli-connect — I'll sign in and click "Register app", and the CLI receives the app key automatically); add the PatchSDK Swift package (https://github.com/patch-release/patch-swift); propose a diff that inserts Patch.configure(appKey:appID:fingerprint:) plus Patch.shared.start() into my @main App struct (ask me before applying); and make my SwiftUI views patchable (route each view body through Patch + generate the route thunks). Note: this view-prep step ALSO runs automatically on every "patchcli release" / "push" / "build", so views I add later are picked up with no extra step — I never have to remember to run "patchcli prepare" manually. Useful flags: --yes (accept the code diff automatically), --no-open (don't auto-open the browser), --skip-package / --skip-code (skip a step), --manual (just print the manual steps). Do NOT pass --yes unless I ask. 3. Verify the setup: - The PatchSDK package is added to the Xcode project (and the project still resolves/builds). - "import PatchSDK" and a Patch.configure(...) + Patch.shared.start() call exist in my @main App struct's init(). - my view bodies are routed (`__patchRoute { … }`) and the route thunks were generated. (This re-runs automatically on every "patchcli release" so views I add later are picked up; I can run "patchcli prepare --check" in CI to verify, or pass "--no-prepare" to opt out.) - .Patch.yml exists in the project root with app_key / publish_token / app_id. 4. After my next App Store build, I should run "patchcli fingerprint register" once to baseline the native shell (pushes are gated on this fingerprint). Then explain, briefly, how I ship an update: I edit my Swift normally, then run patchcli release --rollout 10 --message "what changed" which builds the WASM module, checks the fingerprint gate, uploads, and activates it to 10% of devices — no App Store review. The on-device SDK fetches and hot-swaps it on next launch. I widen the rollout with the dashboard or CLI, and "patchcli rollback" reverts instantly. If any step can't run (no Homebrew, no network, an unusual project layout, or a UIKit AppDelegate app), tell me exactly which step failed and fall back to the manual instructions at https://docs.patchrelease.com/quickstart/#manual-setup-the-fallback. Don't invent flags or steps — only use what's in the docs above. ``` ## Prompt — assistant without terminal access Use this if your assistant edits files but can't run a shell (e.g. an in-editor chat with no command execution). It will make the in-project code changes and hand you the two commands to run yourself. ```text title="paste into your AI assistant" Help me set up Patch (OTA over-the-air code updates for native Swift iOS apps) in this project. You can't run shell commands, so do the file edits you can and give me the exact commands to run myself. The docs are at https://docs.patchrelease.com (LLM-readable: https://docs.patchrelease.com/llms.txt). Follow the real flow: 1. Tell me to run these two commands in a terminal myself, in this order: brew install patch-release/tap/patchcli patchcli init Explain that "patchcli init" does most of the setup automatically: registers the app in the browser (app.patchrelease.com/cli-connect), adds the PatchSDK package, proposes the Patch.configure(...) code diff, and makes my SwiftUI views patchable. Tell me this view-prep step also runs automatically on every "patchcli release" (so views I add later need no extra step), that it's safe to re-run, and that it falls back to printing manual steps if anything can't be done automatically. 2. As a fallback (the manual path at https://docs.patchrelease.com/quickstart/#manual-setup-the-fallback), prepare these edits so I'm ready either way: - In Xcode I'll add the Swift package https://github.com/patch-release/patch-swift (product PatchSDK, from 1.8.0) via File → Add Package Dependencies… — or add it to my Package.swift if I have one; show me the Package.swift dependency + target lines. - In my @main App struct, add "import PatchSDK" and, in init(), call: Patch.configure(.init( appKey: "pak_…", // from .Patch.yml after I run patchcli init appID: "…", // baked in by patchcli init fingerprint: "…")) // this build's native shell Task { await Patch.shared.start() } Show me the edit in context against my actual App struct. 3. Tell me that after running "patchcli init" I should confirm a .Patch.yml file appears in the project root with app_key / publish_token / app_id, and that my SwiftUI view bodies were routed through Patch. (This view-prep re-runs automatically on every "patchcli release", so I never have to remember it when I add new views.) Then explain how I ship an update: edit Swift normally, then run patchcli release --rollout 10 --message "what changed" which compiles the change to WebAssembly and ships it as an OTA patch (no App Store review); the SDK hot-swaps it on next launch, and "patchcli rollback" reverts instantly. Don't invent flags or steps — only use what's in the docs. ``` **Tip** Whichever variant you use, the assistant is just driving the documented `patchcli` commands — there's nothing AI-specific to install. If you'd rather do it yourself, the [Quick Start](/quickstart/) walks through the same steps by hand, and [Manual setup](/quickstart/#manual-setup-the-fallback) lists the fallback for unusual projects. ## Related - [https://docs.patchrelease.com/llms.txt](https://docs.patchrelease.com/llms.txt) — the docs page index, in plain text for agents - [https://docs.patchrelease.com/llms-full.txt](https://docs.patchrelease.com/llms-full.txt) — every docs page inlined as one plain-text file - [Quick Start](/quickstart/) — the same steps by hand - [CLI reference](/cli/) — every command and flag, captured from the binary - [FAQ](/faq/) — the questions an assistant is most likely to get wrong --- # FAQ URL: https://docs.patchrelease.com/faq/ Section: Start here Description: Short answers about Patch: Apple's rules, what an OTA patch can and cannot change, rollback, the fingerprint, self-hosting, pricing, and supported iOS versions. Patch ships over-the-air code updates to native Swift iOS apps: the `patchcli` CLI compiles the Swift you changed to WebAssembly, and the on-device PatchSDK runs that module in the WasmKit runtime inside your signed App Store binary. This page answers the questions people ask before adopting it. Failure modes and their fixes live in [Troubleshooting](/troubleshooting/); term definitions live in the [Glossary](/glossary/). ## Does Apple allow OTA updates to native Swift apps? Yes — Apple's Developer Program License Agreement permits an application to download and run **interpreted code**, provided the downloaded code does not change the application's primary purpose, does not create a store or storefront for other code, and does not bypass the operating system's sandbox or code-signing protections. That is the clause React Native's CodePush, Expo's EAS Update and Flutter's Shorebird have all relied on, and it is the clause Patch relies on. Staying inside it — not shipping functionality you concealed during review — is your responsibility as the developer, not something any OTA tool can enforce. See [Apple compliance](/apple-compliance/). ## Does Patch comply with App Review Guideline 2.5.2? Yes, on Patch's side of the line. Guideline 2.5.2 says apps "may not download, install, or execute code which introduces or changes features or functionality of the app, including other apps." Patch does not modify or replace the signed binary, does not load native machine code, and cannot call a framework your app did not link or acquire an entitlement your app did not declare. What it can do is change code you already shipped — so the guideline's boundary becomes a policy question about your change, not a technical one. Read the full argument on [Apple compliance](/apple-compliance/). ## Is WebAssembly interpreted code? Yes, in the form Patch ships it. A patch is a WebAssembly module executed by [WasmKit](https://github.com/swiftwasm/WasmKit), a Swift WebAssembly runtime compiled into your app. The module is never native machine code, is never `dlopen`ed or loaded as an executable, and reaches the system only through host functions your binary already exposes. See [How it works](/how-it-works/). ## What can a Patch update change? Swift logic, `async`/`await` concurrency, and SwiftUI view bodies — structure, state and interaction. An OTA patch can add, remove, reorder and restyle views, change text and images, rebind controls, change navigation and presentation, and rewrite business rules such as pricing or validation. Code that touches an OS API stays native and keeps running from the signed binary. The measured per-app numbers are on [What Patch can & can't update](/coverage/). ## What can't a Patch update change? Anything that needs a native symbol, framework or entitlement that is not already compiled and signed into your shipped binary — a camera prompt in an app with no `NSCameraUsageDescription`, a `MKMapView` in an app that never linked MapKit. It also cannot rewrite the compiled body of native code that is not a patchable `View.body`: a `UIViewRepresentable`'s `makeUIView`, a custom `ButtonStyle`'s `makeBody`, a Core Data fetch predicate. Those existing views still render, move and reorder over the air — only their native internals are frozen. See [the binary-symbol wall](/coverage/). ## Can Patch update SwiftUI views without wrapping them? Yes. `patchcli prepare` routes each `var body: some View` through a generated per-view thunk (`var body: some View { __patchRoute { … } }`, on the body's own lines). There is no `PatchView` wrapper and no change to how you write your views, and it builds in Debug and optimized Release/archive builds alike. The step runs automatically inside `patchcli build`, `push` and `release`, so views you add later are picked up with no extra command. See [Quick Start](/quickstart/). ## Does prepare put generated code in my source files? Almost none. Thunks live in a gitignored `Patch/Generated/` folder. A view file gets the body route plus a tiny `PATCH-ROUTE` native fallback, and a view that uses `private` members also gets one short, sorted `PATCH-ACCESS` extension of forwarders, so you never have to widen `private` to `internal`. A `private struct … : View` keeps a compact thunk in its own file, because no other file can extend it. `patchcli unprepare` removes everything. See [where prepare puts generated code](/cli/#where-prepare-puts-generated-code). ## Can Patch update UIKit? Yes, for declarative construction. Patch lowers the view-building code in a `UITableViewCell` / `UICollectionViewCell`'s `configure(with:)` and `setup()`, and in a programmatic view controller's `setupViews()` / `viewDidLoad`, into the same over-the-air path SwiftUI uses. Lowering is all-or-nothing per method: one statement the engine does not recognise keeps that whole method native. A subview it cannot reconstruct becomes a native slot rather than being dropped. See [What Patch can & can't update](/coverage/). ## Which iOS versions are supported? iOS 15 and later. That is the deployment target PatchSDK requires. A patched view that uses a SwiftUI API newer than the device's iOS (for example `NavigationStack` or `.scrollDisabled` on iOS 15) shows its original native body on that device instead of an approximation; newer devices get the patch. The deployment target is also part of the [native-shell fingerprint](/fingerprint/), so raising it in a future build means registering a new fingerprint before you release patches against it. ## How is Patch different from CodePush? CodePush patches a React Native app's JavaScript bundle and images; Microsoft's README states that changes "which touch native code … cannot be distributed via CodePush." Patch patches native Swift compiled to WebAssembly, so there is no JavaScript bundle involved. The hosted App Center CodePush service was retired on 31 March 2025 and the client repository was archived on 20 May 2025. See [How Patch compares](/compare/). ## How is Patch different from Expo EAS Update? EAS Update is, in Expo's words, "a cloud service that serves updates for projects using the expo-updates library" — React Native. It updates an app's "non-native pieces (such as JS, styling, and images)". Patch updates native Swift and SwiftUI in an app that has no JavaScript layer at all. If your app is Expo or React Native, EAS Update is the right tool; Patch cannot patch a JS bundle. See [How Patch compares](/compare/). ## How is Patch different from Shorebird? Shorebird patches Dart in a Flutter app and states plainly that it "does not support changing native code (e.g. Java/Kotlin on Android or Objective-C/Swift on iOS)." Patch is the reverse: it patches the Swift, and cannot patch Dart. If your app is Flutter, use Shorebird. See [How Patch compares](/compare/). ## How do I roll back a patch? Run `patchcli rollback --channel `. The previous module re-activates and propagates to devices on their next update check. A device already running the rolled-back release is recalled — the update check tells it to deactivate and fall back. Rollback is channel-scoped, so rolling back `staging` leaves `production` untouched. See [Staged rollouts & A/B](/rollouts/). ## What happens if a patch fails to run? The app runs the signed binary. If a module is missing, corrupt, or fails to verify or activate, the SDK falls back down the chain — the previous good module, then the native code compiled into your App Store build. At build time the same principle applies in reverse: if generated WebAssembly does not compile, the offending code is demoted to native and the build retries, so a broken module is never produced. See [How it works](/how-it-works/). ## What is the native-shell fingerprint, and why did my release say MISMATCH? The fingerprint is a SHA-256 hash of everything that fixes your signed binary's layout — native Swift sources, Info.plist, entitlements, linked frameworks, deployment target, compiler version. A patch is only served to devices running a build whose fingerprint matches the one it was built against. MISMATCH means your native shell changed since the last registered build, so the patch is not provably compatible. Run `patchcli fingerprint diff --explain` to see what moved. See [The compatibility fingerprint](/fingerprint/). ## Does a patch survive an App Store update? No — and that is deliberate. A new App Store build has a different native shell, so it reports a different fingerprint and is not served patches built for the old one. It simply runs its own native code. After the new build is live, run `patchcli fingerprint register` and release patches against it. Devices still on the previous version keep receiving the patches built for them. See [The compatibility fingerprint](/fingerprint/). ## How big is a patch? Small. Most modules use the embedded tier — tens of KB — because Foundation values are satisfied by host bridges into the native shell's real Foundation rather than being compiled into the module. When a module does need the full-Foundation base, that base ships once; every update after it is a compressed binary diff against the version already on the device, typically a few hundred bytes to tens of KB. See [the packaging tiers](/how-it-works/#compatibility-fingerprint--packaging-tiers). ## Will running in WebAssembly slow my app down? No — the overhead is negligible for the code Patch ships. Patched code runs in WasmKit, and a view whose body is byte-identical to the one in your signed binary runs the native body with no WebAssembly at all: only views a patch actually changed execute the module. Performance-critical code — low-level rendering, AVFoundation, heavy compute — stays native by construction. See [How it works](/how-it-works/). ## Do I need to annotate my Swift code? No. You write ordinary Swift with no attributes or macros. The engine classifies every function and decides what can ship over the air: pure logic and WASM-safe Foundation value types compile to WebAssembly, code that touches a bridged API runs over the air and calls back into the shell, mixed code is split, and anything else stays native. When it is unsure, it keeps the code native. See [How it works](/how-it-works/#whats-updatable). ## Can I ship a patch to only some devices? Yes. Three independent controls stack: a **channel** decides which stream a build follows, **targeting** decides which devices are eligible by app version, OS version and cohort, and the **rollout percentage** decides what share of those eligible devices get the release. Bucketing is deterministic per device and release, so raising a percentage only ever adds devices. See [Release targeting](/targeting/) and [Staged rollouts & A/B](/rollouts/). ## Can I self-host Patch? Partly. The SDK talks to any base URL, so you can serve modules from your own infrastructure and point `PatchConfiguration.apiBaseURL` at it. What is not open source is the hosted control plane — rollouts, targeting, analytics, audit, team accounts — so a self-hosted deployment means building or forgoing those. A static bucket-and-CDN publish command is planned and not yet shipped. See [Running it yourself](/self-hosting/). ## Is the SDK open source? Yes. The SDK is MIT-licensed and the engine is Apache-2.0, both in [patch-release/patch-swift](https://github.com/patch-release/patch-swift) — the SDK at the repository root, the engine nested at `cli/`. The engine is Apache-2.0 rather than MIT for its express patent grant. The hosted control plane is a commercial service and its source is not published. See [Open source](/open-source/). ## How much does Patch cost? Hobby is free and serves up to 100 distinct devices, with unlimited apps, unlimited 100% production releases, instant rollback and usage analytics. Startup is $59/month and adds team members, staged rollouts and A/B, channels beyond `production`, and a 10,000-device fleet. Enterprise adds the audit log and activity feed, support, and an unlimited fleet. See [Plans & billing](/billing/) and [pricing](https://patchrelease.com/pricing). ## Will my app keep working if I stop paying? Yes. Your signed App Store binary is never modified, so the app runs exactly as Apple reviewed it, and the SDK keeps activating the module already cached on the device. Downgrading to Hobby re-applies the free-tier limits going forward — new invites and non-100% rollouts are gated again — but existing data is not deleted. Once a Hobby workspace is at its 100-device cap, a *new* device's update check returns "no update"; devices already in the counted set keep updating. See [Plans & billing](/billing/). ## How do I install Patch? Two commands to set up the machine, one to set up the app: ```bash brew install patch-release/tap/patchcli patchcli setup # the swift.org toolchain + WebAssembly SDK cd MyApp && patchcli init # register the app, add the SDK, wire the startup code ``` `patchcli init` is safe to re-run and prints exact manual steps for anything it cannot do safely. See [Quick Start](/quickstart/), or hand the whole thing to an AI assistant with the prompts on [Set up with AI](/ai-setup/). ## Related - [Glossary](/glossary/) — definitions of every term used here - [Apple compliance](/apple-compliance/) — DPLA §3.3.1(B) and Guideline 2.5.2 - [How Patch compares](/compare/) — Patch, CodePush, EAS Update and Shorebird side by side - [Troubleshooting](/troubleshooting/) — failure modes and their fixes - [What is an OTA update for iOS?](https://patchrelease.com/blog/what-is-an-ota-update-for-ios) - [What Apple allows for OTA updates](https://patchrelease.com/blog/what-apple-allows-ota-updates) --- # What Patch can & can't update URL: https://docs.patchrelease.com/coverage/ Section: What Patch can change Description: What an OTA patch can and cannot change in a SwiftUI app, with coverage measured on a real validation corpus and the one permanent binary-symbol wall. An OTA patch can change SwiftUI structure, state and interaction, plus Swift logic and `async`/`await`, in the app your users already have installed. It cannot introduce a native symbol, framework or entitlement that is not already compiled and signed into that binary. This page is the map of that boundary, with per-app coverage measured on a real validation corpus. **These numbers are generated, not written** Everything below is imported from `src/data/coverage.json`, which is derived from a committed census artifact by `npm run gen`. No coverage figure on this site is typed by hand. Reproduce it yourself with `./corpus/fetch.sh` and `./tools/swiftui-corpus-coverage/run.sh`. The honest, developer-facing map of what an OTA patch can reach in a SwiftUI app — and the small set of things it genuinely can't. The short version: **SwiftUI structure is never the wall.** An OTA patch can author, restyle, reorder, rebind, and re-flow essentially your entire view hierarchy. The only true ceiling is the App Store's own boundary — a native capability, framework symbol, or privacy entitlement that **isn't already compiled and signed into your shipped binary**. ## Measured coverage Across **{coverage.appCount} real apps** — ours and well-known open-source ones — **{coverage.viewLevel.pct}%** of SwiftUI view bodies lower to WebAssembly and ship over the air.
{coverage.viewLevel.pct}% view-level — {coverage.viewLevel.lowered.toLocaleString()} / {coverage.viewLevel.total.toLocaleString()}
{coverage.elementLevel.pct}% element-level — {coverage.elementLevel.lowered.toLocaleString()} / {coverage.elementLevel.total.toLocaleString()}
{coverage.best.pct}% best app — {coverage.best.name}
{coverage.worst.pct}% worst app — {coverage.worst.name}
Per-app results vary widely, so the average matters less than the spread. The {coverage.apps.filter((a) => !a.name.startsWith('first-party-app-')).length} named apps are public repositories measured at pinned commits, so you can verify those yourself; the {coverage.apps.filter((a) => a.name.startsWith('first-party-app-')).length}{' '} `first-party-app-*` rows are our own apps, included because dogfooding is the point but not named: {coverage.apps.map((a) => ( ))}
AppViews loweredCoverage
{a.name} {a.lowered} / {a.total} {a.pct}%
The remainder is mostly custom child views the engine cannot reconstruct, and unsupported modifiers. A view that cannot lower is never silently broken — it renders natively from your signed binary. **The headline** Nearly all of the SwiftUI surface that real iOS apps actually use is OTA-reachable. The measured share of view bodies Patch lowers to WebAssembly today is in the table above — and the views that stay native still render and move via OTA (only their internals are frozen). The gap between "reachable" and "lowered today" is engineering backlog, not a fundamental wall. ## What you can update over the air Within your app's already-linked frameworks and declared entitlements, OTA patches reach virtually the entire SwiftUI surface: | Area | What an OTA patch can do | | --- | --- | | View structure | Add, remove, reorder, and restyle views; change the whole hierarchy. Structure is fully authorable over the air. | | Text & images | Text (literals, interpolation, computed strings), Label, Image(systemName:) and bundle assets, AsyncImage. | | Layout | Stacks, grids, List, Form, Section, ScrollView, Spacer/Divider, ViewThatFits, and the layout modifiers — frame, padding, offset, position, safe-area insets. | | Styling, colors & fonts | Foreground/background/tint, borders, shadows, corner radius, fonts and weights, plus design-system tokens — a custom Theme.Colors.ink / Theme.Font.body(…) / Theme.Radius.lg in a modifier value position lowers and rides the patch (resolved natively, fed to the view). | | Controls & state | Button, Toggle, Slider, Stepper, Picker, TextField/SecureField, Link/ShareLink, Gauge/ProgressView, bound to @State / @Binding / @AppStorage / @FocusState. | | Navigation & presentation | NavigationStack / navigationDestination, sheets, full-screen covers, popovers, alerts, confirmation dialogs, toolbars, context menus, and searchable. | | Modifiers & control flow | The vast majority of standard modifiers, plus if / if let / switch control flow inside a view body, gestures (onTapGesture & the gesture algebra), onAppear/onChange/task, and full async/await logic. | For the exhaustive symbol-by-symbol breakdown, see [What's updatable](/how-it-works/#whats-updatable) and the per-function coverage you get from `patchcli build --verbose`. ## What you can't update — the binary-symbol wall There is essentially **one** true wall, with two faces. Both reduce to the same thing: an OTA patch can never introduce a native symbol, framework, or entitlement that **isn't already in your shipped, code-signed app**, and can never rewrite the compiled machine code of native functions that aren't a patchable `View.body`. This is not "hard, just not yet" — it's a physical boundary, and it's exactly the line the App Store draws. **1 · A native capability, framework, or entitlement that isn't in the signed binary** An OTA patch can't *introduce* a privacy-gated capability your app never declared, or call into a framework the app never linked. The OS reads your *signed* Info.plist and entitlements at launch; WebAssembly can't synthesize a permission key or link a new framework. Examples: - Presenting a **camera or photo-library picker** in an app with no `NSCameraUsageDescription` / `NSPhotoLibraryUsageDescription` and no PhotosUI/AVFoundation linked. - **HealthKit, Contacts, Location, Microphone, Bluetooth, Apple Pay / Wallet** presenters when the matching usage string or entitlement was never declared. - A `UIViewRepresentable` wrapping `MKMapView` / `WKWebView` / `MTKView` / `ARView` in an app that **never linked** MapKit / WebKit / MetalKit / RealityKit. - A custom Metal `Shader` (`.colorEffect` / `.layerEffect`) whose `.metal` function was never compiled into the app's `.metallib`. **2 · The internals of non-View.body native code** Patch routes `View.body` getters through generated thunks and data-drives the renderer. It has no mechanism to rewrite arbitrary compiled native functions. So an OTA patch can't change the *body* of: - A `UIViewRepresentable`'s `makeUIView` / `updateUIView`. - A custom `ButtonStyle` / `ViewModifier`'s `makeBody` / `body(content:)`. - A `@FetchRequest` / `@Query` predicate or sort declaration (you can restyle, reorder, and relabel the resulting rows — you just can't rewrite the predicate to reference an attribute the model lacks). - A custom `Shape.path(in:)`, custom `Layout` geometry, or any private native helper that isn't a thunked `View.body`. Be precise here, because it's the most common misconception. An **existing** `MKMapView`, `WKWebView`, custom view, custom style, or `Canvas` in your app is fully reachable by an OTA patch — it still **renders, gets placed, reordered, resized, shown, and hidden** over the air. What's frozen is only its native *internals* (the `makeUIView` body, the Metal kernel, the predicate). Likewise, a design-system color/font/number constant patches fine — unless it references a `private` or body-local member, in which case the view stays native (still rendered, never broken). And things people assume are off-limits — `.drawingGroup()` (SwiftUI's own Metal), `AsyncImage`, `ShareLink`, haptics, built-in format styles, `GeometryReader` — are all patchable, because they live in the SwiftUI/Foundation runtime your app already links. ## The one-line rule If a change lives entirely inside your app's **existing** linked frameworks, declared entitlements, and SwiftUI view bodies, an OTA patch can ship it. If it requires a **new** native symbol, framework, or privacy entitlement that isn't in the signed binary — or it rewrites compiled native code that isn't a `View.body` — it needs an App Store release. That second set is tiny, and it's the same boundary Apple's review process draws. When Patch isn't sure a change is safe, it keeps the code native and renders the bundled fallback, so an update can **never** break your app. New here? Start with the [Quick Start](/quickstart/) or have an AI assistant [set Patch up for you](/ai-setup/). ## Related - [How it works](/how-it-works/) — the partitioning and packaging pipeline - [FAQ](/faq/) — what a patch can and cannot change, in short form - [Glossary](/glossary/) — mixed view, demote-to-native, binary-symbol wall - [Hotfix a Swift app over the air](https://patchrelease.com/blog/hotfix-swift-app-over-the-air) --- # The compatibility fingerprint URL: https://docs.patchrelease.com/fingerprint/ Section: What Patch can change Description: The native-shell fingerprint is a hash of your signed binary's layout; a patch is served only to matching builds. Why a mismatch happens and how to clear it. The native-shell fingerprint is a deterministic SHA-256 hash of everything that fixes your signed binary's layout: native Swift sources, the SDK and WasmKit versions, bridge definitions, Info.plist, entitlements, linked frameworks, the deployment target and the compiler version. A patch is served only to devices whose build reports the same fingerprint it was built against. If you hit one problem with Patch, it will almost certainly be a mismatch here — so this page is worth ten minutes now. ## What is the native-shell fingerprint? Every Patch release is built against a specific version of your app's **native shell** — the compiled Swift that ships inside your signed App Store binary. The CLI hashes that shell into a short string called the **fingerprint**, and records it alongside the release. When a device checks for updates it reports the fingerprint of the build it is running. The backend serves a patch only if the fingerprints match. ## Why it exists A patch is not a whole app. It is a fragment of WebAssembly that calls into symbols already linked in your binary — your types, your functions, your frameworks. If the shell changes underneath it, those calls no longer line up, and the patch would either behave incorrectly or fail to run at all. The fingerprint is what makes an OTA update safe by construction rather than by hope. A mismatch is Patch refusing to ship something it cannot prove is compatible. **A mismatch is not a bug** It means the shell your patch was built against is not the shell running on the device. That is nearly always correct behaviour. The question is *why* they differ. ## What moves the fingerprint **Changing native code.** Anything that stays in the signed binary — a function the engine classified as native, a new stored property, a changed method signature, a new dependency. **Changing your toolchain.** The Swift compiler version participates in the hash, because different compiler versions can produce different native symbols. Upgrading Xcode moves your fingerprint. **Adding or removing a file** that contributes native code. ## What does *not* move it **Editing a view body that already ships over the air.** Patchable bodies are subtracted from the hash at per-function granularity, so editing the SwiftUI you are patching is fingerprint-stable — that is the entire point. **Editing string literals inside a slotted view.** Since CLI 1.6.28 string literals are lifted out of native slot source and ride the patch instead. **What `prepare` adds to your files.** The body route (`__patchRoute { … }`, and the `dynamic` keyword older CLIs inserted on `var body`), the `PATCH-ROUTE` fallback block, the `PATCH-ACCESS` forwarder block and any `PATCH-THUNKS` block are stripped before hashing, so preparing a project, or migrating from the older in-file layout, does not move the fingerprint. `patchcli unprepare` *does* change the source the hash sees; re-register if you prepare again after it. **Comments, whitespace, and formatting** in patchable code. ## Diagnosing a mismatch 1. **See what changed.** ```bash patchcli fingerprint diff --explain ``` This lists which functions are native and why, and shows what moved since the registered fingerprint. Start here — it usually names the culprit directly. 2. **Decide whether the change was intentional.** If you meant to change native code, the fingerprint *should* have moved. You need a new App Store build, and then to register its fingerprint. If you did not change native code, something else moved the hash — most often a toolchain upgrade. 3. **Register the current shell.** ```bash patchcli fingerprint register ``` This records the fingerprint of the build you are about to ship. Run it from the same shell and toolchain you build releases with. 4. **Confirm.** ```bash patchcli fingerprint diff ``` Clean output means the next `patchcli release` will be accepted. ## Common causes, in order of likelihood ### You upgraded Xcode The Swift compiler version is part of the hash. Registering in one shell and releasing from another can also flip it, because the Apple and swift.org toolchains report different version strings. Register and release from the same environment, then re-register after any toolchain change. ### You changed native code without noticing A helper that looks trivial may be classified native — anything touching an OS API, a file handle, or an unsupported type. `patchcli build` prints the per-view demote diagnostic explaining what stayed native and why. ### Your app already had a baked fingerprint literal Apps set up before CLI 1.6.47 baked a `fingerprint:` literal into the `Patch.configure(...)` call, and that literal was itself part of the hash — so re-baking it moved the hash it was supposed to describe. The value is now excluded from the native-shell hash. Affected apps re-register once: ```bash patchcli fingerprint register ``` Apps with no baked fingerprint are unaffected. ### You ran `patchcli init` and released immediately Fixed in CLI 1.6.47. `init` used to snapshot the shell *before* preparing views and injecting `Patch.configure`, so the registered hash described a source state that `release` never sees. Upgrade the CLI, then re-register. ## Shipping a patch alongside a native change If you genuinely need both, the order matters: 1. Ship the new native build to the App Store and wait for it to be live. 2. Register the new shell's fingerprint. 3. Release patches against it. Patches built against the old shell keep serving old builds, which is what you want — devices on the previous version are not stranded. ## Partial patches If you understand the risk and need to ship against a drifted shell, the CLI supports it explicitly: ```bash patchcli release --allow-native-drift ``` This ships only the view bodies that are provably unaffected by the drift, and strips anything whose safety cannot be established. It is deliberately conservative and is not a way to bypass the check. Prefer registering a new fingerprint. `--allow-native-drift` exists for incident response, not routine releases. ## Reporting a fingerprint problem If a mismatch looks wrong, the diagnostic bundle is what makes it actionable: ```bash patchcli doctor --json ``` Attach the output to a GitHub issue, along with the fingerprint diff: ```bash patchcli fingerprint diff --explain ``` Between them these give the versions, the toolchain, and exactly which functions are native and why — which is usually enough to identify the cause without anyone needing your source. ## Related - [What Patch can & can't update](/coverage/) — why a function was classified native - [Troubleshooting](/troubleshooting/) — the other four ways a release fails - [Glossary](/glossary/) — native shell, fingerprint, demote-to-native - [What is an OTA update for iOS?](https://patchrelease.com/blog/what-is-an-ota-update-for-ios) --- # Glossary URL: https://docs.patchrelease.com/glossary/ Section: What Patch can change Description: Definitions of the terms Patch uses: OTA update, native shell, fingerprint, PMOD, WasmKit, host bridge, channel, staged rollout, demote-to-native, thunk. Patch borrows vocabulary from three places: mobile release engineering, the WebAssembly toolchain, and Apple's developer agreements. Each term below is defined once, standalone, in the sense Patch uses it. Short answers to product questions are on the [FAQ](/faq/). ## What is an OTA update? An OTA (over-the-air) update is a code change delivered to an app already installed on a device, without submitting a new binary to the App Store. With Patch the delivered artifact is a WebAssembly module compiled from the Swift you changed; the signed App Store binary stays exactly as Apple reviewed it, and the module runs inside it. ## What is a hotfix? A hotfix is a small, urgent correction to code already in production — a crash, a wrong price, a broken eligibility rule — shipped on its own rather than waiting for the next scheduled release. Patch's purpose is to make a hotfix to a native Swift app a release you run from a terminal instead of a submission you wait on. ## What is a patch, and how is it different from a release? A patch is the WebAssembly module that carries your changed code. A release is the act of publishing one to a channel with a rollout percentage, targeting and release notes — what `patchcli release` performs, and the row you see in the console's Rollouts view. One build can be released more than once: to staging first, then to production. ## What is a channel? A channel is a named update stream — `production`, `staging`, `beta`, or any string you choose. A build subscribes to one channel in `PatchConfiguration` and receives only releases pushed to it, so an internal beta and production run in parallel from one codebase. `status` and `rollback` are channel-scoped too. See [Deployment channels](/channels/). ## What is a staged rollout? A staged rollout is a release served to a percentage of eligible devices rather than all of them, so a problem is found on a slice of the fleet. Patch buckets each device deterministically from its device id and the release, so a device that is in stays in as you raise the percentage. See [Staged rollouts & A/B](/rollouts/). ## What is a rollback? A rollback re-activates the previous module for a channel, undoing a release across the fleet. Devices pick it up on their next update check, and a device already running the withdrawn release is told to deactivate and fall back. It is one command — `patchcli rollback` — and needs no App Store submission. ## What is a cohort, and what is targeting? Targeting decides which devices are even eligible for a release: minimum and maximum app version, minimum OS version, and a named cohort. A cohort is a label a device reports — set by your app, or derived by the backend from a stable hash of the device id. Targeting composes with the rollout percentage: both must pass. See [Release targeting](/targeting/). ## What is a force update? A force update is a release marked mandatory, so your app can require it before the user continues. The flag rides through the update check and surfaces as `UpdateInfo.isMandatory`; you decide what UI to show. It applies to the OTA module only — a native change still needs an App Store release. See [Force updates](/force-updates/). ## What is the native shell? The native shell is the compiled Swift inside your signed App Store binary: the code the engine kept native, plus the SDK, the linked frameworks, the entitlements and the Info.plist. A patch is not a whole app — it is a fragment that calls into symbols the shell already contains, which is why the shell's identity has to be pinned. ## What is a native-shell fingerprint? A native-shell fingerprint is a deterministic SHA-256 hash over everything that fixes the signed binary's layout — native Swift sources, the SDK and WasmKit versions, bridge definitions, Info.plist, entitlements, linked frameworks, the deployment target and the compiler version. A device reports its build's fingerprint on every update check, and the backend serves a patch only when the fingerprints match. See [The compatibility fingerprint](/fingerprint/). ## What is WebAssembly? WebAssembly (Wasm) is a portable binary instruction format with a defined sandbox: a module has its own linear memory and can reach the outside world only through imports its host explicitly provides. Patch compiles the OTA-eligible parts of your Swift to a WebAssembly module, which is why a patch cannot invent a capability your app does not already have. ## What is WasmKit? [WasmKit](https://github.com/swiftwasm/WasmKit) is a WebAssembly runtime written in Swift. PatchSDK embeds it, so the module a patch delivers is executed by an interpreter compiled into your app rather than by anything downloaded or dynamically loaded. It is the reason a patch is interpreted code rather than native machine code. ## What is Embedded Swift? Embedded Swift is a language subset that compiles without the Swift runtime's reflection and metadata machinery, producing very small binaries. Patch's default packaging tier uses it: the module carries no Foundation or ICU, and Foundation values such as `Decimal`, `Date` and JSON are satisfied by host bridges into the native shell's real Foundation instead. ## What is a PMOD container? A PMOD container is the file format a Patch build ships: one default WebAssembly sub-module plus optional additive sub-modules, packaged together. The SDK instantiates each sub-module as its own WasmKit instance and routes calls to the right one. Sub-modules are kept separate rather than merged because two Swift modules cannot share one linear memory. ## What is a body route, and what is a thunk? `patchcli prepare` wraps each `var body: some View` getter in a route call — `var body: some View { __patchRoute { … } }` — and generates a thunk: a small per-view `__patchRoute` method that asks the SDK whether a patched body exists and renders it, with the original native body content as the fallback. (Older CLIs used Swift dynamic replacement — `dynamic` + `@_dynamicReplacement(for: body)` — which miscompiles or crashes in optimized Release builds.) Thunks are generated into `Patch/Generated/`. A view's `private` members are reached through a small `PATCH-ACCESS` forwarder extension in the view's own file. ## What is a mixed view? A mixed view is a SwiftUI view whose body is only partly lowerable. Rather than demoting the whole view, the engine lowers the parts it can and leaves the rest as native slots — a custom child view or an unsupported modifier is passed whole to the renderer by the build-time thunk. The structure ships over the air; the slotted leaves render natively. ## What is demote-to-native? Demote-to-native is the engine's default answer whenever it cannot prove a change is safe to run as WebAssembly: the function or view body stays in the signed binary and runs from there. It happens at build time — with a per-view diagnostic naming what blocked it — and again on-device if a module fails to verify or activate. Coverage is what is at stake, never correctness. ## What is a host bridge? A host bridge is a host function the SDK exposes to the WebAssembly module so guest code can use a capability the module itself does not contain — networking, `UserDefaults`, the keychain, date and locale formatting, regular expressions. The bridge calls code already linked into your signed binary, so it can never widen what the app is able to do. ## What is the binary-symbol wall? The binary-symbol wall is the one hard limit on what an OTA patch can reach: it cannot introduce a native symbol, framework or entitlement absent from the signed binary, and it cannot rewrite the compiled body of native code that is not a patchable `View.body`. This is a property of code signing, not a backlog item. See [What Patch can & can't update](/coverage/). ## What is interpreted code under DPLA §3.3.1(B)? Interpreted code, in the sense of Apple's Developer Program License Agreement §3.3.1(B) (formerly §3.3.2), is downloaded code an app runs through an interpreter it already contains — permitted provided it does not change the app's primary purpose, does not create a store or storefront for other code, and does not bypass the sandbox or code signing. See [Apple compliance](/apple-compliance/). ## What is App Review Guideline 2.5.2? App Review Guideline 2.5.2 is the review rule that 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 verbatim and in context on [Apple compliance](/apple-compliance/). ## What is a phased release? A phased release is App Store Connect's own staged delivery: an approved version is released to a growing share of users who have automatic updates switched on. It happens after review, applies only to a version Apple already approved, and cannot be used to ship a change that has not been reviewed. A Patch rollout percentage is the equivalent control for an OTA module. ## What is an expedited review? An expedited review is a request to Apple to prioritise a submission ahead of the normal queue, reserved for critical situations such as a serious bug in a released app. It is a request, not a setting, and Apple decides. An OTA patch addresses the same urgency without a submission — for changes that stay inside the interpreted-code boundary. ## What is a feature flag? A feature flag is a runtime switch that turns behaviour already shipped in the binary on or off. It changes configuration, not code, so anything the flag can reach had to be written and reviewed in advance. An OTA patch changes the code itself, which is why it can fix a bug a flag can only hide. ## What is remote config? Remote config is server-delivered values — strings, numbers, colours, JSON — that an installed app reads at runtime. Like a feature flag it cannot introduce logic the binary does not already contain. Patch ships the logic, so it covers the cases where the fix is a code change rather than a value change. ## What is server-driven UI? Server-driven UI is an architecture where the server sends a description of the interface and the app renders it from a fixed catalogue of components you built in advance. It requires designing the app around that catalogue up front. Patch lowers the SwiftUI you already wrote, so no architectural commitment is required to change a view over the air. ## What is CodePush? CodePush is Microsoft's over-the-air update service for React Native, which kept an app's JavaScript and images in sync with releases and could not distribute changes that touched native code. The hosted App Center service was retired on 31 March 2025 and the client repository was archived on 20 May 2025; a standalone self-hosted server was published and is archived too. See [How Patch compares](/compare/). ## What is EAS Update? EAS Update is Expo's over-the-air update service — in Expo's words, "a cloud service that serves updates for projects using the expo-updates library". It updates a React Native app's non-native pieces, such as JavaScript, styling and images; native changes require a new build. See [How Patch compares](/compare/). ## What is Shorebird? Shorebird is the over-the-air code-push service for Flutter. It patches Dart code, running it on iOS through a custom Dart interpreter, and states that it "does not support changing native code (e.g. Java/Kotlin on Android or Objective-C/Swift on iOS)". See [How Patch compares](/compare/). ## Related - [FAQ](/faq/) — short answers to the questions behind these terms - [How it works](/how-it-works/) — the pipeline these terms describe - [What Patch can & can't update](/coverage/) — the measured coverage map - [Apple compliance](/apple-compliance/) — DPLA §3.3.1(B) and Guideline 2.5.2 --- # CLI reference URL: https://docs.patchrelease.com/cli/ Section: Reference Description: Every patchcli command, captured from the binary's own --help: setup, init, prepare, doctor, build, push, release, status, rollback, fingerprint and analyze. `patchcli` is the Patch command-line tool: it sets up an app, compiles the OTA-eligible Swift to WebAssembly, gates the result on the native-shell fingerprint, and publishes it to a channel. Install it with `brew install patch-release/tap/patchcli`. Every command and flag below is captured from the binary's own `--help`. Keep it on the same version as the `PatchSDK` package in your app — see [Versions & compatibility](/sdk/#versions--compatibility). **Generated from the real CLI** The command list below is captured from `patchcli --help` by `npm run gen:cli-help` — it is not written by hand, so it cannot drift from the binary. Prose on this page explains behaviour; the flags and usage are the CLI's own output. Captured from **patchcli {cliHelp.version}**. ## Every command {cliHelp.commands .filter((c) => c.name !== 'patchcli') .map((c) => ( ))}
CommandWhat it does
patchcli {c.path.join(' ')} {c.overview || '—'}
## Usage {cliHelp.commands.filter((c) => c.name !== 'patchcli').map((c) => (

patchcli {c.path.join(' ')}

{c.overview ?

{c.overview}

: null}
{c.help}
))} The full command surface of the `patchcli` CLI. `build` and `compile` drive the real engine and the real WASM toolchain; `push`, `release`, `status`, `rollback`, and `fingerprint` talk to the backend over HTTP. Run `patchcli --help` for full details on any command. **Which command compiles vs. uploads?** `patchcli build` compiles your Swift to a real `.wasm` at `.Patch/build/module.wasm` but never talks to the backend. `patchcli push` does **not** build — it only uploads an already-built `.wasm` (the one `build` wrote, or the path you pass with `--module`), gated on the fingerprint check. `patchcli release` is the everyday command: it runs the *same* build pipeline as `build`, then the *same* fingerprint-gated upload as `push` — i.e. `release` = `build` + `push`. | Command | What it does | Key flags | | --- | --- | --- | | `patchcli init [path]` | One-command app setup: detect the Xcode project/target/bundle id, register the app via the browser (`app.patchrelease.com/cli-connect` — the CLI automatically receives the app key **and a publish token**, writing `app_key`/`publish_token`/`app_id`/`workspace_id` into `.Patch.yml`), add the `PatchSDK` package to the project (backup at `project.pbxproj.patch-backup`), and propose the startup-code diff, applied after you confirm. Skips anything already done — safe to re-run. Anything it can't do safely falls back to printed manual steps. | `--manual` · `--no-open` · `--yes` · `--skip-package` · `--skip-code` · `--no-verify` · `--verify-release` · `--force` · `--target ` · `--base-url ` | | `patchcli login [path]` (new) | Get a **publish token** (`ppt_…`) for this project and save it to `.Patch.yml`. Opens your browser, you approve, the CLI receives the token — the same hand-off `init` uses, on its own so you can run it any time. Use it when you clone a repo that has `.Patch.yml` but no token, when a token is revoked, or to mint one for CI. The token is never printed. | `--base-url ` | | `patchcli doctor [path]` (new) | Read-only setup/health preflight — answers "is my app set up correctly for OTA patches?" Runs six checks (each ✓/⚠/✗ with a one-line fix hint), **never mutates the project**, and exits non-zero on any blocking ✗ so it's CI-usable. Run it after `init` or whenever a patch isn't behaving. See [the doctor section](#check-your-setup-patchcli-doctor). | `--json` · `--offline` · `--base-url ` | | `patchcli prepare [path]` | Make your SwiftUI views patchable: route each `var body: some View` through Patch (`var body: some View { __patchRoute {` … `} }`, on the body's own lines) and generate the per-view route thunks, so a future OTA patch re-renders your views with no `PatchView` wrapping. Works in Debug and optimized Release/archive builds. Idempotent. **You normally never run this yourself** — it runs automatically on every `build` / `push` / `release`. This standalone command stays for CI (`--check`) and debugging. Generated code goes to a gitignored `Patch/Generated/` folder; see [where prepare puts generated code](#where-prepare-puts-generated-code). | `--check` · `--thunks-only` · `--yes` · `--verify` · `--verify-config all\|debug\|release` · `--report ` | | `patchcli unprepare [path]` (new) | The inverse of `prepare`: removes the in-file `PATCH-ROUTE` / `PATCH-ACCESS` / `PATCH-THUNKS` blocks and the body routes prepare inserted (and an older CLI's `dynamic` keywords), deletes `Patch/Generated/`, and unwires the generated files + `PatchSwiftUI` from the Xcode project (classic or synchronized groups) or `Package.swift`. Shows the plan first; every edit is verified and restored on failure. `--remove-sdk` also removes the `PatchSDK` package and the injected `Patch.configure` startup code. | `--dry-run` · `--yes` · `--remove-sdk` · `--keep-dynamic` | | `patchcli build [path]` | Parse → classify → split → compile to a real `.wasm` module at `.Patch/build/module.wasm`. Does not upload. Auto-runs `prepare` first (route new views' bodies + generate their thunks) unless `--no-prepare`. | `--verbose` · `--dry-run` · `--report ` · `--optimization size\|speed` · `--output ` · `--no-prepare` | | `patchcli push` | Upload an **already-built** `.wasm` to the backend, gated on a fingerprint-compatibility check. Does **not** build — run `build` first (or use `release`). | `--channel ` · `--rollout 0-100` · `--message ` · `--version ` · `--module ` · `--mandatory` · `--min-app-version ` · `--max-app-version ` · `--min-os-version ` · `--target-cohort ` · `--allow-native-drift` · `--base-url ` · `--dry-run` · `--no-prepare` | | `patchcli release` (new) | Build then push in one command — the everyday deploy. Runs the same build pipeline as `build`, then the same fingerprint-gated upload as `push`; accepts both flag sets (no `--module`, since it builds the module itself). Auto-runs `prepare` first so views you add are always patchable (skip with `--no-prepare`). | `--channel ` · `--rollout 0-100` · `--message ` · `--version ` · `--mandatory` · `--min-app-version ` · `--max-app-version ` · `--min-os-version ` · `--target-cohort ` · `--allow-native-drift` · `--optimization size\|speed` · `--output ` · `--base-url ` · `--dry-run` · `--no-prepare` | | `patchcli status` | Show the current deployment: version, rollout %, adoption, failure rate. | `--channel ` · `--base-url ` · `--json` | | `patchcli rollback` | Roll back to the previous module (or to a specific version). | `--channel ` · `--to ` · `--base-url ` · `--dry-run` · `--json` | | `patchcli fingerprint ` | Diff or register the native-shell fingerprint that gates OTA compatibility. `diff` is the default subcommand. | `--base-url ` · `--app-version ` (register) · `--dry-run` (register) · `--json` | | `patchcli analyze ` | Analysis only, for CI/code review. Reports OTA coverage; with `--check-fingerprint` it exits 1 if the native shell changed. | `--format text\|json` · `--verbose` · `--check-fingerprint` · `--fingerprint-baseline ` | | `patchcli channels` | List the deployment channels seen for the configured app, with the active module per channel. Calls `GET /apps/{app_id}/channels` (falls back to deriving channels from the module list). | `--base-url ` · `--json` | | `patchcli whoami` | Print the configured `app_id` / `workspace_id` / base URL / channel resolved from `.Patch.yml` (and env overrides). Handy for confirming which backend a command will hit. | `--json` | | `patchcli compile ` | Lower-level: compile a directory of generated `_wasm.swift` to a real `.wasm` module. | `--output ` · `--swift-sdk ` · `--exports ` | ## Where prepare puts generated code `prepare` keeps generated code out of the files you edit. Each view's route thunk and its native slot, token and action helpers go into `Patch/Generated/PatchThunks.generated.swift`, a gitignored folder next to your views. Your view file gets two small, same-line edits and one short block: ```swift var body: some View { __patchRoute { // was: var body: some View { VStack { … } } } // was: } // PATCH-ROUTE-BEGIN (generated by `patchcli prepare` — DO NOT EDIT) // Renders this file's `__patchRoute { … }` view bodies natively when Patch/Generated/ has no route … fileprivate extension View { @inline(__always) func __patchRoute<__PatchNativeBody: View>(@ViewBuilder _ __nativeBody: () -> __PatchNativeBody) -> __PatchNativeBody { __nativeBody() } } // PATCH-ROUTE-END ``` Line numbers in your file don't move. The `PATCH-ROUTE` block is a native fallback that needs only SwiftUI, so the file still builds on a fresh clone without `Patch/Generated/`. Projects prepared by an older CLI (which inserted `dynamic` on `var body`) migrate on the next `prepare` with no fingerprint change: that `dynamic` + `@_dynamicReplacement` form miscompiled or crashed in optimized Release builds. **Views that use `private` members.** Swift's `private` and `fileprivate` only reach within one file, so a thunk in `Patch/Generated/` can't read a view's `private var header: some View`, `@State private var isOn`, `private func row(_:)` or `private struct Row: View` directly. You don't have to change your access control. `prepare` adds one compact, marked extension at the end of that view's file. It holds one internal forwarder per private member the thunk uses, sorted by name: ```swift // PATCH-ACCESS-BEGIN (generated by `patchcli prepare` — DO NOT EDIT) // Forwards the private members Patch/Generated/ uses; changes only when that set does. Remove: `patchcli unprepare`. extension MixView { func __patchMake_MixTrackRow(track p0: Track) -> AnyView { AnyView(MixTrackRow(track: p0)) } func __patch_row(_ p0: Track, compact p1: Bool = false) -> some View { row(p0, compact: p1) } var __patchProj_isOn: Binding { $isOn } } // PATCH-ACCESS-END ``` The block only changes when the *set* of private members the thunk uses changes, so editing a view body leaves it untouched. The native-shell [fingerprint](/fingerprint/) ignores it, just like it ignores the body route. Two cases still keep a compact thunk block (`PATCH-THUNKS`) in the file, and `prepare` names each view and the reason: - A `private`/`fileprivate` View **type** (`private struct Row: View`). No other file can extend it, so its own thunk has to live in its file. - A private member whose type can't be written out from source alone, for example `@State private var palette = makePalette()`. Adding an explicit type (`: [Color]`) lets `prepare` forward it. Literals, `Type(…)`, enum cases and common `@Environment(\.key)` values are recognized automatically. Set `PATCH_ACCESS_FORWARDING=0` to go back to the older layout, where helpers stay in the file. The fingerprint is identical either way, and projects prepared by older CLIs migrate automatically on the next `prepare` with no fingerprint change. **Excluded files.** A file listed under `exclude:` in `.Patch.yml` gets no thunks. The next `prepare` (or `build`/`release`) also removes any Patch blocks and body routes an earlier run left in it, so you never have to restore it by hand. **Removing it all.** `patchcli unprepare` shows what it will remove, then deletes the blocks, the body routes (and any `dynamic` keywords an older CLI recorded inserting — a `dynamic` you wrote yourself stays), `Patch/Generated/`, and the project wiring. On a standard project the result is byte-identical to the project before `prepare`. Add `--remove-sdk` to also remove the SDK package and the `Patch.configure` startup code. Build, push and release auto-prepare again unless you pass `--no-prepare`. If you later prepare again, re-register the fingerprint. ## Build coverage output `patchcli build` parses, classifies, splits, and compiles, then prints a coverage report and writes the module to `.Patch/build/module.wasm`. Use `--verbose` for the per-function table and `--dry-run` to skip the WASM compile. ```bash title="~/MyApp — zsh" $ patchcli build ./Sources Coverage report: WASM-eligible (pure) 7 (50.0%) WASM-eligible (bridged) 3 (21.4%) Mixed (auto-split) 0 (0.0%) Native (stays in shell) 4 (28.6%) OTA-updatable (realized): 71.4% Packaging tier (start): T0 Embedded + bridges (tens of KB) Module: .Patch/build/module.wasm (38.2 KB) ✓ Build succeeded — push with: patchcli push ``` ## Release in one command `patchcli release` runs the build pipeline, confirms the module, then runs the same fingerprint-gated push as `patchcli push` — so it accepts both sets of flags. This is the command you'll use day to day. ```bash title="~/MyApp — zsh" # everyday: build + push to a 10% staged rollout on production $ patchcli release --rollout 10 --message "Tighten refund eligibility" # release to a named channel, optimizing the module for size $ patchcli release --channel staging --optimization size # mark a release mandatory (see Force updates) $ patchcli release --rollout 100 --mandatory --message "Critical tax fix" # preflight only — build + run all push checks, but do not upload $ patchcli release --dry-run ``` ## Status & rollback ```bash title="~/MyApp — zsh" # current deployment + device telemetry on a channel $ patchcli status --channel production Active version: 2026.06.03.142210 Rollout: 10% Adoption: 96.4% (activations / downloads) Failure rate: 0.2% (errors / (activations+errors)) # one-click revert to the previous module (≈60s to propagate) $ patchcli rollback --channel production # roll all the way back to a specific version $ patchcli rollback --to 2026.05.30.090112 ``` ## Check your setup: `patchcli doctor` `patchcli doctor` is the "is my setup correct?" command. It runs a **read-only** preflight — it never edits your project (unlike `init` / `prepare`), so it's safe to run anytime, in CI, or on a teammate's machine. Run it right after `patchcli init`, or whenever a patch isn't behaving the way you expect. It reports six checks, each with a ✓ / ⚠ / ✗ and a one-line fix hint: 1. **`.Patch.yml` present + valid** — has a publish token (`ppt_…`, or `PATCH_API_KEY` set); `app_id` recommended for backend commands. An `app_key` alone fails this check: it is a public identifier, not a publish credential. 2. **Publish token kept out of version control** — `.Patch.yml` holds a `publish_token`, so this fails if git *tracks* it (your credential is committed) and warns if it simply isn't ignored. Read-only: it reports, it doesn't edit your `.gitignore`. 3. **PatchSDK package added + linked** — referenced in your `.xcodeproj` *or* `Package.swift` **and** linked into your app target. 4. **`Patch.configure(...)` + `Patch.shared.start()` in the `@main` App** — plus `import PatchSDK` (a `configure` with no `start()` never fetches updates). 5. **Views are patch-ready** — every SwiftUI view `body` is routed and the route thunks exist. If some aren't yet, this is just a ⚠ (not a blocker): `build` / `push` / `release` auto-run `prepare`, so it self-heals on your next release. 6. **Native-shell fingerprint registered + current** — the registered fingerprint matches the current shell (needs network; degrades to a ⚠ with `--offline`). ```bash title="~/MyApp — zsh" $ patchcli doctor Patch doctor ============ ✓ .Patch.yml present + valid publish token set; app_id 3f2b…-uuid. ✓ PatchSDK package added to the project MyApp.xcodeproj references patch-swift and links the PatchSDK product. ✓ Patch.configure(...) + start() in the @main App entry Patch.configure(...) + Patch.shared.start() + import PatchSDK found (MyAppApp.swift). ✓ Views are patch-ready (view bodies routed + thunks) 12 SwiftUI view(s) are routed + have generated thunks. ✓ Native-shell fingerprint registered + current Registered fingerprint matches the current native shell (3f2b9c1e7a4d…). 5/5 checks passed. READY — this project is set up for OTA patches. ✓ ``` It exits non-zero on any blocking ✗ (the ⚠ items are non-fatal but worth fixing), so you can drop it into CI as a setup gate. Add `--json` for a machine-readable report: ```bash title="CI — setup gate" # fail the job if the project isn't set up for OTA patches $ patchcli doctor --json ``` **Fingerprint gate** `push` and `release` refuse to upload if the local native-shell fingerprint doesn't match the backend's active fingerprint — the native shell changed since the last App Store release, so the module isn't compatible with installed apps. Run `patchcli fingerprint diff` to see what changed; after an App Store release, run `patchcli fingerprint register` to re-baseline. ## Native-shell drift: `--allow-native-drift` Sometimes your native shell changes but in ways the OTA patch **can't see** — you added a Swift package, edited `Info.plist`, tweaked an entitlement, or bumped the deployment target, without touching the source the patch is built against. The plain fingerprint gate would block the push, even though the view patch is provably compatible. To handle this, `patchcli fingerprint diff` tags each changed component as either **native-only** (the patch can't see it) or **patch-affecting** (it can change the surface the patch is built against), and `--allow-native-drift` lets you ship a view patch when *every* change is native-only. | Changed component | Classified | Why | | --- | --- | --- | | Info.plist | native-only | Pure native-shell metadata (display name, permission strings, URL schemes). The WASM module never reads or links it. | | Entitlements | native-only | Native binary capabilities (keychain groups, App Groups, push) — an OS/codesign concern the WASM ABI is blind to. | | Linked frameworks | native-only | Adding/removing a framework the shell links. (If a patched view had started using it, the view's source would have changed too — that shows up as a patch-affecting native-source change and blocks the skip.) | | Deployment target | native-only | A native build setting — it changes which OS versions the binary supports, not the OS-version-independent WASM ABI. | | Native .swift files | patch-affecting | A native function, signature, or file add/remove can change the surface the patch is built against — never skipped. | | Bridge definitions | patch-affecting | Host bridges are capabilities the module calls; changing them shifts the shell ABI. | | SDK / WasmKit / compiler version | patch-affecting | These govern how the module is marshalled and executed on-device — a rebuild + re-register is required. | ```bash title="~/MyApp — zsh" # see the per-component breakdown (native-only vs patch-affecting) $ patchcli fingerprint diff # the native shell drifted, but only natively — ship the view patch anyway $ patchcli release --allow-native-drift --message "Restyle the paywall" ``` **The safety model** `--allow-native-drift` only ever bypasses the gate when **every** changed component is native-only. If **any** change is patch-affecting, the push is still refused — you must ship that change through the App Store and re-register; `--allow-native-drift` will not override it. When run interactively on an all-native-only drift, `push`/`release` prompt you instead; the flag is just the non-interactive (CI) form of that "yes." Either way, you should still ship the native change through the App Store and re-run `patchcli fingerprint register` so future devices report the new shell. ## Configuration: .Patch.yml `patchcli init` writes `.Patch.yml` at your project root — including the `app_key`/`publish_token`/`app_id`/`workspace_id` it receives during browser registration. It's a small, fixed-shape YAML file. Environment variables override the API fields at call time; `app_id`/`workspace_id` are only needed for backend commands (`push`/`release`/`status`/`rollback`). :::caution[`.Patch.yml` holds a secret — don't commit it] `publish_token` authorizes publishing code to your users. `app_key` is public and harmless, but the token is not. `patchcli init` and `patchcli login` add `.Patch.yml` to your `.gitignore` automatically when they write a token, and `patchcli doctor` flags it if it isn't ignored. If it's **already committed**, the ignore rule won't help — git honours the index over `.gitignore` — so revoke the token, `git rm --cached .Patch.yml`, and run `patchcli login` for a fresh one. In CI, set `PATCH_API_KEY` rather than committing the file. ::: ```yaml title=".Patch.yml" version: 1 app_key: pak_live_… # PUBLIC app identifier (also baked into your binary) publish_token: ppt_… # SECRET publish credential — env PATCH_API_KEY overrides project: MyApp.xcodeproj target: MyApp app_id: 3f2b…-uuid # required for push/release/status/rollback workspace_id: 9a01…-uuid api_base_url: https://api.patchrelease.com exclude: # paths/globs to skip during analysis [] bridges: # host bridges available to OTA code networking: true userDefaults: true notifications: true navigation: true keychain: true dateLocale: true logging: true build: optimization: size # size | speed stripDebugInfo: true swiftui: true # lower SwiftUI views to WASM (default on) ``` | Field | Type | Description | | --- | --- | --- | | version | int | Config schema version. Currently 1. | | app_key | string | PUBLIC per-app identifier (pak_…). Ships inside your binary and rides the device check-in body. Authenticates nothing — it is not accepted as X-API-Key. | | publish_token | string? | SECRET publish credential (ppt_…), sent as X-API-Key. Written by `patchcli init`/`login`. Overridden by env PATCH_API_KEY. | | project | string | Detected project file (e.g. MyApp.xcodeproj or Package.swift). | | target | string | The app target whose sources Patch analyzes. | | app_id | uuid? | Backend app UUID. Required for push/release/status/rollback; optional for offline build/analyze. | | workspace_id | uuid? | Backend workspace UUID the app belongs to. | | api_base_url | url? | Backend base URL. Overridden by env PATCH_API_URL / the --base-url flag. | | api_key | string? | Legacy alias for publish_token, still read as a fallback. Overridden by env PATCH_API_KEY. An app key here is ignored. | | exclude | string[] | Paths/globs to skip during analysis. Empty list = analyze everything. | | bridges.* | bool | Toggle each host bridge: networking, userDefaults, notifications, navigation, keychain, dateLocale, logging, analytics, fileStorage, connectivity, biometrics, appReview, pasteboard, haptics, deviceInfo, shareSheet, openURL, location, calendar, contacts, appBadge, mailCompose, inAppPurchase, speechSynthesis, documentPicker, systemSound, photoPicker, secureRandom, appGroupStorage, screenControl, audioPlayback, audioRecording, motion, mapsDirections, networkImage, spotlightIndex, backgroundTask, fileDownload, accessibility, appShortcuts, handoff, watchConnectivity, nfcRead, processInfo, videoPlayback, speechRecognition, mediaInfo, imageFilter, pdfGenerate, qrGenerate, camera (51 in total). A disabled bridge means functions using it stay native. | | build.optimization | string | size (default) or speed. Overridden by --optimization. | | build.stripDebugInfo | bool | Strip debug info from the emitted module to shrink it. Default true. | | build.swiftui | bool | Lower SwiftUI View.body to WASM so views ship over the air. Default true (on if omitted); set false to keep views native. Env PATCH_SWIFTUI=0 overrides per-run. | --- # SDK reference URL: https://docs.patchrelease.com/sdk/ Section: Reference Description: PatchSDK loads, verifies, hot-swaps and runs OTA modules on-device: configuration, start(), the imperative check/fetch/reload API, and native fallback. The `PatchSDK` Swift package loads, verifies, hot-swaps, and runs OTA modules on-device, with native fallback and telemetry. It's pure Swift on top of WasmKit — no extra runtime to bundle beyond the package. ## Install with Swift Package Manager `patchcli init` adds this package to your Xcode project (or `Package.swift`) automatically — the steps here are the manual path (and what `init` prints if it can't edit your project safely). In Xcode: **File → Add Package Dependencies…** and paste the URL. Or add it to your `Package.swift`: ```swift title="Package.swift" dependencies: [ .package(url: "https://github.com/patch-release/patch-swift", from: "1.8.0"), ], targets: [ .target(name: "MyApp", dependencies: [ .product(name: "PatchSDK", package: "patch-swift"), ]), ] ``` PatchSDK supports iOS 15 and tvOS 15 or later (macOS 14, visionOS 1). Patches reach every supported device, but a patched view that uses a SwiftUI API newer than the device's OS (for example `NavigationStack` on iOS 15) shows its original native body on that device rather than an approximation, and the SDK logs which API it was. ## Versions & compatibility From 1.7.0, `patchcli` and `PatchSDK` are released together from the same tag of `patch-swift` and carry the same version number. Keep both on the same version. | Pairing | Supported | What happens | | --- | --- | --- | | Same version | Yes | The tested combination. | | SDK newer than CLI (same major version) | Yes | The SDK still renders every older view format (view IR schema v1 onward), and the code `patchcli prepare` generates only calls SDK API that newer versions keep. | | SDK older than CLI | No | The view code `prepare` generates can call SDK API the older package doesn't have, so your app fails to compile. Or a patched view needs a newer view IR schema than the SDK renders: that view shows its native body instead of the patch, and the SDK logs which views it skipped. | | Different major version | No | SwiftPM `from:` never crosses a major version. | CLIs from before the version unification pair the same way: `patchcli` 1.6.41–1.6.48 works with PatchSDK 1.5.18 or 1.7.0. **Check what you have.** The CLI version is `patchcli --version`. The SDK version is the one SwiftPM resolved, recorded in `Package.resolved`. For an Xcode project that's `YourApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved`; for a Swift package it sits next to `Package.swift`. Find the `patch-swift` entry: ```sh title="Terminal" patchcli --version grep -A 8 '"patch-swift"' YourApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ``` At runtime, `Patch.sdkVersion` holds the same value, and the SDK sends it with every update check. **Bring them in line.** Upgrade the CLI with `brew upgrade patchcli`. Update the SDK in Xcode with **File → Packages → Update to Latest Package Versions**, or with `swift package update patch-swift`. If Homebrew hasn't picked up the newest tag yet, pin the SDK to a version your CLI supports with `.package(url: "https://github.com/patch-release/patch-swift", exact: "")` until it does: your CLI's own version, or `1.5.18` for a 1.6.x CLI. A CLI with no `patchcli login` command (check `patchcli --help`) can't fetch a publish token for you. Create one in the console under **Settings → CLI publish tokens** and set it as `PATCH_API_KEY`, or as `api_key:` in `.Patch.yml`. The app key won't work: the backend rejects it for publishing. ## Configure: PatchConfiguration Configure the SDK once, early in launch. `configure` sets up the on-disk cache, installs the default host bridges, and prepares the update checker when an `apiBaseURL` is set. It does no network I/O — that happens in `start()` / the imperative API. ```swift title="PatchConfiguration" Patch.configure(.init( appKey: "pak_live_…", // per-app key from the dashboard appID: "3f2b…-uuid", // backend app_id for the update check apiBaseURL: URL(string: "https://api.patchrelease.com/api/v1"), fingerprint: "3f2b…", // native-shell fingerprint of THIS build (see note) deviceID: nil, // optional — the SDK auto-generates a stable anonymous id channel: .production, // PatchChannel; .staging/.development or .custom("beta-eu") autoApply: true, // start() may apply updates automatically nativeFallbackEnabled: true)) // fall back to baked-in code if needed ``` Only `appKey` is required — the backend resolves the key server-side (SDK ≥ 1.0.3), so the one-liner `Patch.configure(.init(appKey: "pak_…"))` is a complete integration. `patchcli init` bakes in `appID` (keeps older SDKs polling) and the current native-shell `fingerprint` (exact update gating; a device that reports none is served best-effort). `deviceID` needs no value — the SDK generates a stable anonymous per-install id on first launch and persists it, so staged rollouts bucket correctly out of the box. Supply your own only if you already have an install identity scheme. If your app has extensions (widget, share, notification), set `appGroupIdentifier` in every target so they share one identity — otherwise each extension counts as a separate device and can land in a different rollout bucket. | Field | Type | Description | | --- | --- | --- | | appKey | String | Per-app identifier issued by Patch. | | appID | String? | Backend app UUID, sent as app_id in the update check. Optional — when nil the SDK sends app_key and the backend resolves it (SDK ≥ 1.0.3; older SDKs require it for remote checks). | | apiBaseURL | URL? | API root. When nil, the SDK runs purely from the cached/bundled module (no remote check). | | fingerprint | String? | This build's native-shell fingerprint (patchcli init bakes it in). When reported, the backend serves only modules built for the matching shell; a device that reports none is served best-effort. | | deviceID | String? | Stable anonymous device id, used for deterministic rollout bucketing + telemetry. Optional — when nil the SDK mints a random UUID on first launch and persists it: no identifierForVendor, no advertising identifier, no tracking prompt. (It is still a persistent pseudonymous identifier, so treat it as personal data under GDPR.) Set it only to plug in your own install identity. Note: earlier SDKs sent a fixed placeholder here, which put every device in the same rollout bucket. | | channel | PatchChannel | Update channel to subscribe to. Presets .production, .staging, .development, or .custom("…") for any backend channel string. (A channelName: string overload also exists.) | | autoApply | Bool | Whether start() may download + activate an available update automatically. Default true. | | nativeFallbackEnabled | Bool | Fall back to the baked-in native code if no/invalid module. Default true. | ## Startup updates with start() The simplest integration: call `start()` once. It activates the best already-cached module immediately (so the app runs OTA code instantly, even offline), then — if an `apiBaseURL` is set and `autoApply` is true — checks for an update and applies it in the background, with download/activation telemetry. Any failure leaves the active module untouched and falls back down the chain. ```swift title="App launch" ContentView().task { let outcome = await Patch.shared.start() switch outcome { case .activated(let v): print("Running OTA module \(v)") case .fallback(let s): print("Native fallback: \(s)") case .noModule: print("No module — fully native") } } ``` ## Imperative flow: check → fetch → reload For full control over *when* an update applies — for example to show an "Update available → Download now" prompt — use the imperative API (EAS / Expo-Updates style). `checkForUpdate()` reports availability **without applying**; `fetchUpdate()` downloads + verifies + stages it; `reloadAsync()` activates the staged module with a hot-swap. ```swift title="Imperative update API" public struct UpdateInfo: Sendable { public let version: String public let releaseNotes: String? public let isMandatory: Bool // from the release's `mandatory` flag public let sizeBytes: Int } // 1 · is there a newer module for our channel + fingerprint? (does NOT apply) if let update = try await Patch.shared.checkForUpdate() { showBanner("Update \(update.version) available") // 2 · download + verify + stage it on disk let staged = try await Patch.shared.fetchUpdate() // 3 · activate now via hot-swap (next calls hit the new module) if staged { try await Patch.shared.reloadAsync() } } ``` ## Observable state for SwiftUI The SDK exposes a `@MainActor` observable update state you can drive UI off directly — render a banner, a progress bar, and a "Download now" button without managing your own flags. ```swift title="UpdateBanner.swift" // PatchUpdateState: .idle | .checking | .available(UpdateInfo) // | .downloading(Double) | .readyToReload | .upToDate | .failed(String) struct UpdateBanner: View { // The observable is Patch.shared.updateState (a PatchUpdateStateObservable). @ObservedObject var updates = Patch.shared.updateState var body: some View { switch updates.state { case .available(let info): Button("Download update \(info.version)") { Task { try await Patch.shared.fetchUpdate(); try await Patch.shared.reloadAsync() } } case .downloading(let p): ProgressView(value: p) case .readyToReload: Button("Restart to apply") { Task { try await Patch.shared.reloadAsync() } } default: EmptyView() } } } ``` ## Force / mandatory updates When a release is marked `--mandatory`, `UpdateInfo.isMandatory` is `true`. You decide whether to block UI; the SDK also offers a convenience that fetches + reloads any mandatory update automatically. See [Force updates](/force-updates/) for the full pattern. ```swift title="Mandatory enforcement" // Auto fetch+reload if the available update is mandatory; otherwise no-op. await Patch.shared.enforceMandatoryUpdates() ``` ## Telemetry The SDK fires download, activation, error, and fallback events to the backend automatically (gated by your config). These power the dashboard's adoption and failure-rate numbers, the `patchcli status` telemetry, and the received-% on staged rollouts. No extra wiring is required. --- # Deployment channels URL: https://docs.patchrelease.com/channels/ Section: Shipping Description: Run production, staging and beta as separate OTA release streams from one build: set the channel in PatchSDK, then release with patchcli release --channel. A **channel** is a named update stream — `production`, `staging`, `beta`, or any string you like. The same app build (same fingerprint) can subscribe to a channel and receive only the releases pushed to it. Channels let you run an internal beta and production in parallel from one codebase. ## Subscribe a build to a channel Set the channel on the SDK at init. It can be any string; the `PatchChannel` presets are just conveniences. ```swift title="Choosing a channel" // a preset… Patch.configure(.init(appKey: "pak_live_…", channel: .staging)) // …or an arbitrary channel string (e.g. a per-tester or per-tenant stream) Patch.configure(.init(appKey: "pak_live_…", channelName: "beta-eu")) ``` ## Release to a channel Pass `--channel` on `release`/`push`. A device only ever receives modules pushed to the channel it's subscribed to. `status` and `rollback` are also channel-scoped. ```bash title="~/MyApp — zsh" # release a build to the staging channel for internal testers $ patchcli release --channel staging --message "Try the new checkout flow" # promote the same logic to production once it looks good $ patchcli release --channel production --rollout 25 # inspect / roll back a single channel $ patchcli status --channel staging $ patchcli rollback --channel staging ``` **Channels vs fingerprints** Channels are independent of the compatibility fingerprint. A module pushed to any channel still has to match the installed app's native-shell fingerprint to be served — channels segment *who* gets a release, the fingerprint guarantees it's *safe* for that build. --- # Staged rollouts & A/B (%) URL: https://docs.patchrelease.com/rollouts/ Section: Shipping Description: Ship an OTA release to a percentage of devices first, watch the failure rate, then widen it in place — bucketing is deterministic, so a device in stays in. A staged rollout is an OTA release served to a percentage of eligible devices rather than all of them. Every Patch release carries a rollout percentage: ship to a slice first, watch the failure rate, then widen to 100%. Because bucketing is deterministic, a device that is "in" the rollout stays in as you raise the percentage — adoption only ever grows, and devices never flip-flop. ## Release a staged rollout with `--rollout` ```bash title="~/MyApp — zsh" # start at 10% of eligible devices on production $ patchcli release --rollout 10 --message "New pricing engine" ``` Then **widen that same release in place** — from the console (**Rollouts** → the release → raise the percentage), or over the API: ```bash title="Widen the existing release" $ curl -X PATCH https://api.patchrelease.com/api/v1/modules/$MODULE_ID/rollout \ -H "X-API-Key: $PATCH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"rollout_pct": 50}' ``` **Don't re-run `release` to widen** `patchcli release` builds and uploads a **new** module version every time, so running it again at a higher percentage does not widen the release you already started — it publishes a second one. Because the bucket is derived from the device id **and the version** (below), that re-buckets every device and the "already in, stays in" guarantee no longer holds across the two releases. Raise the percentage on the existing module instead. ## Deterministic bucketing The backend buckets each device by hashing its stable `device_id` together with the release into a value in `0–99`. A device receives the release when `bucket < rollout_pct`. Because the hash is stable per device + release: - The same device always lands in the same bucket for a given release — no flapping between launches. - Raising `rollout_pct` only ever **adds** devices; it never removes a device already serving the release. - A `rollout_pct` below 100 is, in effect, a percentage-based A/B test: the in-bucket cohort runs the new logic, everyone else stays on the previous module. ## Watch & adjust The dashboard shows **targeted %** (the release's `rollout_pct`) and **received %** (activations over estimated-eligible devices). From the CLI, `patchcli status` surfaces adoption and a failure rate, with a hint to roll back if failures climb past 2%. ```bash title="~/MyApp — zsh" $ patchcli status --channel production Active version: 2026.06.03.142210 Rollout: 10% Downloads: 1,204 Activations: 1,160 Adoption: 96.4% (activations / downloads) Failure rate: 0.2% (errors / (activations+errors)) ``` ## Rollback If a staged release misbehaves, roll back. The previous module re-activates and propagates to devices on their next update check. ```bash title="~/MyApp — zsh" $ patchcli rollback --channel production Rolled back: 2026.06.03.142210 (now inactive) Now active: 2026.05.30.090112 (rollout 100%) Takes effect on each device's next update check. ``` ## Related - [Release targeting](/targeting/) — which devices are eligible in the first place - [Deployment channels](/channels/) — which stream a build follows - [FAQ](/faq/) — rollback, fingerprint, and what happens if a patch fails - [Instant rollback for an iOS release](https://patchrelease.com/blog/instant-rollback-ios-release) - [The iOS hotfix playbook](https://patchrelease.com/blog/ios-hotfix-playbook) --- # Release targeting URL: https://docs.patchrelease.com/targeting/ Section: Shipping Description: Limit a release to a device cohort by app version, OS version and cohort label; targeting composes with the rollout percentage, and both must pass. Release targeting decides which devices are even eligible for a Patch release: minimum and maximum app version, minimum OS version, and a named cohort. It is set at push time and stored on the release. Targeting composes **with** the rollout %: a device must satisfy the targeting constraints **and** fall inside the rollout bucket before it's served the module. Use it to ship a fix only where it applies — for example, a fix that depends on an API added in app 2.1 on iOS 16. ## Set targeting at push time Targeting is set when you ship a release, from the CLI (or CI), with four flags on `push`/`release`. Any flag you omit is left unconstrained. The constraints are stored on the release and shown in the console and in `patchcli status`. ```bash title="~/MyApp — zsh" # release a fix only to app ≥ 2.1.0 running on iOS ≥ 16.0 $ patchcli release --min-app-version 2.1.0 --min-os-version 16.0 \ --message "Fix that needs the 2.1 checkout API" # bound it on both ends — only the 2.x line, excluding 3.0+ $ patchcli release --min-app-version 2.0.0 --max-app-version 2.9.99 # targeting composes with the rollout % — eligible devices, then 10% of them $ patchcli release --min-app-version 2.1.0 --rollout 10 # tag the release with a cohort label (shown in the console / status) $ patchcli release --target-cohort beta-eu ``` The same four flags are available on `patchcli push`. When any are set, the preflight (and `patchcli release --dry-run`) prints a one-line `Targeting:` summary, e.g. `app ≥ 2.1.0, iOS ≥ 16.0`, so you can confirm the cohort before uploading. ## In the console Rollout rows whose release carries any constraint show a **Targeted** chip; the release drawer has a **Targeting** section that lists the active constraints (app ≥ / ≤, iOS ≥, cohort). `patchcli status` surfaces the same summary on its `Targeting:` line. ## How it's evaluated On every update check the backend evaluates the active release's constraints against the device, then applies the rollout-% bucket. **Both** must pass for the device to receive the module. - **Unset = no constraint.** A field left `NULL` (flag omitted) targets everyone; a release with no targeting behaves exactly as before. - **Semver is numeric, not lexicographic.** Versions compare segment-by-segment as integers, so `1.10 > 1.9` and `1.2` equals `1.2.0`. Pre-release / build / OS-build suffixes are ignored (`16.4 (20E247)` → `16.4`, `2.1.0-beta.3` → `2.1.0`). - **Bounds.** `--min-app-version` requires the device app version `≥` the value; `--max-app-version` requires `≤`; `--min-os-version` requires the device OS version `≥` the value. - **Fail-open per version constraint.** If a device reports an absent or unparseable version, that version constraint is skipped rather than blocking the device — the rollout-% gate still applies. (Cohort matching is the exception — see below — it is exact-match.) - **Cohort filtering.** `--target-cohort ` now *gates* eligibility: the release is served only to devices that report the matching cohort. The SDK reports an app-assigned cohort (`PatchConfiguration.cohort`, e.g. "beta"/"internal"); when the app sets none, the backend derives a stable hash-bucket cohort from the device id (so percentage-style cohort slices still work). Cohort matching is exact and fail-*closed*: a device that reports no cohort is excluded from a release that targets a named cohort. Composes (logical AND) with the app/OS-version constraints and the rollout-% bucket. **Targeting vs. channels vs. rollout %** These three controls are independent and stack. **Targeting** decides *which devices are eligible* (by app / OS version). A **channel** decides *which track* a device follows (`production`, `staging`, a per-tenant stream). The **rollout %** decides *what fraction of the eligible devices* get the release in this staged step. An update is served only to a device that is on the release's channel, satisfies its targeting, and lands inside the rollout bucket. --- # Force updates URL: https://docs.patchrelease.com/force-updates/ Section: Shipping Description: Mark a release mandatory and the SDK reports isMandatory, so your app can require the patch before the user continues. Native changes still need the App Store. A force update is a Patch release marked **mandatory**, so your app can require the patch before the user continues. Some fixes can't wait for the next natural launch — a critical tax bug, a broken eligibility rule. The flag rides through the update check and surfaces as `UpdateInfo.isMandatory`; you decide what UI to show. The pattern mirrors EAS / Expo-Updates' "Update available → Download now." ## Mark a release mandatory ```bash title="~/MyApp — zsh" # push the fix to everyone and flag it mandatory $ patchcli release --rollout 100 --mandatory --message "Critical: VAT rounding fix" ``` The `mandatory` flag rides through the update-check response. On the device, `checkForUpdate()` returns an `UpdateInfo` with `isMandatory == true`. ## The "Download now" pattern You stay in control of UI. The recommended flow: check on launch, and if a mandatory update exists, present a blocking sheet that downloads and reloads before letting the user proceed. ```swift title="Mandatory gate" func gateOnMandatoryUpdate() async throws { guard let info = try await Patch.shared.checkForUpdate() else { return } if info.isMandatory { // Block the UI: "A required update is available." presentBlockingSheet(version: info.version, notes: info.releaseNotes) // Download → verify → stage → hot-swap, then dismiss the gate. if try await Patch.shared.fetchUpdate() { try await Patch.shared.reloadAsync() dismissBlockingSheet() } } else { // Optional update — show a dismissible banner instead. showDismissibleBanner(info) } } ``` Prefer it fully automatic? Call the convenience, which fetches + reloads any mandatory update and no-ops otherwise: ```swift title="Auto-enforce" ContentView().task { await Patch.shared.start() // normal startup updates await Patch.shared.enforceMandatoryUpdates() // force any mandatory release } ``` **Scope** Force updates apply to the OTA module only — they hot-swap WASM logic, not native code. A change to the native shell still requires an App Store release (the fingerprint gate enforces this). Keep mandatory updates within Apple's Developer Program License Agreement and Review Guidelines. ## Related - [Staged rollouts & A/B](/rollouts/) — percentages, adoption, and rollback - [SDK reference](/sdk/) — `checkForUpdate`, `fetchUpdate`, `reloadAsync` - [Instant rollback for an iOS release](https://patchrelease.com/blog/instant-rollback-ios-release) - [The iOS hotfix playbook](https://patchrelease.com/blog/ios-hotfix-playbook) --- # CI/CD URL: https://docs.patchrelease.com/cicd/ Section: Shipping Description: Run patchcli release from CI: gate pull requests on the native-shell fingerprint, ship the patch on merge, and keep publish tokens off developer machines. A Patch CI pipeline does two things: it gates pull requests on the native-shell fingerprint, and it runs `patchcli release` on merge so deploys are reproducible, auditable, and free of long-lived credentials on developer machines. Publishing authenticates with a publish token (`ppt_…`) held as a repository secret — never the app key that ships inside your binary. The shape of a pipeline: 1. **Analyze (gate):** on pull requests, run `patchcli analyze ./Sources --check-fingerprint`. If the native shell changed, fail the job — that change must ship through the App Store. 2. **Release:** on merge to `main`, run `patchcli release` with the swift.org WASM SDK to build the module and push it behind the fingerprint check, then start a staged rollout. ## GitHub Actions ```yaml title=".github/workflows/patch.yml" # Build + release an OTA patch on every merge to main — automatically SKIPS the # release when the change isn't OTA-compatible (it touched the native shell, # which must ship through the App Store). One workflow, no separate gate. name: Patch OTA on: push: branches: [main] jobs: release: runs-on: macos-14 steps: - uses: actions/checkout@v4 # The Patch CLI + the Swift→WASM toolchain it compiles with - name: Install patchcli + toolchain run: | brew install patch-release/tap/patchcli patchcli setup # Ship the patch ONLY if the native shell is unchanged: analyze exits 1 when it # changed → not OTA-compatible → skip the release (ship via the App Store). - name: Release OTA if compatible run: | if patchcli analyze ./Sources --check-fingerprint; then patchcli release --channel production --rollout 10 \ --message "OTA: ${{ github.event.head_commit.message }}" else echo "Native shell changed — skipping OTA release (ship via the App Store)." fi env: PATCH_API_KEY: ${{ secrets.PATCH_API_KEY }} ``` Prefer to catch it at PR time too? Run the same `patchcli analyze ./Sources --check-fingerprint` as a `pull_request` check to block an incompatible merge before it lands. ## API keys & secrets Publishing authenticates with a **publish token** (`X-API-Key: ppt_…`). Create one with `patchcli login`, store it as a repository secret, and pass it to `patchcli` via `PATCH_API_KEY` — that's all the release step needs. (Self-hosting Patch? Point the CLI at your own backend with `PATCH_API_URL`.) Do **not** use `app_key` (`pak_…`) here. It ships inside your app binary, so it is public by design and the backend rejects it for anything that changes what your users run. A token is scoped to one workspace, optionally pinned to a single app, and can be revoked without rebuilding or resubmitting anything. --- # Open source URL: https://docs.patchrelease.com/open-source/ Section: Open source & self-hosting Description: The PatchSDK (MIT) and the engine (Apache-2.0) both live in patch-release/patch-swift: what is in each package, why two licences, and how to build them. The PatchSDK (MIT) and the Patch engine (Apache-2.0) are both public, in the repository [`patch-release/patch-swift`](https://github.com/patch-release/patch-swift) — the SDK as the SwiftPM package at the root, the engine as a second package nested at `cli/`. Patch edits your Xcode project and injects code into your `@main` App struct, so you shouldn't have to take either on trust. The hosted control plane is a commercial service and is not open source. ## One repository, two packages Both live in **[`patch-release/patch-swift`](https://github.com/patch-release/patch-swift)**. The SDK is the SwiftPM package at the repository root — which is why the package URL you add in Xcode is just the repository URL — and the engine is a second, independent SwiftPM package nested at `cli/`. - patch-swift/ - Package.swift **the SDK** — MIT, the package SwiftPM resolves - Sources/ the SDK's targets - cli/ **the engine** — Apache-2.0, its own SwiftPM package - Package.swift - Sources/ the engine's targets - [The SDK — repository root](https://github.com/patch-release/patch-swift): MIT · Swift 6 · runs inside your app. WasmKit runtime, value marshalling, update lifecycle, host bridges, the SwiftUI renderer. - [The engine — cli/ in the same repo](https://github.com/patch-release/patch-swift): Apache-2.0 · Swift 6 · runs on your machine or in CI. Partitioning, Swift→WebAssembly compilation, SwiftUI/UIKit lowering, project integration. ### Why two licences The SDK executes a WebAssembly interpreter **inside your users' app**, so it has to be maximally auditable and maximally permissive — MIT, no conditions worth arguing about. The engine is Apache-2.0 rather than MIT for one reason: Apache-2.0 carries an **express patent grant**. Patch's partitioning and lowering approach is novel enough that a licence which says nothing about patents is a worse answer for you, not just for us. **What is not open** The hosted control plane — rollouts, cohorts, device targeting, analytics, audit, team accounts — is a commercial service and its source is not published. The SDK will talk to **any** base URL, so you are not tied to our infrastructure. See [Running it yourself](/self-hosting/). ## What's in the engine - cli/ - Sources/ - **PartitioningEngine/** decides, per function, what can ship OTA - **CodeGenerator/** SwiftUI + UIKit lowering to the view IR - **Compiler/** Swift→WebAssembly pipeline, fingerprinting, upload - **ViewNodeIR/** the wire format the SDK renders - PatchCLI/ the `patchcli` command surface - Tests/ ~1,700 tests The interesting part is `PartitioningEngine` + `CodeGenerator`. Everything else is plumbing. If you want to understand how a SwiftUI view body becomes WebAssembly, start at `CodeGenerator/SwiftUIClassifier.swift` and follow it into `SwiftUIEmitter.swift`. ## What's in the SDK - (repository root) - Sources/ - **PatchSDK/** runtime, loader, fallback chain, update checker, host bridges - **PatchSwiftUI/** the thunk entry point every generated view calls - **PatchRender/** reconstitutes the view IR into real SwiftUI - PatchViewIR/ the SDK's copy of the wire format - PatchUIKit/ the UIKit equivalent - Tests/ ~1,000 tests **One invariant worth knowing** `cli/Sources/ViewNodeIR` and `Sources/PatchViewIR` are the **same wire format**, vendored into both packages. If you change one you must change the other, or a guest module will emit a tree the host cannot decode. The packages carry guard tests for this, and it has drifted before. ## Building from source 1. **Clone the repository and build the SDK.** One clone gets you both packages. Nothing exotic — the SDK is a normal SwiftPM package at the root. ```bash git clone https://github.com/patch-release/patch-swift cd patch-swift swift build && swift test ``` 2. **Build the engine**, the separate package in `cli/`. ```bash cd cli swift build -c release .build/release/patchcli --help ``` 3. **Add the WebAssembly toolchain** — only needed to actually compile patches. ```bash patchcli setup ``` This installs the pinned swift.org toolchain and the WebAssembly SDK. The Apple/Xcode toolchain **cannot** target WebAssembly, which is why a second toolchain is required at all. 4. **Check the setup.** ```bash patchcli doctor ``` **PATH order matters** Building the CLI for your Mac uses the Apple toolchain; compiling patches uses the swift.org one. If swiftly is first on your `PATH` you'll build the CLI with the wrong compiler. `patchcli setup` and `doctor` handle this for you — if you're doing it by hand, put `/usr/bin` first for host builds and `~/.swiftly/bin` first for WebAssembly builds. ## Reproducing our coverage numbers Every coverage figure on this site is generated from a committed measurement, not written by hand. You can re-run that measurement: ```bash ./corpus/fetch.sh # the corpus apps, at pinned commits ./tools/swiftui-corpus-coverage/run.sh # the census ``` `corpus/fetch.sh` clones each app at the exact commit recorded in `corpus/manifest.yml`, so the corpus is reproducible rather than a snapshot we happen to have on a laptop. See [what Patch can and can't update](/coverage/) for the current results. ## Contributing Issues and pull requests for both packages go to the same repository, [`patch-release/patch-swift`](https://github.com/patch-release/patch-swift). Two things that make a bug report actionable immediately: ```bash patchcli doctor --json ``` That prints your setup as JSON — versions, toolchain, and whether the app's fingerprint is registered. Attach it. If you're reporting that a view didn't patch when you expected it to, the build output already names the blocking reads per view; paste that too. --- # Running it yourself URL: https://docs.patchrelease.com/self-hosting/ Section: Open source & self-hosting Description: PatchSDK talks to any base URL, so you can serve modules yourself — reference architectures for GCP, AWS and Azure, and what static hosting cannot do. Self-hosting Patch means serving OTA modules from infrastructure you control: the engine and SDK are open source, and `PatchConfiguration.apiBaseURL` accepts any base URL. What is not open source is the hosted control plane — rollouts, targeting, analytics, audit and team accounts — so a self-hosted deployment means building or forgoing those. This page is deliberately honest about what that gets you and what it doesn't. ## Decide what you actually need A bucket and a CDN. **Planned, not shipped** — there is no static-publish command yet. No rollouts, no targeting, no recall even once it lands. A small service that decides per request, so percentage rollouts, cohorts and rollback recall all work. You run it and you support it. The hosted control plane — rollouts, targeting, analytics, audit, team accounts, and someone to call. Most teams should use the hosted service. The self-host path exists so that choosing it isn't a one-way door. ## Static vs a server Static publishing is **not shipped yet** (see the "Static only" tab). This is what the two paths will look like when it is, so you can plan — and because the limits are structural, not temporary: | Capability | Static | Server | | --- | --- | --- | | Serve the newest patch | Yes | Yes | | Fingerprint gating | Yes | Yes | | Rollback by republishing | Yes | Yes | | **Percentage rollouts** | No | Yes | | **Cohort / version targeting** | No | Yes | | **Rollback recall** — pull a patch back from devices that already took it | No | Yes | | Device-cap enforcement, adoption metrics | No | Yes | Rollout bucketing is computed per device, per request. A file in a bucket can't do that. Recall is worse: it needs to know what each device is currently running, which is a query, not an object. ## Pointing the SDK somewhere else Wherever you host, this is the only client change: ```swift Patch.configure(.init( appKey: "pak_…", apiBaseURL: URL(string: "https://patches.example.com/api/v1"))) ``` And the CLI: ```bash patchcli release --base-url https://patches.example.com # or: export PATCH_API_URL=https://patches.example.com ``` Setting `apiBaseURL` to `nil` disables remote checks entirely — the app runs whatever module is bundled or already cached. ## Reference architectures The shape is the same everywhere: **object storage** for the module bytes, a **CDN** in front of it, a **small stateless service** for the check endpoint, and **Postgres** for releases and device state. Nothing here is exotic — it fits in the smallest tier of any of these providers. ``` patchcli ──HTTPS──▶ Cloud Run ──▶ Cloud SQL (Postgres) │ releases · fingerprints · check-ins └──▶ GCS bucket (module.wasm.br) │ device ◀── Cloud CDN ◀───────┘ ``` 1. **Bucket for modules.** ```bash gcloud storage buckets create gs://patch-modules-prod \ --location=us-central1 --uniform-bucket-level-access ``` 2. **Postgres.** The smallest shared-core tier is plenty — this database holds releases and device check-ins, not user data. ```bash gcloud sql instances create patch-db \ --database-version=POSTGRES_16 --tier=db-f1-micro --region=us-central1 ``` 3. **Deploy the check service to Cloud Run**, pointed at both. ```bash gcloud run deploy patch-api \ --image= --region=us-central1 --allow-unauthenticated \ --set-env-vars=DATABASE_URL=...,STORAGE_BUCKET=patch-modules-prod ``` 4. **Put Cloud CDN in front of the bucket** via a backend bucket on an external HTTPS load balancer. Modules are immutable and content-addressed, so cache them hard. ``` patchcli ──HTTPS──▶ ALB ──▶ ECS Fargate ──▶ RDS Postgres │ └──▶ S3 bucket (module.wasm.br) │ device ◀── CloudFront ◀──────┘ ``` 1. **Bucket for modules**, with public access blocked — CloudFront reads it through an Origin Access Control and nothing else does. ```bash aws s3api create-bucket --bucket patch-modules-prod --region us-east-1 aws s3api put-public-access-block --bucket patch-modules-prod \ --public-access-block-configuration \ BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true ``` 2. **Postgres.** ```bash aws rds create-db-instance --db-instance-identifier patch-db \ --engine postgres --db-instance-class db.t4g.micro --allocated-storage 20 ``` 3. **Run the check service on ECS Fargate** behind an ALB. App Runner works too and is less to configure; Fargate is the right answer if you already have a VPC and want the service inside it. 4. **CloudFront distribution** over the bucket, using the OAC from step 1. If you'd rather not run a container at all, the check endpoint is small enough to be a Lambda behind API Gateway. The tradeoff is a cold start on the update check — usually fine, since it happens at launch and is already asynchronous. ``` patchcli ──HTTPS──▶ Container Apps ──▶ Azure Database for PostgreSQL │ └──▶ Blob Storage (module.wasm.br) │ device ◀── Azure Front Door ◀──┘ ``` 1. **Storage account and container** for the modules. ```bash az storage account create --name patchmodules --sku Standard_LRS \ --resource-group patch-rg --location eastus az storage container create --name modules --account-name patchmodules ``` 2. **Postgres.** ```bash az postgres flexible-server create --name patch-db \ --resource-group patch-rg --tier Burstable --sku-name Standard_B1ms ``` 3. **Deploy the check service to Container Apps.** ```bash az containerapp create --name patch-api --resource-group patch-rg \ --image --ingress external --target-port 8000 \ --env-vars DATABASE_URL=... STORAGE_CONTAINER=modules ``` 4. **Azure Front Door** in front of the blob container for edge caching. **Not available yet** A one-command static publish (`patchcli publish --static`) is **planned, not shipped**. Today the CLI uploads through the check API; there is no command that emits a static manifest. This tab documents the intended shape so you can plan for it — don't build against it yet. The idea: emit the module plus the JSON manifest the SDK expects, upload both to any bucket or static host, and point the SDK at it. ```swift Patch.configure(.init( appKey: "pak_…", apiBaseURL: URL(string: "https://cdn.example.com/patch"))) ``` Rolling back would mean republishing the previous manifest. If you need self-hosting **today**, use the server path in the other tabs — the SDK's `apiBaseURL` already points anywhere, so the constraint is that something must answer the check endpoint, not that it must be ours. ## Sizing The check endpoint is a small read — one row lookup plus a hash. It is not the expensive part of your infrastructure. | Fleet | Check service | Postgres | Egress | | --- | --- | --- | --- | | Up to ~50k devices | One small instance | Smallest tier | Patches are KB, not MB | | ~500k devices | Two instances behind a load balancer | Smallest tier plus a read replica | CDN absorbs it | | Millions | Autoscale on CPU | Managed, connection-pooled | CDN absorbs it | Modules are content-addressed and immutable, so CDN hit rates are very high — the origin only serves the first request per release per edge. ## Operational notes **Modules are immutable.** A release is content-addressed by its SHA-256. Never overwrite one in place — publish a new version and update the manifest. The SDK verifies the hash before activating, so a swapped or corrupted object fails closed rather than running. **Keep the fingerprint contract.** The device reports the fingerprint of the build it's running, and your service must only serve modules built against that same shell. Getting this wrong is the one way to ship a patch that genuinely breaks an app — see [the compatibility fingerprint](/fingerprint/). **Serve the compressed artifact as-is.** Modules ship brotli-compressed (`.wasm.br`). Don't set `Content-Encoding: br` on the object — the SDK fetches the compressed bytes and decompresses them itself, so a transport-level re-encode breaks the hash check. **Retention.** Device check-ins accumulate a row per device per app. Add a retention job; nothing prunes it for you. ## What you give up - No rollout dashboard, adoption graphs, or error-spike alerts - No team accounts, roles, or audit trail - No `patchcli init` browser onboarding — you hand-write `.Patch.yml` and issue your own app keys - No support for your deployment - [Open source](https://docs.patchrelease.com/open-source/): What's in each package, how to build from source, and how to reproduce our coverage numbers. --- # How Patch compares URL: https://docs.patchrelease.com/compare/ Section: Open source & self-hosting Description: Patch vs CodePush, Expo EAS Update and Shorebird: which stack each one patches, what stays native, the Apple basis, rollback, rollouts and open-source status. Patch, CodePush, Expo EAS Update and Shorebird all ship code to an installed app without a new store binary, and they differ in one decisive way: which stack each one can patch. Patch patches native Swift and SwiftUI. CodePush and EAS Update patch a React Native JavaScript bundle. Shorebird patches Dart in a Flutter app. ## The table Every row below is taken from the vendor's own documentation, linked in the section for that tool. A dash means the value was not verified against the vendor's own docs while this page was written — it is not a claim the tool lacks the feature. | | Patch | CodePush | Expo EAS Update | Shorebird | | --- | --- | --- | --- | --- | | Framework / language | Native Swift + SwiftUI | React Native (JavaScript) | React Native / Expo (JavaScript) | Flutter (Dart) | | What gets patched | Swift logic, `async`/`await`, SwiftUI view bodies — compiled to WebAssembly | "your JavaScript and images" | "non-native pieces (such as JS, styling, and images)" | "any Dart code in your application no matter of size" | | What stays native | OS-API code, and any native symbol/framework/entitlement not already in the signed binary | "changes which touch native code … cannot be distributed via CodePush" | "Change to native code or native dependencies" needs a new build | "does not support changing native code (e.g. Java/Kotlin on Android or Objective-C/Swift on iOS)" | | Stated Apple basis | Interpreted code under DPLA §3.3.1(B) (formerly §3.3.2) | — | "your updates need to follow the App Store and Play Store guidelines" | DPLA "section 3.3.1b"; "a custom Dart interpreter to comply with the interpreter-only restriction for updates on iOS" | | Rollback | `patchcli rollback`, fleet-wide, propagates on the next update check | Automatic client-side rollback of a crashing update | Revert / republish a previous update | — | | Channels & staged rollouts | Channels + `--rollout N` percentage bucketing + version/OS/cohort targeting | Staging/Production deployments; `promote … -r 20` percentage rollout | Channels/branches; per-update and branch-based rollouts | Tracks (stable / beta / staging); percentage rollout via tracks + app-side grouping | | Minimum OS | iOS 15+ | — | — | "the same versions of platforms that Flutter supports" | | Open-source SDK | Yes — MIT (engine Apache-2.0) | Repository archived 20 May 2025 | — | — | | Hosted service | Yes — Hobby free (100 devices), Startup $59/mo, Enterprise | Retired 31 March 2025 | Yes | Yes | ## Can CodePush update a native Swift app? No — CodePush updates a React Native app's JavaScript bundle and images, not Swift. Microsoft's README states: "The CodePush plugin helps get product improvements in front of your end users instantly, by keeping your JavaScript and images synchronized with updates you release", and that "Any product changes which touch native code (e.g. modifying your `AppDelegate.m`/`MainActivity.java` file, adding a new plugin) cannot be distributed via CodePush." The hosted service is also gone: Microsoft states "Visual Studio App Center is scheduled for retirement on March 31, 2025", and the `react-native-code-push` repository records "This repository was archived by the owner on May 20, 2025." A standalone [code-push-server](https://github.com/microsoft/code-push-server) was published for self-hosting; that repository is archived too. If your app is React Native and you want to keep the CodePush workflow, the self-hosted server is the migration Microsoft points at. If your app is native Swift, CodePush was never able to patch it. Sources: [App Center retirement](https://learn.microsoft.com/en-us/appcenter/retirement) · [react-native-code-push](https://github.com/microsoft/react-native-code-push). See also [Patch vs CodePush](https://patchrelease.com/codepush-alternative). ## Can Expo EAS Update update a native Swift app? No — EAS Update serves updates to projects using the `expo-updates` library, which means React Native. Expo describes it as "a cloud service that serves updates for projects using the expo-updates library", and an app can "update its own non-native pieces (such as JS, styling, and images) over-the-air". Its FAQ lists "Change to native code or native dependencies" and "Anything that requires a new app binary version" as cases where you build instead. For a React Native or Expo team, EAS Update is the right tool and it is more mature at what it does: channels and branches, per-update and branch-based rollouts, and revert. Patch is for teams whose app is Swift, where there is no JavaScript bundle to replace. Sources: [EAS Update introduction](https://docs.expo.dev/eas-update/introduction/) · [EAS Update rollouts](https://docs.expo.dev/eas-update/rollouts/). See also [Patch vs EAS Update](https://patchrelease.com/expo-eas-update-alternative). ## Can Shorebird update a native Swift app? No — Shorebird patches Dart code in a Flutter app. Its FAQ states that Shorebird "can change any Dart code in your application no matter of size" and that it "does not support changing native code (e.g. Java/Kotlin on Android or Objective-C/Swift on iOS)". Shorebird is the one tool here that names Apple's clause: its FAQ quotes DPLA "3.3.1b" and states that "Shorebird uses a custom Dart interpreter to comply with the interpreter-only restriction for updates on iOS". Percentage rollouts are built on tracks: the guide describes "how you can implement a percentage-based patch rollout system using predefined tracks", with the app reading its group number and deciding between the beta and stable track. If your app is Flutter, Shorebird is the tool for it. Patch cannot patch Dart, and Shorebird cannot patch Swift. Sources: [Shorebird FAQ](https://docs.shorebird.dev/code-push/faq/) · [Percentage-based rollouts](https://docs.shorebird.dev/code-push/guides/percentage-based-rollouts/). See also [Patch vs Shorebird](https://patchrelease.com/shorebird-alternative). ## What does Patch do that the other three do not? Patch patches native Swift. The `patchcli` CLI compiles the Swift you changed to WebAssembly, and the PatchSDK runs that module in [WasmKit](https://github.com/swiftwasm/WasmKit) inside your signed binary. There is no JavaScript bridge, no web view and no cross-platform runtime, and the App Store binary is never modified. Code that touches an OS API stays native, and a patch that cannot run falls back to the signed binary. See [how it works](/how-it-works/) and [what Patch can & can't update](/coverage/). ## Which one should I use? It follows from the stack, not from the feature list. React Native or Expo: use [EAS Update](https://docs.expo.dev/eas-update/introduction/), or a self-hosted CodePush server if you are migrating an existing CodePush app. Flutter: use [Shorebird](https://docs.shorebird.dev/). Native Swift and SwiftUI: none of those three can patch your app, and Patch is built for it. ## Related - [OTA update tools compared](https://patchrelease.com/ota-update-tools-compared) — the same comparison on the marketing site - [Patch vs CodePush](https://patchrelease.com/codepush-alternative) - [Patch vs Expo EAS Update](https://patchrelease.com/expo-eas-update-alternative) - [Patch vs Shorebird](https://patchrelease.com/shorebird-alternative) - [Apple compliance](/apple-compliance/) — the DPLA clause and Guideline 2.5.2 - [Pricing](https://patchrelease.com/pricing) --- # Apple compliance URL: https://docs.patchrelease.com/apple-compliance/ Section: Open source & self-hosting Description: Apple permits an app to download and run interpreted code that does not change its primary purpose — the DPLA clause, Guideline 2.5.2 verbatim, and the limits. Apple's Developer Program License Agreement permits an app to download and run interpreted code that does not change the app's primary purpose. Patch ships a WebAssembly module executed by an interpreter already inside your signed binary. Shorebird cites the same clause by number for Flutter; CodePush and EAS Update ship a JavaScript bundle to a binary they likewise never modify. This page sets out the clause, the review guideline, and the boundary that is your responsibility rather than the tool's. ## Does Apple allow over-the-air updates to a Swift app? Yes — Apple's Developer Program License Agreement permits an application to download and run **interpreted code**, provided that the downloaded code does not change the primary purpose of the application, does not create a store or storefront for other code, and does not bypass the operating system's sandbox or code-signing protections. Nothing in that clause is specific to JavaScript or Dart, so a WebAssembly module interpreted by a runtime compiled into your app sits in the same position as a React Native bundle. What the clause does not do is license a change that turns your app into a different product after review — that limit is on you, not on the mechanism. ## What does DPLA §3.3.1(B) require? DPLA §3.3.1(B), headed "Executable Code" and formerly numbered §3.3.2, is the interpreted-code provision. Its first paragraph reads, verbatim: > 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. — [Apple Developer Program License Agreement](https://developer.apple.com/support/downloads/terms/apple-developer-program/Apple-Developer-Program-License-Agreement-English.pdf), section 3.3.1 B, retrieved 22 August 2026 Three conditions, then: primary purpose, security features, and no storefront. Section numbers have moved between revisions of the agreement: section 3.3.2 in the current text is "Regulatory Compliance", an unrelated rule about laws and regulations. Cite both numbers, and read the current agreement, published at [developer.apple.com/support/terms](https://developer.apple.com/support/terms/). **Read the current agreement** The quote above was taken from Apple's PDF on 22 August 2026. Apple revises the agreement, and the clause has already been renumbered once — the 2017 warning emails quoted below cite section 3.3.2 for the same rule. Re-read the current text at [developer.apple.com/support/terms](https://developer.apple.com/support/terms/) before you rely on it, and treat this page as a pointer, not as legal advice. ## What does App Review Guideline 2.5.2 say? Guideline 2.5.2 is the review-time counterpart to the DPLA clause. Apple's current text reads, verbatim: > 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. — [App Review Guidelines, 2.5.2](https://developer.apple.com/app-store/review/guidelines/#software-requirements) Read alongside §3.3.1(B), the operative words are *introduces or changes features or functionality*. Fixing a defect in a feature you shipped and had reviewed is not the same act as introducing one that was not reviewed, and the DPLA's interpreted-code allowance is what carves out the former. That reading is what every OTA update tool on iOS depends on, and it is a reading, not a guarantee — Apple decides. ## How does Patch stay inside the rules? **Your signed binary is never modified.** Patch does not rewrite, re-sign, or replace the app you shipped through review. The binary on the device is byte-for-byte the one Apple notarised. **Only interpreted code updates.** A patch is WebAssembly, executed by the [WasmKit](https://github.com/swiftwasm/WasmKit) interpreter embedded in your app. It is never native machine code, and it is never loaded as an executable. **The sandbox is intact.** Patched code reaches the system only through host functions your binary already exposes. It cannot call a framework you did not link, cannot acquire an entitlement you did not declare, and cannot escape the app sandbox. This is enforced by the architecture, not by policy — see [what Patch can change](/coverage/). **The primary purpose is unchanged.** Patch is for fixing and iterating on the app you shipped. Which leads to the part that is genuinely your responsibility. ## What did Apple ban in 2017, and why is this different? In March 2017 Apple sent warning emails to developers whose apps embedded "hot code push" SDKs. The notice, as reproduced at the time, read: "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, which is not in compliance with section 3.3.2 of the Apple Developer Program License Agreement and App Store Review Guideline 2.5.2." It added that "This code, combined with a remote resource, can facilitate significant changes to your app's behavior compared to when it was initially reviewed for the App Store." ([9to5Mac, 7 March 2017](https://9to5mac.com/2017/03/07/apple-cracks-down-on-hot-push-code-sdks/)) Two things follow from that. First, Apple did not withdraw the interpreted-code provision — it is in the current agreement as §3.3.1(B), quoted verbatim above. Second, the objection Apple stated was about *changing behaviour beyond what was reviewed*, which is the same boundary §3.3.1(B) draws. Patch narrows the mechanism as far as the architecture allows: a patch is interpreted WebAssembly that can only call symbols already compiled and signed into the reviewed binary, and it can never add a framework, entitlement or privacy capability the app did not ship with. It cannot narrow your intent for you. ## What must a patch never do? Do not ship a patch that introduces functionality you concealed during review. Do not use patches to enable features App Review rejected. Do not build a mechanism for users to obtain or run third-party code. Patch cannot enforce any of this — no OTA tool can, because these are statements about intent, not about bytes. They are terms you accepted as a developer, and they are the part of the compliance argument that stays with you. The uses Patch is designed for sit well inside the line: fixing bugs, correcting copy, adjusting layout, changing business rules, iterating on views within the app you already shipped. ## What is the precedent for this? React Native's CodePush, Expo's EAS Update and Flutter's Shorebird all deliver interpreted code updates under the same provision. Microsoft published [react-native-code-push](https://github.com/microsoft/react-native-code-push) in June 2015, and interpreted-code updates have been standard practice in production React Native apps since. The practice is well established and widely documented. Patch differs in what it interprets — WebAssembly compiled from your Swift, rather than JavaScript or Dart — but the compliance position is identical, and if anything narrower: the code that ships is a fragment that can only call into symbols your reviewed binary already contains. See [How Patch compares](/compare/) for what each tool patches. ## What should I say if App Review asks? Be straightforward and factual. The app downloads interpreted WebAssembly used to update covered application logic and UI. The signed binary is unchanged, no new capabilities are introduced, and nothing bypasses the sandbox or code signing. If it helps, name the mechanism: the module runs in an embedded WebAssembly interpreter and can only call functions the reviewed binary already exports. We are not your lawyers, and this page is not legal advice. If your app is in a regulated category, or you are unsure whether a change alters your primary purpose, get your own advice. ## Related - [What Apple allows for OTA updates](https://patchrelease.com/blog/what-apple-allows-ota-updates) - [Update an iOS app without App Store review](https://patchrelease.com/blog/update-ios-app-without-app-store-review) - [OTA update tools compared](https://patchrelease.com/ota-update-tools-compared) - [How Patch compares](/compare/) — Patch, CodePush, EAS Update and Shorebird - [FAQ](/faq/) — short answers, including the Guideline 2.5.2 question - [What Patch can & can't update](/coverage/) — the architectural boundary --- # Team accounts & roles URL: https://docs.patchrelease.com/team/ Section: Account Description: A Patch workspace owns apps, releases and members under three ranked roles — owner, admin, member — with last-owner protection on every mutation. Patch organizes everything under a **workspace**. A workspace owns apps, releases, channels, and members. When you first sign in to the dashboard, Patch creates a personal workspace for you with the `owner` role; you can then invite teammates and provision more apps. **Team collaboration (inviting a second member) is a paid feature** — see [Plans & billing](/billing/). ## Roles (RBAC) Every member holds exactly one role in their workspace. The roles are ranked **`owner` > `admin` > `member`**, and that rank drives who can manage whom. Owner Full control: manage the billing plan, apps, channels, and members. Can invite, remove, and change roles for anyone — and is the only role that can grant the `owner` role. A workspace must always keep at least one owner. Admin Manage apps, ship releases, adjust rollouts, roll back, and invite, remove, or re-role members — but only `admin`/`member` roles. An admin can never grant `owner`, nor manage (re-role/remove) an existing owner. Member Day-to-day access: view apps, releases, and rollout status, and ship within the team's apps. Can view the member list, but cannot invite, remove, or re-role anyone, change the plan, or manage workspace settings. ## Who can do what Two invariants govern every action: the **rank rule** (you can only assign or manage a role at or below your own rank) and the **last-owner protection** (the workspace can never drop to zero owners). | Action | Owner | Admin | Member | | --- | --- | --- | --- | | View the member list | ✓ | ✓ | ✓ | | Invite an admin / member | ✓ | ✓ | — | | Invite / grant the owner role | ✓ | — | — | | Change an admin / member role | ✓ | ✓ | — | | Change (re-role) an existing owner | ✓ | — | — | | Remove an admin / member | ✓ | ✓ | — | | Remove an owner | ✓ | — | — | | Remove / demote the last owner | — | — | — | | Change the billing plan | ✓ | — | — | A **deactivated** member loses access to all role-gated actions regardless of their assigned role. Requests to a workspace you don't belong to are rejected. ## Member endpoints These workspace-scoped routes back the dashboard's **Team** page. You must be a member of the workspace; mutating actions additionally require `owner` or `admin`. | Endpoint | What it does | Who | | --- | --- | --- | | POST /workspaces/`{id}`/members | Invite a member by email (creates a pending member until they sign in). Audited as member.invite. | owner / admin | | GET /workspaces/`{id}`/members | List all members (active and pending) of the workspace. | any member | | PATCH /workspaces/`{id}`/members/`{user_id}` | Change a member's role. Blocks demoting the last owner. Audited as member.role_change. | owner / admin | | DELETE /workspaces/`{id}`/members/`{user_id}` | Remove a member (revokes access immediately). Blocks removing the last owner. Audited as member.remove. | owner / admin | ## Inviting members Owners and admins invite by email from the dashboard's **Team** page. An invite creates a *pending* member; when that person signs in with the same email, their account links automatically and they take the assigned role. Removing a member revokes access immediately (you can't remove the last owner). Trying to invite a second member on the free Hobby plan returns `402 Payment Required` with an upgrade message — see [Plans & billing](/billing/). **Auth model** Three credentials, deliberately separate: - **Publish token** (`X-API-Key: ppt_…`) — the CLI's write credential. Scoped to a workspace, optionally pinned to one app, revocable. Created by `patchcli login` or from the dashboard. - **App key** (`pak_…`) — a *public* app identifier. It ships inside your binary and rides the request body when a device checks for an update. It authenticates nothing. - **Firebase ID token** (`Authorization: Bearer …`) — the dashboard, for humans. Revoking a publish token cuts off publishing immediately and needs no app release, because the credential was never in the app. ## The dashboard The dashboard is the team's web app for everything that isn't a CLI command: - **Apps overview** — every app in the workspace; pick one to drill in. - **Rollouts** — the main per-app view: recent releases across channels, newest first, with version, channel, status (active / superseded / rolled back), targeted %, received % (a progress bar), the mandatory flag, and when it was pushed. Click a release for stats (received vs targeted, download / activation / error counts) and actions to adjust the rollout % or roll back. - **Channels** — the active module per channel and its rollout/mandatory state. - **Usage** — per-app release-health analytics: active devices, adoption, error rate, and trends over the window — plus devices checked in from the base build before your first release ships (see [Usage & analytics](/usage/)). - **Team** — list, invite, re-role, and remove members. - **Activity** — the workspace audit trail of who did what, on the **Enterprise plan** (see [Audit log](/audit/)). - **Settings & Billing** — the current plan, the plan comparison matrix, and the owner-only upgrade/downgrade control (see [Plans & billing](/billing/)). - **Quickstart** — for a workspace that hasn't deployed yet, a guided setup with your app's API key prefilled and the same `patchcli init` / `patchcli release` snippets shown here. --- # Plans & billing URL: https://docs.patchrelease.com/billing/ Section: Account Description: Hobby is free and serves up to 100 devices; Startup and Enterprise add team members, staged rollouts, channels beyond production, and a larger device fleet. Every Patch workspace carries a billing **plan** — `hobby`, `pro` or `team` — that gates a small set of team-scale features. Everything core to shipping OTA updates is on the free tier: unlimited apps, unlimited 100% production releases, instant rollback, the CLI, CI/CD, and the ship-safety checks. What the paid plans add is collaboration, staged rollouts, channels beyond `production`, a larger device fleet, and the audit log. ## Tiers The pricing tiers on [patchrelease.com](https://patchrelease.com/#pricing) map to the plan stored on your workspace: | Marketing tier | Workspace plan | Who it's for | | --- | --- | --- | | Hobby (Free) | hobby | Individual workspace — one member, full 100% production releases, up to 100 distinct devices. | | Startup | pro | Teams that need collaboration, staged rollouts, A/B, and channels. Serves up to 10,000 devices. | | Enterprise | team | Larger orgs; everything in Startup, plus the audit log & activity feed, support/SLA, and an unlimited device fleet. | A new workspace defaults to `hobby`. Startup and Enterprise differ commercially (usage limits, support) and on one capability — the **audit log & activity feed** is an **Enterprise (Team) plan** feature. ## What's paid vs. free A small set of features are paid-only, gated consistently across the CLI, API, and console: - **Team collaboration** — inviting members. Hobby is an individual workspace capped at one member; a second invite is a paid feature. - **Staged rollouts & A/B** — any non-100% `rollout_pct`. Hobby ships full 100% releases only; shipping to a percentage of devices (a phased rollout / A/B cohort) is paid. - **Non-production channels** — Hobby ships to `production` only; canary, beta, staging and other channels are paid. - **Device fleet size** — Hobby serves patches to at most **100 distinct devices** (counted over the recent active window). Pro raises the cap to **10,000** devices and Team is **unlimited**. Once a Hobby workspace is at the cap, a *new* device's update check simply returns "no update" — devices already in the counted set keep updating, so the existing fleet is never cut off. | Feature | Hobby (free) | Pro / Team (paid) | | --- | --- | --- | | Apps | Unlimited | Unlimited | | OTA releases (100% production) | Unlimited | Unlimited | | Instant rollback | ✓ | ✓ | | Usage analytics | ✓ | ✓ | | Distinct devices served | Up to 100 | Pro: 10,000 · Team: unlimited | | Team members | 1 (individual) | Unlimited | | Staged / A-B rollouts (non-100%) | — | ✓ | | Release channels (beyond production) | — | ✓ | | Audit log & activity feed | — | Enterprise (Team) only | ## How gating works When a Hobby workspace attempts a paid action, you receive a **`402 Payment Required`** response with a plain-language upgrade message. The console surfaces that message as an upgrade prompt. The affected operations: - Inviting a second member (`POST /workspaces/{id}/members`). - Pushing a non-100% rollout or a non-production channel (`POST /modules`). - Adjusting rollout % from the console on a Hobby plan. The **device-fleet cap** works differently: once a Hobby workspace reaches its 100-device cap, new devices are simply served "no update" at their next check — devices already in the active set keep receiving updates, so the existing fleet is never cut off. Upgrading lifts the cap immediately. ## Changing the plan The plan lives on the workspace and is changed by the **owner** from the console's **Settings & Billing** page, which calls: ```bash title="PATCH the workspace plan" $ curl -X PATCH https://api.patchrelease.com/api/v1/workspaces/$WS/plan \ -H "Authorization: Bearer $ID_TOKEN" \ -H "Content-Type: application/json" \ -d '{"plan":"pro"}' ``` Only the workspace owner may call it; valid plans are `hobby`, `pro`, and `team`. The change is recorded in the audit log as `plan.change`. **Downgrading** Switching back to Hobby re-applies the free-tier limits going forward — new invites and non-100% rollouts are gated again. Existing data isn't deleted; the gate simply applies the next time you attempt a paid action. --- # Usage & analytics URL: https://docs.patchrelease.com/usage/ Section: Account Description: Per-app release health derived from device events: active devices, adoption, error rate and a daily time series, broken down by version, channel and OS. Patch usage analytics are read-only aggregates over the device events PatchSDK already reports: download, activation and error, per device (see [Telemetry](/sdk/)). The console's **Usage** page rolls those events into release health so you can answer "is this release healthy and adopted?" at a glance. There is no extra wiring beyond the SDK, and every query is scoped to your own workspace. ## Endpoints Both endpoints are Firebase-authed and take an optional `?days=N` window (default 30, 1–365). They power the console; you can also call them directly for your own dashboards. | Endpoint | What it returns | | --- | --- | | GET /apps/`{id}`/usage?days=30 | App-level summary: active devices, adoption %, error rate, distinct releases, plus a daily time series and breakdowns by version, by channel, and by OS. | | GET /workspaces/`{id}`/usage?days=30 | Workspace rollup: one row per app (active devices, error rate, latest released version) and workspace-wide totals for the window. | ## What each metric means - **Active devices** — distinct devices (by `device_id`) that sent any event in the window. - **Adoption %** — of those active devices, the share currently running the app's *active production version*. This is the number that should climb toward 100% as a rollout widens. - **Error rate** — `errors / (activations + errors)` over the window; `0` when there's no activity. The same definition the CLI's `patchcli status` uses. - **Releases** — count of distinct module versions ever pushed for the app. - **Daily time series** — one zero-filled point per calendar day (UTC) in the window — downloads, activations, errors, and active devices — so the series length always equals `days`. - **By version** — downloads / activations / errors / active devices per module version. - **By channel** — the same counts folded per channel, with the channel's currently-active version. - **By OS** — the top OS versions by distinct devices (active devices + event count each), to spot OS-specific regressions. **Scoping** Usage is strictly workspace-scoped: you can only query apps in your own workspace, and the workspace rollup only ever covers your own data. --- # Audit log URL: https://docs.patchrelease.com/audit/ Section: Account Description: Every action in a Patch workspace — pushes, rollout changes, rollbacks, role and plan changes — is recorded to an immutable trail, in the console or by API. The Patch audit log is an immutable, workspace-scoped record of who did what: sign-ins, app creation, fingerprint registration, module pushes, rollout changes, rollbacks, member invites and role changes, and plan changes. Each entry captures the actor, the workspace, the app when relevant, a structured before/after payload, and a timestamp. The console shows it on the **Activity** page; the same data is readable over the API. **Enterprise plan** The audit log & activity feed is an **Enterprise (Team) plan** feature. The events below are recorded for every workspace, but the **Activity** page and the audit API are available on the Enterprise plan — see [Plans & billing](/billing/). Every meaningful action in a workspace is recorded to an immutable audit trail — useful for security review, compliance, and answering "who changed this, and when?". The console surfaces it on the **Activity** page; you can also read it over the API. ## What's recorded Each entry captures the actor (a user's email, or `api-key` for CLI/SDK-driven actions), the workspace, the app when relevant, the action, a structured `details` payload (old → new values), and a timestamp. The recorded actions: | Action | When | | --- | --- | | login / account.create | A user signs in; a brand-new account + personal workspace is created on first sign-in. | | app.create / fingerprint.register | An app is provisioned; a native-shell fingerprint is registered. | | module.push | A module is pushed (via patchcli release / push). | | rollout.change | A release's rollout % is adjusted. | | module.rollback | A release is rolled back. | | member.invite / member.role_change / member.remove | A teammate is invited, re-roled, or removed. | | plan.change | The workspace billing plan changes (old → new). | ## Reading the trail The **Activity** page reads `GET /api/v1/audit`. It's always scoped to your own workspace (a cross-workspace read is impossible), newest first, with pagination and optional filters. ```bash title="Read the audit trail" # newest 50 events for your workspace $ curl https://api.patchrelease.com/api/v1/audit \ -H "Authorization: Bearer $ID_TOKEN" # filter by action and/or app, with pagination $ curl "https://api.patchrelease.com/api/v1/audit?action=module.rollback&limit=20&offset=0" \ -H "Authorization: Bearer $ID_TOKEN" $ curl "https://api.patchrelease.com/api/v1/audit?app_id=$APP_ID" \ -H "Authorization: Bearer $ID_TOKEN" ``` Query params: `action` (an exact action string), `app_id` (must belong to your workspace, else `403`), `limit` (1–200, default 50), and `offset`. --- # Webhooks & error-spike alerts URL: https://docs.patchrelease.com/webhooks/ Section: Account Description: Patch sends signed POSTs for release.pushed, rollout.changed, rollback and error_spike, so Slack or on-call hears about a release without polling a dashboard. A Patch webhook is an HTTPS endpoint you register to receive workspace events in real time — post a release to Slack, page on-call when an error rate spikes, or kick off a downstream job. Patch sends a signed `POST` to every registered endpoint subscribed to the event. They're managed by a workspace **owner** or **admin**, in the console under **Settings → Webhooks** or over the API. ## Events Pick any combination of the four event types per endpoint: | Event | Fires when | | --- | --- | | release.pushed | A new OTA release is shipped (patchcli release / push). Payload includes the app, module & version, channel, rollout %, mandatory, sha256, and release notes. | | rollout.changed | A release's rollout % is raised or lowered. | | rollback | A release is rolled back to the previous module. | | error_spike | An app's active production version crosses the error-rate threshold (see below). Payload includes the app, version, error_rate, errors, activations, sample, window_minutes, and threshold. | ## Register an endpoint In the console, open **Settings → Webhooks**, paste an HTTPS URL, tick the events you want, and save. Patch reveals a one-time **signing secret** at creation — copy it then; it's the key you use to verify deliveries. Each row has a **Test** button that sends a sample `ping` delivery so you can confirm connectivity and your signature check before relying on it, and a **Delete** that stops delivery immediately. The same operations are available over the API (owner/admin, scoped to the workspace): `POST` / `GET /api/v1/workspaces/{workspace_id}/webhooks`, `DELETE …/webhooks/{webhook_id}`, and `POST …/webhooks/{webhook_id}/test`. ```bash title="Create a webhook" # register an endpoint subscribed to releases + error spikes $ curl -X POST https://api.patchrelease.com/api/v1/workspaces/$WORKSPACE_ID/webhooks \ -H "Authorization: Bearer $ID_TOKEN" \ -H "Content-Type: application/json" \ -d '{"url":"https://hooks.example.com/patch","events":["release.pushed","error_spike"]}' # the response includes the signing secret — store it now, you'll need it to verify # { "id": "...", "url": "...", "secret": "whsec_…", "events": [...], "is_active": true } # send a sample `ping` delivery to confirm the endpoint + signature setup $ curl -X POST https://api.patchrelease.com/api/v1/workspaces/$WORKSPACE_ID/webhooks/$WEBHOOK_ID/test \ -H "Authorization: Bearer $ID_TOKEN" ``` ## Delivery format Every delivery is an HTTP `POST` with a compact-JSON body — the envelope `{event, workspace_id, data, timestamp}`, where `data` is the event-specific payload — and an `X-Patch-Signature` header: ```json title="POST body (error_spike)" { "event": "error_spike", "workspace_id": "a1b2c3d4-…", "data": { "app_id": "…", "app_name": "Acme", "version": "1.4.2", "error_rate": 0.18, "errors": 9, "activations": 41, "sample": 50, "window_minutes": 15, "threshold": 0.1 }, "timestamp": "2026-06-04T12:00:00Z" } ``` ## Verify the signature The `X-Patch-Signature` header is `sha256=`, computed over the **raw request bytes** with your endpoint's signing secret. Recompute the HMAC on your side and compare with a constant-time check before trusting a delivery: ```python title="verify_signature.py" # Flask receiver — reject any delivery whose signature doesn't match. SECRET = "whsec_…" # the signing secret shown once when the webhook was created def verify(raw_body: bytes, header: str) -> bool: expected = "sha256=" + hmac.new( SECRET.encode(), raw_body, hashlib.sha256 ).hexdigest() # constant-time compare — never use == return hmac.compare_digest(expected, header or "") # in your handler: hash the *raw* body, not a re-serialized dict # if not verify(request.get_data(), request.headers.get("X-Patch-Signature")): # abort(401) ``` ## Error-spike alerts Patch watches the error rate of each app's **active production version** from the device events it ingests, and fires `error_spike` when something goes wrong in the field — without you polling a dashboard. **Error-spike threshold** The alert fires when the error rate exceeds **10%** over a rolling **15-minute** window, with a minimum sample of **10** events (activations + errors) so a single early error can't trip a false 100% rate. It's debounced per app + version: once fired, the same version won't alert again for 15 minutes, so a burst sends at most one notification. **Deliveries are best-effort** Webhook delivery is fire-and-forget with a short timeout — a slow or failing receiver is logged and skipped, never retried, and never blocks the action (a release or rollback still succeeds even if your endpoint is down). Own your endpoint's availability: keep it fast, return `2xx` quickly, and treat events as best-effort rather than guaranteed-once. --- # Troubleshooting URL: https://docs.patchrelease.com/troubleshooting/ Section: Help Description: Fixes for the ways a Patch release goes wrong: fingerprint mismatch, a function that stayed native, a missing WASM toolchain, and recovering from a bad patch. Almost every Patch failure is one of five: the fingerprint gate refused the push, a function you expected to ship stayed native, the WebAssembly toolchain is missing, a device is not picking the update up, or a patch misbehaved in production. Each one has a section below with the command that diagnoses it. Product questions — Apple's rules, pricing, what can be patched — are answered on the [FAQ](/faq/). ## `push`/`release` says "FINGERPRINT MISMATCH" — what now? Your native shell changed since the last App Store release (a native `.swift` file, a bridge toggle, Info.plist, entitlements, a linked framework, the deployment target, or the compiler version), so the OTA module isn't compatible with installed apps. Run `patchcli fingerprint diff` to see exactly what changed. Ship the change through the App Store, then re-baseline with `patchcli fingerprint register` after the new build is live. Pure-logic patches never trip this — only native-shell changes do. The full diagnosis walkthrough is on [The compatibility fingerprint](/fingerprint/). ## My app stopped compiling after `patchcli init` or `prepare` Run `patchcli prepare --verify`. It builds the prepared project and keeps native any view whose generated code breaks the build, restoring that view's source, then rebuilds until the build is clean. Those views are recorded under `native_views:` in `.Patch.yml`, so later runs remember them. An error that is still there once Patch's changes are removed is reported as your project's own, not Patch's. `--verify` builds Debug and then the configuration your scheme archives with (usually Release), because an optimized build can fail where Debug doesn't; `--verify-config debug` builds only Debug and roughly halves the time. If most views fail in Patch-generated code, or the compiler crashes on it, that is a Patch bug: nothing is kept native, the command exits non-zero naming your patchcli and Xcode versions, and `patchcli unprepare` restores your project. `init` runs this step by default (Debug only; add `--verify-release`). Add `--report patch-compatibility.md` to get the per-view table, and please send it with a bug report. To remove everything `prepare` generated, run `patchcli unprepare` (add `--remove-sdk` to also remove the package and startup code). ## My function stayed native — why isn't it updatable? It touches a must-stay-native API (low-level platform rendering, the file system, threads, the ObjC runtime, unsafe pointers, or device/OS APIs) somewhere it depends on, or it relies on a bridge you disabled in `.Patch.yml`. Logic and `async`/`await` code *is* updatable, and SwiftUI view code is updatable when SwiftUI coverage is enabled — run `patchcli build --verbose` (or `patchcli analyze ./Sources --verbose`) to see the per-function classification and the reason each eligible function wasn't emitted. Factoring the pure logic out of a UI/IO function usually moves it into the WASM bucket. ## "WASM toolchain NOT FOUND" during build You need the **swift.org** toolchain plus the WebAssembly Swift SDK — the Apple/Xcode toolchain cannot target `wasm32-unknown-wasi`. Install with `swiftly install 6.3.2` and `swift sdk install https://download.swift.org/swift-6.3.2-release/wasm-sdk/swift-6.3.2-RELEASE/swift-6.3.2-RELEASE_wasm.artifactbundle.tar.gz --checksum a61f0584c93283589f8b2f42db05c1f9a182b506c2957271402992655591dd7c`. Without them, `patchcli build` still generates sources and a coverage report (effectively a dry run) but emits no `.wasm`. `patchcli setup` installs both for you. ## The device isn't picking up my update Check three things: (1) the app is on the same **channel** you shipped to; (2) the device is inside the current **rollout %** — raise it if you're staging; (3) the app's **fingerprint** matches the release. Devices resolve the active release through the update-check API, so changes apply on the next poll. With `start()` the update applies on the next launch/check; with the imperative API you must call `fetchUpdate()` then `reloadAsync()` yourself. ## A patch caused errors in production — how do I recover? Run `patchcli rollback --channel ` to re-activate the previous module; it propagates within ~60s, and a device already running the withdrawn release is told to deactivate and fall back. On-device, the SDK already recovers automatically — if a module fails to verify or activate, it falls back to the previous good module (or the bundled native code) so the app keeps working. Watch the failure rate with `patchcli status`; it warns past 2%. ## My build fails only in CI, not on my machine The usual cause is `PATH` order. Building the CLI for a Mac uses the Apple toolchain; compiling patches uses the swift.org one, and if swiftly is first on the `PATH` a host build picks the wrong compiler. Put `/usr/bin` first for host builds and `~/.swiftly/bin` first for WebAssembly builds, or let `patchcli setup` and `patchcli doctor` handle it. A toolchain difference between the machine that registered the fingerprint and the machine that releases also produces a mismatch — register and release from the same environment. ## How do I file a useful bug report? Two commands make a report actionable without anyone needing your source. `patchcli doctor --json` reports the versions, the toolchain and the project setup, with a fix hint per gap. `patchcli fingerprint diff --explain` lists which functions are native and why, and what moved since the registered fingerprint. Attach both to an issue on [patch-release/patch-swift](https://github.com/patch-release/patch-swift/issues). ## Related - [FAQ](/faq/) — Apple's rules, coverage, pricing, rollback, self-hosting - [The compatibility fingerprint](/fingerprint/) — the full mismatch walkthrough - [What Patch can & can't update](/coverage/) — why a view stayed native - [Glossary](/glossary/) — fingerprint, demote-to-native, host bridge, PMOD - [The iOS hotfix playbook](https://patchrelease.com/blog/ios-hotfix-playbook) ## Ready to ship your first patch? Install the CLI, run `patchcli init`, and `patchcli release` a fix this afternoon. [Start the Quick Start →](/quickstart/)