Quick Start
From zero to a live OTA patch. Machine setup is two commands you run once. Per-app setup is one command — patchcli init registers the app, adds the SDK, and wires the startup code for you. Then you write normal Swift and release.
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 — marks each view
bodypatchable and generates the dynamic-replacement thunks (the same mechanism Xcode Previews uses), 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.)
$ 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.0.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().