Quick Start
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)
Section titled “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.
# 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
Section titled “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):
- Detects the Xcode project, build target, bundle id, and app icon.
- 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 writesapp_key/publish_token/app_id/workspace_idinto.Patch.yml— no copying credentials. - 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. - Proposes a diff inserting the Patch startup code into your
@mainApp struct — applied only after you confirm (y/n). - Makes your SwiftUI views patchable — routes each view
bodythrough 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 noPatchViewwrapping and no changes to your views. This runs automatically on everypatchcli release/push/build, so views you add later are picked up with no extra step. (You can still runpatchcli prepare --checkin CI to verify, or--no-prepareto opt out.)initthen builds the prepared project (prepare --verify): any view whose prepared code doesn’t compile is kept native automatically, andinitends with a compatibility summary naming each native view and why.initbuilds Debug; add--verify-releaseto 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 inPatch/Generated/, not in your view files — see where prepare puts generated code.patchcli unprepareremoves all of it.
$ patchcli initPatch init==========Detected Xcode project: MyApp.xcodeproj (target MyApp · com.example.myapp)Opening browser to register the app… ✓ app key + publish token receivedWrote: /Users/you/MyApp/.Patch.ymlAdding 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 releaseThe 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:
import PatchSDK
@mainstruct 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() } }}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 for the same steps, and the CLI reference for flags like --manual, --no-open, and --yes.
3 · Write a patchable Swift function
Section titled “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.
import Foundation
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
Section titled “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, so the backend only ever serves modules compatible with installed apps. Re-run it after each App Store release to re-baseline.
$ patchcli fingerprint register✓ Registered native-shell fingerprint 3f2b… for MyApp5 · Release it
Section titled “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.
$ 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: trueRe-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.
Manual setup (the fallback)
Section titled “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):
- Add the SDK package — in Xcode, File → Add Package Dependencies… with
https://github.com/patch-release/patch-swift(productPatchSDK, from1.8.0). See SDK reference for thePackage.swiftform. - Create the app in the dashboard to get its
pak_…app key. - Paste the key — set
app_key(and optionallyapp_id/workspace_id) in.Patch.yml. - Get a publish token — run
patchcli login, or create one in the dashboard and setpublish_tokenin.Patch.yml. Without it,push/releasecannot authenticate. - Add the startup code — the
import PatchSDK+Patch.configure/Patch.shared.start()snippet from step 2 above, in your@mainApp struct’sinit().
Related
Section titled “Related”- FAQ — Apple’s rules, what can be patched, rollback, pricing
- Set up with AI — hand the same flow to a coding assistant
- The compatibility fingerprint — the one gate to understand first
- Troubleshooting — when a step does not go to plan
- Update an iOS app without App Store review