# CI/CD Integration In CI, authenticate with a scoped CI token instead of user auth. Create tokens from the dashboard or via `swiftpatch ci-tokens create`. ## Creating a CI token ```bash # Scoped to one app + one channel swiftpatch ci-tokens create \ --name "GitHub Actions · production" \ --apps my-app \ --channels production ``` The token is printed once. Store it as an encrypted CI secret. You cannot retrieve it later — regenerate with `swiftpatch ci-tokens regenerate ` if lost. ## Signing keys in CI Your CI pipeline needs access to your Ed25519 private key (`.swiftpatch/keys/private.pem`). Store it as an encrypted secret: - **GitHub Actions**: `secrets.SWIFTPATCH_PRIVATE_KEY` (the full PEM contents). - **CircleCI**: environment variable. - **Bitrise**: secret env var. Write it to a file in the job, then pass with `-k`: ```yaml - run: echo "$SWIFTPATCH_PRIVATE_KEY" > ./private.pem && chmod 600 ./private.pem - run: npx swiftpatch-cli deploy -p ios --hermes -k ./private.pem ``` ## GitHub Actions ### Deploy on tag push ```yaml title=".github/workflows/ota.yml" name: OTA Deploy on: push: tags: ['v*'] jobs: deploy: runs-on: macos-latest strategy: matrix: platform: [ios, android] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 20 } - run: npm ci - name: Write signing key env: SWIFTPATCH_PRIVATE_KEY: ${{ secrets.SWIFTPATCH_PRIVATE_KEY }} run: | mkdir -p .swiftpatch/keys echo "$SWIFTPATCH_PRIVATE_KEY" > .swiftpatch/keys/private.pem chmod 600 .swiftpatch/keys/private.pem - name: Deploy OTA (${{ matrix.platform }}) env: SWIFTPATCH_CI_TOKEN: ${{ secrets.SWIFTPATCH_CI_TOKEN }} run: | npx swiftpatch-cli deploy \ -p ${{ matrix.platform }} \ --hermes \ --rollout 5 \ --release-note "${{ github.event.head_commit.message }}" ``` ### Staged rollout over a week ```yaml title=".github/workflows/rollout.yml" name: Gradual rollout on: workflow_dispatch: inputs: release_id: required: true percent: required: true jobs: rollout: runs-on: ubuntu-latest steps: - run: | npx swiftpatch-cli releases rollout \ ${{ inputs.release_id }} \ --percent ${{ inputs.percent }} env: SWIFTPATCH_CI_TOKEN: ${{ secrets.SWIFTPATCH_CI_TOKEN }} ``` ## CircleCI ```yaml title=".circleci/config.yml" version: 2.1 jobs: deploy-ota: docker: - image: cimg/node:20.10 parameters: platform: { type: enum, enum: [ios, android] } steps: - checkout - run: npm ci - run: | mkdir -p .swiftpatch/keys echo "$SWIFTPATCH_PRIVATE_KEY" > .swiftpatch/keys/private.pem chmod 600 .swiftpatch/keys/private.pem - run: | npx swiftpatch-cli deploy \ -p << parameters.platform >> \ --hermes \ --rollout 10 workflows: ship: jobs: - deploy-ota: { platform: ios } - deploy-ota: { platform: android } ``` ## Bitrise Add a **Script** step: ```bash export SWIFTPATCH_CI_TOKEN="$SWIFTPATCH_CI_TOKEN" mkdir -p .swiftpatch/keys echo "$SWIFTPATCH_PRIVATE_KEY" > .swiftpatch/keys/private.pem chmod 600 .swiftpatch/keys/private.pem npx swiftpatch-cli deploy -p ios --hermes --rollout 10 ``` ## GitLab CI ```yaml title=".gitlab-ci.yml" ota:deploy: image: node:20 stage: deploy only: [tags] script: - npm ci - mkdir -p .swiftpatch/keys - echo "$SWIFTPATCH_PRIVATE_KEY" > .swiftpatch/keys/private.pem - chmod 600 .swiftpatch/keys/private.pem - npx swiftpatch-cli deploy -p ios --hermes --rollout 10 variables: SWIFTPATCH_CI_TOKEN: $SWIFTPATCH_CI_TOKEN ``` ## JSON output for pipelines Every command supports `--json`. Pipe it into downstream steps: ```bash RELEASE=$(npx swiftpatch-cli deploy -p ios --hermes --json | jq -r '.release.id') echo "Released $RELEASE" npx swiftpatch-cli status "$RELEASE" --json --timeout 300 ``` ## Exit codes ``` 0 Success 1 Generic failure 2 Validation error (bad flag, missing arg) 3 Authentication failure (token invalid, expired) 4 Network / API error 5 Signing error (missing key, bad PEM, verification failed) 6 Build / bundle error (Metro crashed, Hermes failed) ``` Use these to fail the pipeline precisely. For example, retry on `4` (network blips) but fail fast on `3` (auth) or `5` (signing). ## Multi-tenant CI (white-label apps) Deploy to multiple tenant deployment keys from the same pipeline: ```yaml strategy: matrix: tenant: [acme, contoso, fabrikam] steps: - name: Deploy ${{ matrix.tenant }} env: SWIFTPATCH_CI_TOKEN: ${{ secrets[format('TOKEN_{0}', matrix.tenant)] }} run: npx swiftpatch-cli deploy -a ${{ matrix.tenant }}-app -p ios --hermes ``` See [White-label](/sdk/white-label) for the runtime side. ## Sourcemap uploads For clean stack traces in crash clusters: ```bash npx swiftpatch-cli deploy -p ios --hermes --sourcemap ``` The sourcemap is uploaded alongside the bundle and consumed by the crash clustering pipeline. ## Best practices - **One token per pipeline.** Scope tokens tightly. If a pipeline is only for staging, scope the token to `staging`. - **Rotate tokens every 90 days.** `swiftpatch ci-tokens regenerate ` — update the secret in your CI provider. - **Start rollouts at 5%.** Watch the dashboard for a few hours before promoting to 100%. - **Use `--paused` for scheduled releases.** Ship a release in paused state, then promote at the scheduled time via a separate job. - **Always pass `--release-note`.** The release page needs context; your future self will thank you. - **Fail on non-zero exit.** Don't swallow errors. If `swiftpatch deploy` fails, the pipeline should fail. ## Next steps - [Commands](/cli/commands) — every flag. - [Staged rollouts](/guides/staged-rollouts) — graduated rollout patterns. - [White-label](/sdk/white-label) — multi-tenant runtime. --- # Commands Full reference for every Swiftpatch CLI command. Every command supports `--help` (or `-h`) for inline docs, and most support `--json` for machine-readable output. ## Project setup ### `swiftpatch init` Initialize Swiftpatch in a React Native project. Patches native files, generates an Ed25519 keypair, creates (or links) an app in the dashboard, writes `.swiftpatchrc`, and updates `.gitignore`. ```bash swiftpatch init [options] ``` | Flag | Description | |---|---| | `-y, --yes` | Accept defaults (non-interactive). | | `--manual` | Skip auto-patching of native files. Print the exact snippets to paste, still generate keys and config. | Idempotent — re-running is a no-op on already-patched files. ### `swiftpatch unlink` Reverse `swiftpatch init` cleanly. Removes the `// swiftpatch-begin` / `// swiftpatch-end` blocks from AppDelegate / MainApplication / Info.plist / strings.xml. Leaves your signing keys and `.swiftpatchrc` untouched. ```bash swiftpatch unlink ``` ### `swiftpatch doctor` Diagnose setup issues. ```bash swiftpatch doctor [options] ``` | Flag | Description | |---|---| | `--json` | JSON output (non-zero exit on any failure). | | `--ai` | Deep analysis via Claude. Requires a Claude API key. | See [AI doctor](/guides/ai-doctor) for the full story. ## Authentication ### `swiftpatch login` Authenticate via browser OAuth. ```bash swiftpatch login ``` ### `swiftpatch logout` Clear stored credentials. ### `swiftpatch whoami` Print the current authenticated user and org. ## Deploying ### `swiftpatch deploy` Bundle, sign, upload, and release in one step. The main command you'll use. ```bash swiftpatch deploy -p [options] ``` | Flag | Description | Default | |---|---|---| | `-p, --platform ` | `ios` or `android`. Required. | Auto-detect. | | `-a, --app ` | App to deploy to. | `.swiftpatchrc` `app` field. | | `-o, --org ` | Organization ID. | Default org. | | `--app-version ` | Target app version. | `package.json` `version`. | | `--channel ` | Channel to ship to. | `production`. | | `--rollout ` | Initial rollout percent (0–100). | Channel default. | | `-n, --release-note ` | Release notes. | Empty. | | `-m, --mandatory` | Mark as mandatory. | false. | | `--paused` | Ship paused (invisible until rollout set). | false. | | `--hermes` | Compile to Hermes bytecode. | false. | | `--hermesc-path ` | Custom hermesc binary. | Auto-resolve. | | `-e, --entry-file ` | Metro entry file. | `index.js`. | | `--dev` | Create a dev bundle (non-production). | false. | | `--sourcemap` | Upload source maps. | false. | | `-k, --private-key ` | Ed25519 private key. | `.swiftpatchrc` `signingKey`. | | `--allow-unsigned` | Ship without a signature (dev only). | false. | | `--reproducible [true/false]` | Strip absolute paths so the same commit hashes identically on any machine. | `true`. | | `--ci-token ` | CI auth token. | `$SWIFTPATCH_CI_TOKEN`. | | `-y, --yes` | Skip confirmation prompts. | false. | | `--json` | JSON output. | false. | Example: ```bash swiftpatch deploy -p ios --hermes --rollout 5 --release-note "v1.2.3 hotfix" ``` ### `swiftpatch release-bundle` Two-step CI flow. Use when you bundle and upload in one job, then promote in another. ```bash swiftpatch release-bundle --hash [options] ``` ### `swiftpatch status ` Live SSE stream of rollout progress. ```bash swiftpatch status r_abc123 ``` | Flag | Description | |---|---| | `--json` | One compact JSON object per line (one per SSE event). | | `--watch [duration]` | Stream for a given duration (e.g. `30s`, `5m`). | | `--timeout ` | Disconnect after N seconds of silence. | ### `swiftpatch migrate from codepush` / `from expo-updates` Generate a migration plan for your project. ```bash swiftpatch migrate from codepush swiftpatch migrate from expo-updates ``` The plan includes file-by-file diffs, dependency swaps, and a checklist. See [Migrating from CodePush](/guides/migrating-from-codepush). ## Releases ### `swiftpatch releases list` ```bash swiftpatch releases list -a [--channel ] [--limit ] [--json] ``` ### `swiftpatch releases rollout` Update a live rollout percent. ```bash swiftpatch releases rollout r_abc --percent 50 ``` ### `swiftpatch releases rollback` Roll back a release for all devices on the next check. ```bash swiftpatch releases rollback r_abc ``` ## Channels ### `swiftpatch channels list` ```bash swiftpatch channels list -a [--json] ``` ### `swiftpatch channels create` ```bash swiftpatch channels create -a ``` ### `swiftpatch channels delete` ```bash swiftpatch channels delete -a ``` ## Apps ### `swiftpatch apps list` / `create` / `info` / `update` / `delete` ```bash swiftpatch apps list swiftpatch apps create swiftpatch apps info swiftpatch apps update swiftpatch apps delete ``` ## Signing ### `swiftpatch generate-key-pair` Generate an Ed25519 keypair for release signing. ```bash swiftpatch generate-key-pair [options] ``` | Flag | Description | Default | |---|---|---| | `-o, --output ` | Where to write the keys. | `./.swiftpatch/keys`. | | `-f, --force` | Overwrite existing keys (irreversible). | false. | | `--json` | JSON output. | false. | See [Bundle signing](/concepts/bundle-signing) and [Key rotation](/guides/key-rotation). ## CI tokens ### `swiftpatch ci-tokens create` / `list` / `delete` / `regenerate` ```bash swiftpatch ci-tokens create --name "GitHub Actions" swiftpatch ci-tokens list swiftpatch ci-tokens delete swiftpatch ci-tokens regenerate ``` Scope tokens with `--apps` and `--channels`: ```bash swiftpatch ci-tokens create --apps my-ios-app,my-android-app --channels staging,production ``` ## AI ### `swiftpatch pr ` Open a draft GitHub PR from a crash cluster (F8). See [F8 setup](/guides/github-app-setup). ```bash swiftpatch pr crash_abc123 [options] ``` | Flag | Description | |---|---| | `--title ` | Override the PR title. | | `--body <body>` | Override the PR body. | | `--dry-run` | Print the plan without opening a PR. | | `--local` | Use the local `git apply` flow instead of the GitHub App. | | `--branch <name>` | Override the branch name. | | `--base <branch>` | Override the base branch. | | `--json` | JSON output. | ### `swiftpatch ai doctor` / `explain` / `review` Local AI commands. See [AI doctor](/guides/ai-doctor). ### `swiftpatch ai ask` / `risk-score` / `crashes` / `insights` Backend AI commands. Require Swiftpatch auth (no Claude API key needed). ```bash swiftpatch ai ask "Why did r_abc crash?" swiftpatch ai risk-score r_abc swiftpatch ai crashes -a my-app swiftpatch ai insights -a my-app ``` ## Configuration ### `swiftpatch config` CLI-level config. Stored encrypted at `~/.swiftpatch/config`. ```bash swiftpatch config set <key> <value> swiftpatch config get <key> swiftpatch config delete <key> swiftpatch config list ``` | Key | Purpose | |---|---| | `apiUrl` | Override the API endpoint. | | `defaultOrg` | Default organization. | | `defaultApp` | Default app. | | `defaultPlatform` | Default platform for deploy. | | `defaultChannel` | Default channel. | | `claudeApiKey` | Claude API key for local AI commands. | ## Exit codes Every command exits with a stable code: | Code | Meaning | |---|---| | `0` | Success. | | `1` | Generic failure. | | `2` | Validation error (bad flag, missing arg). | | `3` | Authentication failure. | | `4` | Network / API error. | | `5` | Signing error. | | `6` | Build / bundle error. | `--json` always includes `{ "ok": boolean, "code": string, ... }`. ## Next steps - [CI/CD](/cli/ci-cd) — GitHub Actions, CircleCI, Bitrise. - [Overview](/cli/overview) — tour of the most-used commands. - [Staged rollouts](/guides/staged-rollouts) — automate multi-step rollouts. --- # CLI Installation <!-- source: docs/cli/installation.md --> The Swiftpatch CLI ships as `swiftpatch-cli` on npm. Install globally for convenience, or invoke via `npx` to avoid a global install. ## Requirements - Node.js 20 or higher. - npm, yarn, or pnpm. - For OTA deploys: a bundler-capable environment (Metro ships with every React Native project). - For iOS deploys: CocoaPods, Xcode command-line tools. - For Android deploys: Java 17, Android SDK (only needed for local `react-native bundle`). ## Install globally ```bash npm2yarn npm install -g swiftpatch-cli ``` Verify: ```bash swiftpatch --version # swiftpatch-cli 1.x.x ``` ## Use via `npx` No install required: ```bash npx swiftpatch-cli deploy -p ios --hermes ``` This is the recommended approach for CI. `npx` caches the CLI after the first run. ## Log in ```bash swiftpatch login ``` Opens your browser for OAuth. After consent, a token is stored at `~/.swiftpatch/config`. The token is encrypted at rest. Verify: ```bash swiftpatch whoami # Signed in as prince@example.com (org: swiftpatch) ``` ## Upgrade ```bash npm2yarn npm install -g swiftpatch-cli@latest ``` Release notes live at [github.com/swiftpatch/swiftpatch-cli/releases](https://github.com/swiftpatch/swiftpatch-cli/releases). ## Uninstall ```bash npm2yarn npm uninstall -g swiftpatch-cli ``` Your local `~/.swiftpatch/config` persists after uninstall. To wipe it: ```bash rm -rf ~/.swiftpatch ``` ## Next steps - [Overview](/cli/overview) — the commands you'll use most. - [Commands](/cli/commands) — full reference. - [CI/CD](/cli/ci-cd) — GitHub Actions, CircleCI, Bitrise. --- # CLI Overview <!-- source: docs/cli/overview.md --> `swiftpatch-cli` is how you ship OTA updates. Bundle your JavaScript, sign it with Ed25519, upload to the CDN, and create a live release — all in one command. ## Install ```bash npm install -g swiftpatch-cli ``` Requires Node.js 20 or higher. Or run without installing globally via `npx swiftpatch-cli <command>`. ## The three-command workflow ```bash # 1. Log in (opens a browser) swiftpatch login # 2. Link your project (first time only) npx swiftpatch init # 3. Ship swiftpatch deploy -p ios --hermes ``` For the full story of what `swiftpatch init` does, see [SDK installation](/sdk/installation). ## Core commands | Command | Description | |---|---| | `swiftpatch init` | Set up Swiftpatch in your project. Patches native files, generates keys, creates the app. | | `swiftpatch unlink` | Reverse `swiftpatch init` cleanly. | | `swiftpatch deploy` | Bundle, sign, upload, and release in one step. | | `swiftpatch login` | Authenticate via browser or API key. | | `swiftpatch logout` | Clear stored credentials. | | `swiftpatch whoami` | Show the current authenticated user. | | `swiftpatch doctor` | Diagnose setup issues. Add `--ai` for deep analysis. | | `swiftpatch status <releaseId>` | Live SSE stream of rollout progress. | ## Releases | Command | Description | |---|---| | `swiftpatch releases list -a <app>` | List releases for an app. | | `swiftpatch releases rollout <id> --percent 50` | Update a rollout percent. | | `swiftpatch releases rollback <id>` | Roll back a release across all devices. | | `swiftpatch release-bundle --hash <hash>` | Promote a pre-built bundle (CI two-step flow). | ## Channels | Command | Description | |---|---| | `swiftpatch channels list -a <app>` | List channels for an app. | | `swiftpatch channels create <name> -a <app>` | Create a channel. | | `swiftpatch channels delete <id> -a <app>` | Delete a channel. | ## Signing | Command | Description | |---|---| | `swiftpatch generate-key-pair` | Generate an Ed25519 keypair. | ## AI | Command | Description | |---|---| | `swiftpatch pr <clusterId>` | Open a draft GitHub PR for a crash cluster (F8). | | `swiftpatch ai doctor` | Deep project analysis via Claude. | | `swiftpatch ai explain` | Generate a changelog from git log. | | `swiftpatch ai review` | Safety-review your JS/TS changes. | | `swiftpatch ai crashes` | List AI-clustered crash reports. | | `swiftpatch ai risk-score <releaseId>` | Get the AI risk assessment for a release. | | `swiftpatch ai insights` | View AI-generated insights. | ## CI tokens | Command | Description | |---|---| | `swiftpatch ci-tokens create` | Generate a CI token. | | `swiftpatch ci-tokens list` | List CI tokens. | | `swiftpatch ci-tokens delete <id>` | Revoke a CI token. | | `swiftpatch ci-tokens regenerate <id>` | Regenerate a CI token. | ## Authentication ### Local dev ```bash swiftpatch login ``` Opens a browser, completes OAuth, stores an encrypted token at `~/.swiftpatch/config`. Subsequent commands authenticate automatically. ### CI Use a scoped CI token: ```bash export SWIFTPATCH_CI_TOKEN=sp_ci_xxxxx swiftpatch deploy -p ios --hermes ``` Or pass with `--ci-token`: ```bash swiftpatch deploy -p ios --hermes --ci-token sp_ci_xxxxx ``` Create tokens from `swiftpatch ci-tokens create` or the dashboard. Scope them to specific apps and channels. ## Output modes Every command supports `--json` for machine-readable output: ```bash swiftpatch deploy -p ios --hermes --json ``` ```json { "ok": true, "release": { "id": "r_abc123", "version": "1.2.3", "bundleHash": "sha256:...", "rolloutPercent": 100 } } ``` Exit codes follow a stable taxonomy — see [CI/CD](/cli/ci-cd). ## Environment variables | Variable | Purpose | |---|---| | `SWIFTPATCH_CI_TOKEN` | CI authentication token. | | `SWIFTPATCH_API_URL` | Override the API endpoint (self-hosted, staging). | | `SWIFTPATCH_CLAUDE_API_KEY` | Claude API key for AI commands. | | `DEBUG` | Enable verbose logs. | ## Next steps - [Installation](/cli/installation) — install the CLI. - [Commands](/cli/commands) — full flag reference for every command. - [CI/CD](/cli/ci-cd) — GitHub Actions, CircleCI, Bitrise examples. --- # Brick protection <!-- source: docs/concepts/brick-protection.md --> A bricked app — one that launches into a white screen or crash loop — is the worst outcome of any OTA system. Swiftpatch is architected so this outcome is **unreachable by design**, not merely discouraged. The guarantee: **the store-bundled JavaScript always runs as a floor.** Even if every Swiftpatch release on the device is corrupted, your users see a working app. ## The three-slot layout Every device running the Swiftpatch SDK has three bundle slots on disk: ``` ┌─────────────────────────────────────────────────────────────┐ │ Binary slot (immutable) │ │ • JS bundle embedded in the .ipa / .apk │ │ • Installed by the App Store / Play Store │ │ • Always present, never modified, never deleted │ │ • THE FLOOR — the app can always fall back here │ ├─────────────────────────────────────────────────────────────┤ │ Stable slot │ │ • The last OTA release that survived crash detection │ │ • Promoted from NEW after N successful cold starts │ ├─────────────────────────────────────────────────────────────┤ │ New slot │ │ • The most recent downloaded OTA release │ │ • Staging area — becomes Stable after surviving │ └─────────────────────────────────────────────────────────────┘ ``` The binary slot is the **floor**. On Android, `super.getJSBundleFile()` returns the bundle embedded in the APK. On iOS, `Bundle.main.url(forResource: "main", withExtension: "jsbundle")` returns the bundle embedded in the IPA. Swiftpatch's override calls these as a last resort — if every OTA slot fails, the app boots on the factory bundle. ## The boot counter Layer 1 of the safety net. On every cold start, the SDK increments a **boot counter** stored in native preferences **before** JS begins executing: ``` Cold start begins ↓ bootCounter++ (native, synchronous) ↓ if bootCounter > maxCrashesBeforeRollback (default 2): fall back to previous slot → reset counter → emit telemetry ↓ Load active slot ↓ JS starts ``` If the active OTA slot crashes hard enough that the process dies before JS can signal "I'm alive," the counter increments on the next launch. Three crashes in a row (default) and the SDK drops to the previous stable slot. If that also crashes, it falls to the binary slot. ## The mount marker Layer 2. Reaching `JS start` is not enough — a bundle can crash 2 seconds into execution (a reducer throws, a dependency is missing, a native module's API changed). So the SDK also arms a **mount watchdog**: 1. On load, the native side records that the app attempted to mount. 2. JS executes. Your app renders. 3. Once the JS has been alive for `crashDetectionWindowMs` (default 10s) without crashing, the SDK calls `markMounted()`, which clears the mount marker and boot counter. 4. If JS crashes before `markMounted()` fires, the marker stays set. 5. On the next cold start, native sees the marker still set → the active slot is bad → revert to the previous slot → reset counter. ```tsx // SwiftPatchProvider calls markMounted() automatically after initial render. <SwiftPatchProvider config={{ debug: __DEV__ }}> <App /> </SwiftPatchProvider> ``` If you're using the lower-level imperative API, call `NativeSwiftPatch.markMounted()` yourself after first successful render. ## The async-signal-safe crash handler Layer 3. The mount marker catches crashes that happen before the watchdog fires. But what about a crash at 9.999 seconds? The SDK installs a native crash handler: - **iOS** — a C shim attached to `SIGSEGV`, `SIGABRT`, `SIGBUS`, `SIGILL`, `SIGFPE`. Async-signal-safe — uses `write(2)` with a pre-opened file descriptor, not `fprintf`. - **Android** — a JNI breakpad-style signal handler installed during native module load. On crash, the handler writes a minimal crash marker to disk before the process dies. The next cold start sees the marker, attributes the crash to the active slot, and rolls back. ## Native-version pinning Layer 4. If you ship a JS bundle that requires a newer native binary than the device has, the JS will crash at the first `require()` of the missing native module. Swiftpatch handles this with **native-version pinning**: - Every release in the dashboard has a `minNativeVersion` field. - The SDK reports its native version on every `checkForUpdate` call. - The server never serves a release whose `minNativeVersion` exceeds the device's native version. Result: a JS bundle that needs native changes is invisible to older binaries. Users on the old binary keep running the last compatible OTA until they update from the store. ## Full lifecycle — bad bundle recovery ``` Cold-start with pending bundle r_xyz │ ▼ Native: bootCounter++, load pending → active, arm watchdog (10s) │ ▼ JS runs │ ├── Alive for 10s → markMounted() → counter cleared, marker cleared │ Bundle r_xyz is now VERIFIED. Normal operation. │ └── Crash before 10s │ ▼ Next cold start: ├── Native: mount marker still set → r_xyz is BAD ├── Revert active slot to previous stable slot (or binary) ├── Emit ROLLBACK_PROD telemetry event ├── Server: if ≥N devices report bad on r_xyz in a 15-min window: │ → auto-pause rollout │ → alert release owner (email + dashboard incident) └── User sees the previous working app. Never sees r_xyz again. ``` ## What this guarantees - **Binary slot is always viable.** Your users always have a working app floor. Even if Swiftpatch's servers are down and every cached release is corrupted, the store build runs. - **Bad bundles are locally recovered.** The device reverts itself without waiting for the backend. No server round-trip means no dependency on connectivity during recovery. - **Blast radius is bounded.** The server auto-pauses the rollout the moment multiple devices report a bad bundle. The fraction of users who ever see a bad release is near-zero at any non-trivial rollout percent. - **Signatures protect the pipeline.** A release that does not pass Ed25519 signature verification is never promoted to active in the first place. ## What it does not guarantee Brick protection cannot recover from: - A **signed, non-crashing bundle with a logic bug** — the bundle runs, `markMounted` fires, and the SDK considers it healthy. Your users see the bug. Rollback with `swiftpatch releases rollback`. - A **store-build crash** — if your binary itself is broken, Swiftpatch cannot save you. Ship a new store build. - **Custom `Error.captureStackTrace`-swallowing code** — if your app silently catches every error, the mount marker still clears. Crashes that aren't crashes cannot be auto-detected. ## Configuration ```tsx <SwiftPatchProvider config={{ crashDetectionWindowMs: 10_000, // How long JS must survive for markMounted (default 10s) maxCrashesBeforeRollback: 2, // Cold-start crashes before rolling back (default 2) }} > <App /> </SwiftPatchProvider> ``` Most apps should leave these at defaults. Tune only if you have unusual cold-start patterns. ## Next steps - [Bundle signing](/concepts/bundle-signing) — the Ed25519 signature layer. - [Rollbacks](/concepts/rollbacks) — server-initiated rollbacks. - [Crash detection](/concepts/crash-detection) — how native crash reports fuel cluster analysis. --- # Bundle signing <!-- source: docs/concepts/bundle-signing.md --> Every Swiftpatch release is signed with an Ed25519 keypair. The SDK refuses to apply a bundle whose signature doesn't verify under the configured public key. This is the core security boundary — without it, anyone who could redirect the download URL could ship code to your users. ## Why Ed25519 and not RSA? - **Small signatures.** 64 bytes vs. 256+ for RSA. - **Fast verification.** ~100x faster than RSA verification on mobile ARM. - **No padding pitfalls.** RSA has a long history of signature-scheme footguns. Ed25519 has one correct way to use it. - **Strong defaults.** NIST-equivalent security without the parameter choices. `swiftpatch init` generates an Ed25519 keypair in `.swiftpatch/keys/`. The private key stays on your machine (gitignored). The public key is uploaded to the dashboard and embedded into every OTA bundle request. ## The signing flow ``` Developer machine (or CI): swiftpatch deploy: 1. Bundle JS + assets → bundle.zip 2. Compute SHA-256 of bundle.zip 3. Sign the hash with the Ed25519 private key (~1 ms) 4. Upload bundle.zip + signature to the server 5. Server verifies the signature server-side before even accepting the release Device: SDK downloads bundle.zip 1. Verify signature against the embedded public key 2. If invalid → reject, emit SWIFTPATCH_SIGNATURE_INVALID 3. If valid → verify SHA-256 of bytes 4. Promote to NEW slot ``` Both the server and the device verify the signature. Belt and suspenders. ## Generating keys `swiftpatch init` does this automatically. If you need a new pair: ```bash swiftpatch generate-key-pair ``` Output: ``` Ed25519 keypair created. Public: ./.swiftpatch/keys/public.pem Private: ./.swiftpatch/keys/private.pem (mode 0600) Key ID: ed25519:abc123... Next: upload the PUBLIC key in your app settings. Private key is required by `swiftpatch deploy` to sign releases. Never commit the private key. .gitignore has been updated. ``` The private key is written with `chmod 600` — owner read/write only. ## Installing the public key in the app Two places: 1. **Dashboard** — upload the public PEM. The server uses it to verify releases on upload. 2. **Your app binary** — the SDK embeds the same public key in `Info.plist` / `strings.xml`: ```xml title="ios/YourApp/Info.plist" <key>SwiftPatchPublicKey</key> <string>-----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEA... -----END PUBLIC KEY-----</string> ``` ```xml title="android/app/src/main/res/values/strings.xml" <string name="SwiftPatchPublicKey">-----BEGIN PUBLIC KEY-----\nMCowBQY...\n-----END PUBLIC KEY-----</string> ``` Or pass it at runtime (rarely needed): ```tsx <SwiftPatchProvider config={{ publicKey: MY_PUBLIC_KEY }}> <App /> </SwiftPatchProvider> ``` ## Rotating keys When a private key is compromised, rotate it: 1. In the dashboard: mark the old public key as revoked. 2. Run `swiftpatch generate-key-pair --force` on your machine. 3. Upload the new public key in the dashboard. 4. Ship a new store build with the new public key embedded. This is the critical step — **only store binaries holding the new public key can verify new releases**. 5. Once most users have adopted the new binary, you can stop shipping releases under the old key. During the transition, the server signs releases with both old and new keys. Old binaries verify with the old key; new binaries verify with the new key. Devices on the old binary still get updates. See the [key rotation guide](/guides/key-rotation) for a step-by-step runbook. ## Unsigned releases (not recommended) `swiftpatch deploy --allow-unsigned` will ship a release without a signature. The SDK will download it, but any app that has `SwiftPatchPublicKey` set will reject it at verification time. Use this only for short-lived dev channels. ## What signatures don't protect Signatures prove a bundle was produced with your private key. They don't prove: - **The code is safe.** A legitimately-signed bundle with a bug still ships the bug. For that, see [staged rollouts](/concepts/rollouts) and the [risk score](/guides/ai-doctor). - **The user is authorized to receive it.** Deployment keys enforce channel membership; signatures are separate. - **The server is uncompromised.** If your dashboard account is taken over, the attacker can sign releases with your key. Use SSO + 2FA on the dashboard. ## Next steps - [Brick protection](/concepts/brick-protection) — what happens if a signed bundle still crashes. - [Key rotation](/guides/key-rotation) — step-by-step key rotation runbook. - [Delta patches](/concepts/delta-patches) — delta patches are signed too. --- # Crash detection <!-- source: docs/concepts/crash-detection.md --> Every crash in a Swiftpatch-managed app is captured, clustered, and — when F8 is enabled — root-caused and turned into a suggested fix by the AI PR agent. ## The three layers ### 1. Native crash handler An async-signal-safe native handler is installed during SDK load: - **iOS** — C shim attached to `SIGSEGV`, `SIGABRT`, `SIGBUS`, `SIGILL`, `SIGFPE`. Uses `write(2)` with a pre-opened file descriptor — the only file-writing primitives guaranteed to be safe inside a signal handler. - **Android** — JNI breakpad-style handler installed when the native module loads. The handler writes a minimal crash marker (release id + timestamp + signal number) to disk in microseconds, before the process dies. It does **not** attempt to collect a full stack trace — doing that from a signal handler is unreliable. ### 2. JS error capture For JS-level errors (uncaught exceptions, unhandled promise rejections), the SDK installs lightweight error boundaries: - `ErrorUtils.setGlobalHandler` — catches uncaught JS exceptions. - `process.on('unhandledRejection', ...)` — for promise rejections in non-React surfaces. These surface via the `error` event on the bus. They count against the crash counter during the crash-detection window. ### 3. Boot counter + mount marker The boot counter trips when the same bundle fails to reach `markMounted()` N times in a row. See [Brick protection](/concepts/brick-protection) for the full algorithm. ## Upload + clustering On the next successful launch after a crash, the SDK uploads: - The crash marker (release id, timestamp, signal). - The JS error payload (message, stack trace if available, component stack from React error boundary). - Device metadata (OS version, model, locale, free disk, free memory). The server clusters crashes by: - Normalizing the stack trace (strip line numbers, minification noise, device-specific paths). - Grouping by the first N normalized frames. - De-duping across devices so a single bug doesn't appear as thousands of unique crashes. Each cluster gets a stable `clusterId`. The dashboard shows: first seen, last seen, unique device count, trend, affected releases. ## AI root-cause analysis For each cluster, Swiftpatch runs an AI analysis (Claude-powered) that produces: - **Root cause summary** — one sentence explaining the bug. - **Suggested fix diff** — a unified diff that, applied to your repo, is likely to resolve the crash. - **Confidence score** — 0–100, based on how well the stack trace maps to identified code paths. - **Affected files + lines** — so you can jump straight to the relevant code. Analysis runs on-demand (when you open the cluster in the dashboard) or on every new cluster for customers on Pro plans. :::info AI features are opt-in F8 and root-cause analysis require an organization-level opt-in. Until enabled, Swiftpatch captures and clusters crashes, but does not run AI analysis or open PRs. See [Data privacy](https://swiftpatch.io/privacy). ::: ## F8 — the crash-to-PR agent When you click "Open PR" on a crash cluster (or run `swiftpatch pr <clusterId>`), the F8 agent: 1. Fetches the suggested fix diff from the server. 2. Creates a branch named `swiftpatch/fix/<shortHash>` in your configured repo. 3. Applies the diff. 4. Commits with a descriptive message (`auto-fix: <rootCause>`). 5. Opens a draft PR via the GitHub App. 6. Sets the PR body to include the cluster id, affected releases, and fix summary. **Guardrails:** - Denylist of files the agent will not touch (lockfiles, CI configs, secret-related paths). - Max 500 lines changed per PR. - Max 10 PRs per repo per day. - Always opens drafts — never auto-merges. - The PR body links back to the cluster for context. See the [F8 PR agent guide](/guides/github-app-setup). ## Telemetry events | Event | Fires when | |---|---| | `CRASH_DETECTED` | Native signal handler fired. | | `SYNC_ERROR` | Upload-related error. | | `ROLLBACK_PROD` | Crash counter tripped, device reverted. | These feed the dashboard's per-release crash-rate metric, which drives the auto-pause-on-crash-rate policy. ## Integrating with existing crash reporters Swiftpatch's crash detection is complementary to Sentry / Bugsnag / Crashlytics. It fires on the same underlying crashes but is focused on OTA-specific intelligence (which release, which slot, what to do about it). Pipe Swiftpatch's lifecycle breadcrumbs into your existing reporter so your crash dashboards show the OTA context: ```ts import * as Sentry from '@sentry/react-native'; import { installSentrySink } from '@swiftpatch/react-native/integrations/sentry'; installSentrySink(Sentry); ``` See [Integrations](/sdk/integrations). ## Next steps - [Brick protection](/concepts/brick-protection) — how the crash handler ties into auto-rollback. - [F8 PR agent](/guides/github-app-setup) — install the GitHub App and open your first AI fix PR. - [Rollbacks](/concepts/rollbacks) — what happens when the crash counter trips. --- # Delta patches <!-- source: docs/concepts/delta-patches.md --> Most React Native bundles change by a few kilobytes between releases. Shipping the entire 5–10 MB bundle every time wastes bandwidth and battery for no reason. Swiftpatch's delta patches fix that with zero configuration — you deploy normally, and the server emits patches against the last five releases automatically. **Typical savings:** 60–98%. A 10 MB bundle with a small code change produces a patch of 100–400 KB. ## How it works ``` ┌──────────────────────────────┐ │ Server: bsdiff generator │ │ │ │ When you ship r_new: │ │ • Compute bsdiff(r_last, r_new) │ │ • Compute bsdiff(r_last-1, r_new) │ │ • ... for last 5 releases │ │ • Store each .bsdiff blob on R2 │ └───────────────┬──────────────┘ │ ▼ ┌──────────────────────────────┐ │ /sdk/check-update response │ │ { │ │ release: r_new, │ │ patches: [ │ │ { fromHash: r_last, │ │ patchUrl, patchSize } │ │ { fromHash: r_last-1, │ │ ... }, │ │ ... │ │ ] │ │ } │ └───────────────┬──────────────┘ │ ▼ ┌──────────────────────────────┐ │ SDK patch picker │ │ │ │ 1. Find the patch whose │ │ fromHash == device's │ │ current bundle hash. │ │ 2. Pick the smallest one. │ │ 3. Download + verify + apply│ │ 4. Fall back to full bundle │ │ on ANY failure. │ └──────────────────────────────┘ ``` ## The pipeline 1. **Check.** Device calls `/sdk/check-update` with its current bundle hash. The server returns the new release + a `patches[]` array with one patch per prior release hash. 2. **Pick.** The SDK selects the patch whose `fromHash` matches the device's current bundle. If no patch matches, it downloads the full bundle. 3. **Verify the patch.** The patch bytes are Ed25519-signed. The SDK verifies the signature before feeding them to `bspatch`. 4. **Apply.** Native `bspatch` (iOS + Android share the same vendored BSD-2-clause C library) reconstructs the new bundle. 5. **Verify the output.** SHA-256 of the reconstructed bundle is checked against the advertised target hash. 6. **Commit.** Atomic slot promotion. If any step fails, the SDK silently falls back to downloading the full bundle. A `PATCH_FALLBACK_TO_FULL` telemetry event fires so you can see degraded patch paths in the dashboard. ## Safety guarantees - **Signed patches.** The patch blob itself is signed — not just the reconstructed output. A tampered patch can't reach `bspatch`. - **Output hash verification.** The patched output is SHA-256-checked against the target hash. If bsdiff was run on the wrong base, the check fails and the SDK falls back. - **Never corrupts the active slot.** Patches are applied in a staging directory and atomically renamed on success. A mid-apply crash leaves the old active slot intact. ## Telemetry Two events fire from the patch pipeline: - `PATCH_APPLIED_PROD` — successful delta apply. Payload includes `bytesSaved` so you can graph bandwidth savings in your dashboard. - `PATCH_FALLBACK_TO_FULL` — patch path failed. Payload includes `failureKind`: - `patch_verify_failed` — signature mismatch. - `bspatch_failed` — corrupt or truncated patch stream. - `output_hash_mismatch` — base drift (the device's current bundle wasn't the declared `fromHash`). - `base_missing` — the current bundle file is absent (rare, only after filesystem corruption). - `patch_download_failed` — network error fetching the patch. All of these are non-fatal. The full bundle ships next. ## Why bsdiff? Two reasons: 1. **Proven.** bsdiff (Colin Percival, 2003) is the basis of FreeBSD's binary updates, macOS updates, and countless other systems. The algorithm is mature and the patches are dense. 2. **Language-agnostic.** The C reference implementation compiles cleanly on both iOS and Android. A single source file ships on both platforms. The SDK uses the BSD-2-clause reference implementation with no modifications beyond a categorized error-code wrapper (`sp_bspatch`) for telemetry. ## Configuration There is no configuration. The server emits patches automatically for every release. The SDK picks the best patch and falls back to full bundles when needed. Your deploy command doesn't change: ```bash swiftpatch deploy -p ios --hermes ``` The CLI uploads the full bundle. The server generates patches against the last 5 releases and caches them on R2. Your job is done. ## Monitoring savings In the dashboard, the Releases page shows per-release delta-patch metrics: - Percent of downloads that used the patch path (vs. full). - Total bytes saved. - Patch ratio histogram (distribution of patch sizes as a percent of full bundle). - Fallback rate (`PATCH_FALLBACK_TO_FULL` events per release). ## Next steps - [How it works](/concepts/how-it-works) — the full update lifecycle. - [Bundle signing](/concepts/bundle-signing) — how Ed25519 signatures protect patches. - [Brick protection](/concepts/brick-protection) — what happens if a patched bundle crashes. --- # Differential updates <!-- source: docs/concepts/differential-updates.md --> This page has moved to [Delta patches](/concepts/delta-patches). The two terms refer to the same thing — the bsdiff-based delta pipeline that ships only the changed bytes of your JS bundle. See the [Delta patches](/concepts/delta-patches) page for the full explainer, safety guarantees, and telemetry. ## Quick summary - **Typical savings:** 60–98% of bundle size. - **Algorithm:** bsdiff (BSD-2-clause reference implementation). - **Signed:** patch bytes are Ed25519-signed before `bspatch` runs. - **Safe fallback:** any failure drops back to the full bundle, silently. A `PATCH_FALLBACK_TO_FULL` telemetry event fires. - **No configuration:** enabled for every deploy automatically. --- # How it works <!-- source: docs/concepts/how-it-works.md --> Swiftpatch is four components: - **CLI** — `swiftpatch-cli`. Bundles JS, signs it, uploads it, creates the release. - **API** — `api.swiftpatch.io`. Accepts releases, generates delta patches, serves check-for-update, ingests telemetry, runs AI analysis. - **CDN + storage** — Cloudflare R2. Stores full bundles and delta patches. - **SDK** — `@swiftpatch/react-native`. Checks for updates, downloads and verifies bundles, applies them safely, reports telemetry. The dashboard at `app.swiftpatch.io` sits alongside these, reading from the same API. ## Architecture diagram ``` ┌────────────────────┐ ┌──────────────────────────┐ │ Dev / CI machine │ │ User's device │ │ │ │ │ │ swiftpatch-cli │ │ @swiftpatch/ │ │ (bundle, sign, │ │ react-native (SDK) │ │ upload) │ │ │ └─────────┬──────────┘ └──────────┬───────────────┘ │ HTTPS │ HTTPS │ │ (check / download) ▼ ▼ ┌──────────────────────────────────────────────────┐ │ api.swiftpatch.io │ │ ┌──────────┐ ┌────────────┐ ┌──────────────┐ │ │ │ Releases │ │ Delta-patch│ │ Crash │ │ │ │ service │ │ generator │ │ clustering + │ │ │ │ │ │ │ │ F8 PR agent │ │ │ └──────────┘ └────────────┘ └──────────────┘ │ └──────────────┬───────────────────┬───────────────┘ │ │ ▼ ▼ ┌─────────────────┐ ┌──────────────┐ │ Cloudflare R2 │ │ Dashboard │ │ (bundles + │ │ app.swift │ │ patches + │ │ patch.io │ │ manifests) │ │ │ └─────────────────┘ └──────────────┘ ``` ## Deploy lifecycle You run `swiftpatch deploy -p ios --hermes`. The CLI: 1. Runs `react-native bundle` for your entry file. 2. Compiles to Hermes bytecode (`--hermes`). 3. Computes SHA-256 of the bundle + each asset. 4. Builds a manifest (`manifest.json`) pinning per-file hashes. 5. Zips everything into `bundle.zip`. 6. Signs the archive hash with your Ed25519 private key. 7. Uploads `bundle.zip` + signature to the API. 8. API pushes to R2, issues a signed CDN URL, stores the release in Postgres. 9. API enqueues a delta-patch generation job — bsdiff against the last 5 releases. 10. Delta patches land in R2 with their own signatures. 11. CLI returns the release id. ## Device lifecycle Every cold start and foreground transition, the SDK: ```mermaid sequenceDiagram participant App as React Native App participant SDK as Swiftpatch SDK participant API as api.swiftpatch.io participant CDN as Cloudflare R2 App->>SDK: Cold start / resume SDK->>SDK: Load current bundle (slot system) SDK->>SDK: markMounted + arm crash watchdog SDK->>API: POST /sdk/check-update<br/>{ currentHash, channel, cohorts } API-->>SDK: { release, patches[] } alt Update available SDK->>SDK: Pick best delta patch (fromHash match) SDK->>CDN: GET patch bytes (or full bundle) CDN-->>SDK: bytes SDK->>SDK: Verify Ed25519 signature SDK->>SDK: Apply bspatch (if patch) SDK->>SDK: Verify output SHA-256 SDK->>SDK: Commit to NEW slot SDK->>App: Emit update-ready App->>SDK: applyNow() (user tap or auto) SDK->>SDK: Restart JS runtime else No update SDK->>App: Continue end ``` ## The slot system Three slots per device. See [Brick protection](/concepts/brick-protection) for the full explainer: - **Binary** — JS embedded in the IPA/APK. Always present. The floor. - **Stable** — last OTA release that survived crash detection. - **New** — most recent downloaded OTA release. A downloaded release lands in `New`. After `autoStabilizeAfterLaunches` crash-free cold starts (default 2), it's promoted to `Stable`. A crash during that window rolls back. ## Safety guarantees Three independent layers that must all fail for a user to ever see a broken app: 1. **Signatures.** Every bundle and patch is Ed25519-signed. Unsigned or tampered bytes never reach disk. 2. **Slot system.** Old stable bundle is retained until the new one is verified. 3. **Boot counter + mount marker.** If the new bundle crashes at launch, the SDK rolls it back within 2–3 cold starts — automatically, without needing a server round-trip. See [Brick protection](/concepts/brick-protection). ## Observability Every phase emits events on the event bus (`update-available`, `download-progress`, `update-ready`, `update-applied`, `rollback`, `error`) and corresponding breadcrumbs for Sentry / Bugsnag / Crashlytics. The backend ingests per-device telemetry (`DOWNLOAD_PROD_STARTED`, `INSTALLED_PROD`, `ROLLBACK_PROD`, `PATCH_APPLIED_PROD`, `PATCH_FALLBACK_TO_FULL`, `CRASH_DETECTED`, etc.). The dashboard graphs adoption, crash rate, and delta-patch savings per release. ## AI features When enabled for your org: - **Risk score** per release, computed from bundle diff heuristics + crash model. - **Crash cluster analysis** — root cause, suggested fix diff, confidence score. - **F8 PR agent** — `swiftpatch pr <clusterId>` opens a draft GitHub PR with the suggested fix. See [Crash detection](/concepts/crash-detection) and [AI doctor](/guides/ai-doctor). ## Next steps - [Delta patches](/concepts/delta-patches) — bandwidth math and safety guarantees. - [Bundle signing](/concepts/bundle-signing) — the Ed25519 security boundary. - [Brick protection](/concepts/brick-protection) — why a bad bundle never bricks your app. - [Rollouts](/concepts/rollouts) — percent-based gradual rollouts. --- # Rollbacks <!-- source: docs/concepts/rollbacks.md --> A rollback reverts a device from a bad release to a known-good one. Swiftpatch rolls back in three ways: 1. **Device-local** — the SDK detects a crash loop and reverts on its own. No server involved. 2. **Server-initiated** — you mark a release rolled back in the dashboard, and every device picks that up on the next check. 3. **User-initiated** — your app calls `rollback()` explicitly. ## 1. Device-local rollback (automatic) The mechanism: the boot counter + mount marker. See [Brick protection](/concepts/brick-protection) for the full details. Triggers: - The freshly applied bundle crashes `maxCrashesBeforeRollback` times (default 2) in a row. - The JS layer calls `Swiftpatch.onError` with a crash-class error during the crash-detection window. - The native signal handler catches a segfault / abort. Result: on the next cold start, the SDK reverts the active slot to the previous stable slot (or the binary slot if no stable is available), fires the `ROLLBACK_PROD` telemetry event, and the user is back to a working app. Typical time to recovery: one cold-start cycle (a few seconds). ## 2. Server-initiated rollback You noticed a bad release in the dashboard and want every device off it, fast: ```bash swiftpatch releases rollback r_abc123 ``` This marks the release rolled back in the server's state. The next `checkForUpdate` from any device currently on that release will receive a `appliedReleaseRolledBack: true` signal and immediately revert locally. Devices that had this release in the `NEW` slot but haven't applied yet will have the slot cleared. Devices already running it will revert on the next check (next cold start or resume). ## 3. User-initiated rollback (from your app) The low-level hook: ```ts import { useSwiftPatch } from '@swiftpatch/react-native'; function SettingsScreen() { const { rollback } = useSwiftPatch(); return ( <Button title="Revert to previous version" onPress={async () => { await rollback(); // App will boot on the previous stable slot on next launch. }} /> ); } ``` `rollback()` reverts the active slot and fires the `rollback` event on the bus. You can drive your own UI off that event. ## Rollback telemetry Every rollback — regardless of trigger — fires a `ROLLBACK_PROD` telemetry event with the following payload: - `releaseId` — the bundle being reverted from. - `reason` — `boot_loop_limit` | `server_rollback` | `signature_invalid` | `user_initiated`. - `deviceId` — for server-side correlation. - `crashTrace` — when the rollback was crash-triggered. The dashboard clusters these events and surfaces a per-release rollback rate. When the rollback rate exceeds the channel's `auto-pause-crash-rate` threshold, the rollout auto-pauses. ## Preventing rollback loops If the bundle you roll back to is also broken, the SDK falls one layer deeper: ``` New slot crashes → revert to Stable slot Stable slot also crashes → revert to Binary slot Binary slot always works (shipped with the store build) ``` The binary slot is the floor. Your app always boots. ## Auto-pause on elevated rollback rate In the dashboard, each channel has an `auto-pause-crash-rate` policy. When the rollback rate for a live rollout exceeds this threshold, Swiftpatch: 1. Pauses the rollout at its current percent (no new devices download the release). 2. Fires an incident in the dashboard. 3. Notifies release owners via email. You can then either: - Rollback the release globally: `swiftpatch releases rollback r_abc`. - Investigate the crash cluster (AI-powered — see [crash detection](/concepts/crash-detection)) and fix forward. ## Next steps - [Brick protection](/concepts/brick-protection) — the device-local mechanism. - [Crash detection](/concepts/crash-detection) — how crashes are clustered and analyzed. - [Rollouts](/concepts/rollouts) — how auto-pause ties into your deployment strategy. --- # Rollouts <!-- source: docs/concepts/rollouts.md --> A rollout is the gradual release of a new version to a percentage of your users. Instead of pushing a bundle to 100% of devices at once, you release to 5%, then 25%, then 50%, then everyone — pausing at each step to verify the new release is healthy. Swiftpatch treats rollouts as a first-class primitive. Every release has a rollout percent; devices are deterministically bucketed so the same device stays in the same bucket across releases. ## Setting the rollout percent From the CLI: ```bash # Deploy to 5% of users swiftpatch deploy -p ios --hermes --rollout 5 # Update an existing release's rollout percent swiftpatch releases rollout r_abc123 --percent 25 ``` From the dashboard, drag the rollout slider per release. ## Deterministic bucketing Each device lands in a deterministic bucket `[0, 100)` based on a hash of `deviceId + deploymentKey`. If the release's rollout is `N%`, devices with bucket `< N` receive the release. Properties of this bucketing: - **Stable across releases.** A device in bucket 7 is in bucket 7 forever. Moving from 5% → 10% adds new devices; it doesn't shuffle existing ones. - **Independent per deployment key.** A device in bucket 7 on `production` may be in bucket 43 on `beta`. - **No cohort memory.** The bucket is computed client-side, so there's no server lookup. Result: if you roll out to 5% and see a spike in crashes, you've affected only the specific 5% of users in buckets `0–4` — the same 5% who saw the previous 5% rollout. If they're a good canary cohort, keep going. If you find yourself always hitting the same users, rotate `deploymentKey` suffixes or use cohorts (next section). ## Cohorts For targeted rollouts, cohorts let you scope a release to a subset of users beyond just percent: ```bash swiftpatch deploy --cohort internal-testers --rollout 100 swiftpatch deploy --cohort us-west --rollout 50 ``` A cohort is a label your app reports at check-time: ```ts import { Swiftpatch } from '@swiftpatch/react-native'; // On app startup or after the user joins internal-testers await Swiftpatch.setCohorts(['internal-testers', 'us-west']); ``` The check-for-update API filters by cohort membership. Users not in a given cohort never see releases targeted at it. ## Auto-pause on crash rate Each channel has an `auto-pause-crash-rate` policy. When the crash rate on a live release exceeds the threshold: 1. Rollout auto-pauses at its current percent. 2. Dashboard incident opens. 3. Release owners receive email + Slack notifications. Default threshold: 0.5% above baseline (the channel's baseline crash rate over the trailing 7 days). Override per channel: ```bash swiftpatch channels update production --auto-pause-crash-rate 0.3 ``` ## Staged rollout recipe The default production rollout cadence: ```bash # Day 0, 09:00 swiftpatch deploy -p ios --hermes --rollout 1 --release-note "v1.2.3" # Day 0, 12:00 — verify dashboard shows no elevated crash rate swiftpatch releases rollout r_new --percent 5 # Day 0, 17:00 swiftpatch releases rollout r_new --percent 25 # Day 1, 09:00 swiftpatch releases rollout r_new --percent 50 # Day 1, 14:00 — full rollout swiftpatch releases rollout r_new --percent 100 ``` See [Staged rollouts guide](/guides/staged-rollouts) for CI automation that does this for you. ## Paused releases Create a release in a paused state: ```bash swiftpatch deploy --paused ``` The release is uploaded but invisible to devices. Devices won't see it until you set a rollout percent. Useful for pre-loading a release on the server that you want to ship at a specific time. ## Live rollout status ```bash swiftpatch status r_abc123 ``` Opens an SSE stream showing: - Current rollout percent. - Number of devices eligible. - Number downloaded. - Number applied. - Number of crashes attributed to this release. - Current release status (`ACTIVE`, `PAUSED`, `ROLLED_BACK`, `COMPLETED`). Add `--json` for one compact JSON object per line (useful in CI logs). ## Mandatory releases A mandatory release bypasses the user's "Later" dismissal. Set via the `-m` / `--mandatory` flag: ```bash swiftpatch deploy -p ios --hermes --mandatory ``` When a mandatory release is ready, the provider renders `<UpdateBlocker />` — a full-screen modal with no dismiss action. Use mandatory for security-critical fixes only. ## Next steps - [Staged rollouts](/guides/staged-rollouts) — CI recipe for percent-based rollouts. - [Mandatory updates](/guides/mandatory-updates) — when to use the mandatory flag. - [Rollbacks](/concepts/rollbacks) — what auto-pause does under the hood. --- # Analytics <!-- source: docs/dashboard/analytics.md --> The Analytics dashboard shows how your OTA updates are performing across all apps. Click **Analytics** in the sidebar to open it. ![Analytics dashboard](/img/screenshots/analytics-overview.png) ## Filtering Use the dropdowns in the top-right corner: - **Platform** -- All Platforms, iOS, or Android - **Time range** -- Last 7 days, Last 30 days, or custom All cards and charts update instantly when you change a filter. ## Summary Cards ### Total Installs How many times devices downloaded and applied an OTA update. A single device updating twice counts as two installs. ### Success Rate Percentage of update attempts that completed without error. A healthy rate is above 98%. :::tip A sudden drop often means a mismatched target binary version. Verify that `targetBinaryVersion` matches what your users are running. ::: ### Bandwidth Saved Data your users avoided downloading because SwiftPatch sends differential patches instead of full bundles. ### Money Saved Estimated savings based on standard CDN egress (data transfer from servers to user devices) pricing. ## Charts ### Installs Over Time Daily install count as a time-series chart. Use it to spot adoption spikes, drops, and correlations with your release schedule. ### Installs by Platform iOS vs. Android split as a donut chart. ### Adoption by Version Which release versions your users are running. Helps you spot adoption stalls. ## Patch Size Distribution Size breakdown of your patches. :::tip If sizes are growing, ship updates more frequently so each differential is smaller. ::: ## Crash Groups The most impactful crashes after OTA updates. SwiftPatch uses AI-powered analysis to cluster related reports by error type and stack trace. ![Crash analytics](/img/screenshots/crash-analytics.png) Filter by app, status (Open, Resolved, All), and severity. :::info SwiftPatch can automatically roll back when it detects a crash spike. Configure thresholds in your [release settings](/dashboard/releases). ::: ## Exporting Data **Step 1:** Set your date range and platform filters. **Step 2:** Click **Export**. **Step 3:** Choose **CSV**. --- # Apps <!-- source: docs/dashboard/apps.md --> An **app** in SwiftPatch represents a React Native application you want to deliver OTA updates to. :::note In older dashboard versions, apps were called "projects." Some screenshots and API fields (such as `projectId`) still use the previous name. ::: ## The Apps Page The Apps page shows a summary of your workspace -- total apps, channels, team members, and active releases -- followed by a searchable list of every app. ![The Apps page](/img/screenshots/projects-list.png) You can search by name, sort by last modified date, and switch between grid and list views. Click **+ New App** to create one. ## App Overview Click an app card to see its details at a glance. ![App overview](/img/screenshots/project-overview.png) | Section | What it shows | |---------|---------------| | **Summary cards** | Channels, packages, releases, and members | | **Live Versions** | Bundles currently active across channels | | **Channels** | Deployment channels with a link to add more | | **App Details** | Members and tokens | ## Creating an App **Step 1:** Click **+ New App**. **Step 2:** Enter a name. **Step 3:** Select platforms -- **iOS**, **Android**, or both. **Step 4:** Click **Create**. :::info A default **production** channel is created automatically. You can add more channels (staging, beta, canary) from the app overview. The app name generates a lowercase slug (e.g., `AwesomeProject` becomes `awesomeproject`) used in CLI commands and API calls. ::: ## Configuring Platforms - **iOS only** -- Expects iOS-compatible bundles - **Android only** -- Bundles target the Android runtime - **Both** -- Separate bundles per platform, released on independent schedules ## Managing Channels Channels are named deployment targets that control which users get which bundle version. ### Default Channel Every app starts with a **production** channel. ### Adding a Channel **Step 1:** Open the app overview. **Step 2:** In the **Channels** panel, click **+ New Channel**. **Step 3:** Name it (e.g., `staging`, `beta`, `canary`). **Step 4:** Click **Create**. ### Common Strategies | Strategy | Channels | Purpose | |----------|----------|---------| | **Simple** | `production` | Ship directly to all users | | **Staged** | `staging` + `production` | Test before promoting | | **Multi-tier** | `canary` + `beta` + `production` | Graduated rollout | :::tip Promote a bundle between channels instead of re-uploading. This guarantees the exact same bundle. ::: ## Deployment Keys and Tokens Each channel has a unique deployment key your app uses to check for updates. Find them under the **Tokens** tab in app details. :::warning Keep deployment keys secure. They grant read-only access but should not be publicly exposed. ::: ## App Settings | Setting | Description | |---------|-------------| | **Name** | Display name | | **Platforms** | Target platforms | | **Deployment Keys** | Per-channel keys used by the SDK | | **Collaborators** | Team members and their roles | | **Bundle Signing** | Enable RSA code signing | ## Deleting an App **Step 1:** Open the app and click **Settings**. **Step 2:** Scroll to **Danger Zone**. **Step 3:** Click **Delete App** and confirm by typing the app name. :::danger This permanently removes all releases, channels, analytics, and configuration. It cannot be undone. ::: --- # Dashboard Overview <!-- source: docs/dashboard/overview.md --> The SwiftPatch dashboard is your command center for managing apps, shipping releases, tracking adoption, and collaborating with your team. Sign in at [app.swiftpatch.io](https://app.swiftpatch.io) to get started. :::note Terminology In the dashboard, your organization is called a **workspace**. ::: ![SwiftPatch dashboard home screen](/img/screenshots/dashboard-home.png) ## Home Screen Your home screen gives you a quick snapshot of your workspace: - **Your Apps** -- Total connected apps - **Total Releases** -- OTA releases shipped across all apps - **Team Members** -- People with workspace access - **Avg Patch Size** -- Average bundle size Below the stats, two panels keep you oriented: | Panel | What it shows | |-------|---------------| | **Your Apps** | Apps with platform badges, environment count, and member count | | **Activity** | Recent events -- deployments, rollbacks, and status changes | ## Sidebar Navigation **Workspace-level pages:** - **Overview** -- Home screen - **Analytics** -- Metrics across all apps **App-level pages** (expand any app): - **Releases** -- All releases for that app - **Channels** -- Channel-specific release history - **Members** -- People with access - **Settings** -- App configuration The sidebar also links to **Workspace Settings**, **Help**, and the **AI Assistant** (Beta). :::tip Press `/` from any page to open global search. ::: ## Apps A searchable, sortable list of every app in your workspace. Switch between grid and list views. ![Apps list page](/img/screenshots/projects-list.png) :::info In older dashboard versions, apps were called "projects." Some screenshot filenames still reflect this. ::: See [Apps](/dashboard/apps) for details. ## App Detail View Click any app to see its channels, live versions, members, and tokens. ![Project overview](/img/screenshots/project-overview.png) :::info Channels vs. Releases A **channel** is a deployment target (like `production` or `staging`). A **release** is a specific bundle shipped to a channel. ::: ## Releases A table of every OTA update across your workspace: | Column | Description | |--------|-------------| | **Release** | Version, channel, platform, and description | | **Status** | `Live` or `Disabled` | | **Rollout** | Percentage of users receiving this release | | **Installs** | Devices that installed the update | | **Success** | Install success rate | ![Releases table](/img/screenshots/releases-list.png) See [Releases](/dashboard/releases) for details. ## Analytics Aggregated data across all apps. Filter by platform and time range. ![Analytics overview](/img/screenshots/analytics-overview.png) Here is what you will find: - **Summary cards** -- Total Installs, Success Rate, Bandwidth Saved, Money Saved - **Installs Over Time** -- Time-series chart - **Installs by Platform** -- iOS vs. Android split - **Adoption by Version** -- User adoption per release - **Patch Size Distribution** -- Bundle size breakdown - **Top Crash Groups** -- Most frequent crashes after updates See [Analytics](/dashboard/analytics) for details. ## Workspace Settings Click **Workspace Settings** in the sidebar. ![Workspace settings](/img/screenshots/settings-general.png) - **Workspace** -- [General](/dashboard/settings), [Team](/dashboard/team), [Keys & Tokens](/dashboard/settings), [Security](/dashboard/settings) - **Billing** -- Plans & Usage, Audit Log - **Connect** -- Integrations ## Dark Mode Toggle dark mode using the sun/moon icon in the top-right corner. ![Dashboard in dark mode](/img/screenshots/dashboard-home-dark.png) ## Next Steps 1. [Create your first app](/dashboard/apps) 2. [Publish a release](/dashboard/releases) 3. [Configure rollouts](/dashboard/rollouts) 4. [Monitor analytics](/dashboard/analytics) 5. [Invite your team](/dashboard/team) --- # Releases <!-- source: docs/dashboard/releases.md --> The Releases page tracks every OTA update shipped across your apps. ## Releases Overview ![Releases table](/img/screenshots/releases-list.png) | Column | Description | |--------|-------------| | **Release** | Version, channel, platform badge, and description | | **Status** | `Live` or `Disabled` | | **Rollout** | Percentage of users receiving this update | | **Installs** | Devices that installed the update | | **Success** | Install success rate | :::tip Click **Ship Release** in the top-right to publish a new update from the dashboard. ::: ## Project-Level Releases Expand an app in the sidebar and click **Releases** to see only that app's updates, organized by platform tabs. ![Project releases](/img/screenshots/project-releases.png) ## Release Statuses - **Live** -- Active. Eligible devices receive this update on their next check, subject to rollout percentage. - **Disabled** -- Rolled back or manually disabled. No new devices receive it. Devices already running it are unaffected. :::warning A low Success rate on a Live release signals a possible crash or compatibility issue. Consider disabling while you investigate. ::: ## Managing Rollout The Rollout column shows what percentage of users receive the update. A typical strategy: **Step 1:** Start at 10-25%. **Step 2:** Monitor Success rate and Installs. **Step 3:** Increase to 50%, then 100%. **Step 4:** Roll back if issues appear. See [Rollouts](rollouts.md) for details. ## Release Details Click a release row for deeper info: - **Install metrics** -- Total installs and success rate - **Rollback count** -- Devices that reverted - **Bundle size** -- Size of the differential patch - **Release notes** -- Description from publish time - **Target version** -- Compatible binary version ## Rolling Back a Release **Step 1:** Find the release in the list. **Step 2:** Click the **...** menu. **Step 3:** Select **Rollback** or **Disable**. **Step 4:** Confirm. The release becomes `Disabled` immediately. Users who have not installed it yet receive the most recent Live release instead. :::tip Rolling back does not remove the update from devices that already installed it. Publish a new release with the fix instead. ::: ## Publishing from the Dashboard **Step 1:** Click **Ship Release**. **Step 2:** Select your app and platform. **Step 3:** Upload your JavaScript bundle. **Step 4:** Set channel, target version, rollout percentage, and release notes. **Step 5:** Click **Publish**. --- # Rollouts <!-- source: docs/dashboard/rollouts.md --> Phased rollouts let you deliver updates to a controlled percentage of users first, monitor the impact, and widen the rollout once you confirm stability. ## How Phased Rollouts Work When you publish a release below 100% rollout, SwiftPatch delivers it only to that fraction of eligible devices. The selection is **deterministic** -- based on a hash of the device ID: - A device in the 10% group stays in at 25% - You compare a stable cohort against a stable control group - Increasing the percentage only adds new devices, never removes ones that already received the update ## Managing Rollouts ### From the Dashboard Every release row in the **Releases** table shows the current rollout percentage. ![Releases table](/img/screenshots/releases-list.png) **Step 1:** Find the release. **Step 2:** Click the **Rollout** percentage or the **...** menu > **Edit Rollout**. **Step 3:** Enter the new percentage (0-100). **Step 4:** Click **Save**. The change takes effect immediately. :::warning Lowering the rollout percentage does not remove the update from devices that already installed it. To stop a problematic update entirely, **disable the release**. ::: ### From the CLI ```bash swiftpatch releases rollout <release-id> --percent 25 ``` ## Recommended Strategy | Stage | Percentage | Hold | What to validate | |-------|-----------|------|------------------| | 1 -- Canary | 1-5% | 1-2 hours | No crashes or startup failures | | 2 -- Early ring | 10-25% | 4-12 hours | Success rate holds, no rollback spikes | | 3 -- Broad | 50% | 12-24 hours | Performance, user behavior, edge cases | | 4 -- GA | 100% | -- | Full release | :::tip For critical hotfixes, compress the timeline: canary at 5% for 30 minutes, then jump to 100%. ::: ## What to Monitor ![Analytics dashboard](/img/screenshots/analytics-overview.png) | Metric | Healthy | Warning | |--------|---------|---------| | **Success Rate** | Above 98% | Below 95% or trending down | | **Crash Groups** | No new groups | New group appears after rollout | | **Rollback Count** | Zero or near-zero | Climbing steadily | | **Install Volume** | Proportional to rollout % | Plateaus unexpectedly | ### When to Increase Increase when **all** of these hold: - Success rate above 98% for the hold period - No new crash groups linked to the release - Rollback count is zero or negligible - No regression reports from support or testing ### When to Pause or Roll Back Act immediately if you see: - Success rate below 95% - New crash group tied to the release - Steadily climbing rollback count - User reports of broken functionality :::warning Do not wait for metrics to stabilize if you see a clear negative trend. Pausing is always cheaper than pushing a broken update to more users. ::: ## Rollback vs. Pause - **Pause** -- Keep at the current percentage while you investigate. You can resume later. - **Disable / Rollback** -- Mark the release as `Disabled`. Devices receive the most recent Live release instead. To disable: click the **...** menu on the release row, select **Rollback** or **Disable**, and confirm. :::tip If you disable a release and need to ship a fix, publish a new release rather than re-enabling the old one. ::: ## Automating Rollout Promotion Script rollout increases using the CLI and your CI/CD system: ```bash swiftpatch releases rollout <release-id> --percent 25 swiftpatch releases rollout <release-id> --percent 100 ``` Combine with the analytics dashboard to build automated gates that check success rate and crash count before promoting. --- # Settings <!-- source: docs/dashboard/settings.md --> Access workspace settings from the bottom-left of the sidebar. :::note Terminology In the dashboard, your organization is called a **workspace**. ::: | Group | Sections | |-------|----------| | **Workspace** | General, Team, Keys & Tokens, Security | | **Billing** | Plans & Usage, Audit Log | | **Connect** | Integrations | --- ## General ![Workspace settings](/img/screenshots/settings-general.png) ### Workspace Name Appears across the dashboard and in notifications. Change it under **Settings > General**. ### Workspace Short Name Part of your workspace URL: `swiftpatch.io/{short-name}`. :::warning Changing the short name breaks existing bookmarks and shared links. ::: ### Storage Overview | Metric | Description | |--------|-------------| | **Storage Used** | Total storage out of your plan limit | | **Total Bundles** | Bundles uploaded across all apps | | **Total Channels** | Deployment channels created | | **Avg Bundle Size** | Average bundle size | | **Storage Limit** | Maximum for your plan | ### Danger Zone **Delete Workspace** permanently removes all apps, bundles, channels, and team members. :::danger This is irreversible. Export any data you need first. ::: --- ## Integrations ![Integrations](/img/screenshots/settings-integrations.png) | Integration | What it does | |-------------|-------------| | **Firebase Crashlytics** | Import native crash data with AI analysis | | **Slack** | Real-time release and crash notifications | | **Linear** | Auto-create issues for deployments | | **Notion** | Sync release notes to Notion | | **AI Assistant (Claude)** | Auto-generate release notes and crash analysis | To connect: go to **Settings > Integrations**, click the service card, and follow the prompts. :::info Coming Soon Discord, GitHub, Webhooks, and Analytics Export integrations are on the way. ::: --- ## Keys & Tokens ![API keys and tokens](/img/screenshots/settings-api-keys.png) ### API Keys API keys provide programmatic access to the SwiftPatch API with configurable scopes and expiration. To create one: **Settings > Keys & Tokens > API Keys > + Create Key**. :::warning API keys are shown only once at creation time. Copy it immediately. ::: ### CI Tokens App-scoped tokens for CI/CD pipelines. Authenticate via the `x-ci-token` header. To create one: switch to the **CI Tokens** tab and click **Create Token**. **Token format:** `sp_ci_{8-char-prefix}_{32-char-secret}` Tokens are stored as SHA-256 hashes on the server. You can **Revoke** (immediate) or **Regenerate** (new token, old one revoked). ### App-Level Tokens Each app has two tokens: | Token | Prefix | Purpose | |-------|--------|---------| | **App Token** | `at_` | SDK uses this to check for and download updates | | **Deployment Key** | `dk_` | CLI and CI/CD use this for publishing | Regenerating one does not affect the other. :::tip Use **App ID** in native config (`SwiftPatchAppId`) so the SDK resolves the latest token automatically. This lets you rotate tokens without an app store update. See [Key Rotation](/guides/key-rotation). ::: --- ## Security ### Bundle Signing Enable RSA code signing to verify bundle integrity. Recommended for enterprise deployments. **Step 1:** Go to **Settings > Security**. **Step 2:** Enable **Bundle Signing**. **Step 3:** Upload your RSA public key. **Step 4:** Configure the SDK with the same key. :::warning Once enabled, every release must be signed. The SDK rejects unsigned bundles. Configure CI signing first. ::: ### Notifications Configure alerts for new releases, rollbacks, usage limits, and security events. --- ## Audit Log Tracks every significant action in your workspace. ![Audit log](/img/screenshots/settings-audit-log.png) **What gets logged:** team changes, key/token creation and revocation, app creation/deletion, releases, rollbacks, integration changes, billing changes, and security setting changes. Filter by event type, actor, or date range. Click **Export CSV** for offline analysis. --- ## Billing -- Plans & Usage ![Billing](/img/screenshots/settings-billing.png) Your plan card shows included features: deploys per month, apps, team members, bandwidth, analytics retention, and support level. A usage meter shows your current month's consumption. ### Plans | Plan | Price | Best for | |------|-------|----------| | **Free** | Free | Side projects | | **Indie** | Paid/month | Solo developers | | **Team** | Paid/month | Small teams | | **Business** | Paid/month | Growing companies | | **Enterprise** | Custom | Large organizations | :::info Upgrades take effect immediately. Downgrades apply at the end of your billing cycle. ::: --- ## Quick Reference | I want to... | Go to | |--------------|-------| | Rename my workspace | General | | Check storage usage | General | | Invite a team member | Team | | Create an API key | Keys & Tokens > API Keys | | Create a CI token | Keys & Tokens > CI Tokens | | Rotate an app token | App Settings > Tokens | | Enable bundle signing | Security | | Connect Slack or Linear | Integrations | | Review workspace activity | Audit Log | | Upgrade my plan | Plans & Usage | | Delete my workspace | General > Danger Zone | --- # Team <!-- source: docs/dashboard/team.md --> # Team Management Manage who can access your workspace and what they can do. Access is controlled at two levels: **workspace-wide** and **per-app**. :::note Terminology In the dashboard, your organization is called a **workspace**. Similarly, "project" is the legacy name for **app** -- some UI labels may still use the older term. ::: ## Workspace Team Go to **Settings > Team** to see all members with their role, last activity, and join date. ![Team management](/img/screenshots/settings-team.png) ## Roles and Permissions | Action | Owner | Admin | Member | Viewer | |---|:---:|:---:|:---:|:---:| | View dashboard | Yes | Yes | Yes | Yes | | View analytics | Yes | Yes | Yes | Yes | | Create releases | Yes | Yes | Yes | No | | Rollback releases | Yes | Yes | Yes | No | | Create apps | Yes | Yes | No | No | | Delete apps | Yes | Yes | No | No | | Manage team | Yes | Yes | No | No | | Manage API keys | Yes | Yes | No | No | | View billing | Yes | Yes | No | No | | Change billing | Yes | No | No | No | | Delete workspace | Yes | No | No | No | Here is what each role means: - **Owner** -- Full control including billing and workspace deletion. One per workspace. - **Admin** -- Manages apps, releases, team, and API keys. Cannot modify billing. - **Member** -- Creates and rolls back releases. Cannot manage apps, team, or billing. - **Viewer** -- Read-only access to dashboard and analytics. :::tip Assign the most restrictive role that still lets a person do their job. ::: ## Inviting Members **Step 1:** Go to **Settings > Team** or an app's **Members** tab. **Step 2:** Click **Invite Member**. **Step 3:** Enter the email and select a role. **Step 4:** Click **Send Invite**. :::warning Invitations expire after 7 days. Resend from the same page if needed. Pending invitations count toward your plan's seat limit. ::: ## App-Level Members You can restrict access to specific apps by managing members at the app level. Open an app, then click **Members**. ![Project members](/img/screenshots/project-members.png) :::tip Use app-level access when different teams own different apps in the same workspace. ::: ## Changing a Role **Step 1:** Go to **Settings > Team**. **Step 2:** Click the **...** menu on the member's row. **Step 3:** Select **Change Role** and choose the new role. Takes effect immediately. ## Removing a Member **Step 1:** Go to **Settings > Team**. **Step 2:** Click the **...** menu > **Remove**. **Step 3:** Confirm. :::warning Removal is immediate. The member loses all access and any active API keys are revoked. ::: ## Plan Limits | Plan | Team Members | |---|---| | Free | 3 | | Indie | 3 | | Team | 10 | | Business | 25 | | Enterprise | Unlimited | See [Limits](/reference/limits) for full plan details. --- # Installation <!-- source: docs/getting-started/installation.md --> Swiftpatch installs in three commands. Everything from patching your native files to creating your app in the dashboard is automated. ```bash # 1. Install the SDK npm install @swiftpatch/react-native # 2. Install the CLI npm install -g swiftpatch-cli # 3. Wire everything up npx swiftpatch init ``` For a walkthrough with screenshots and troubleshooting, see [Quickstart](/getting-started/quickstart). For the detailed breakdown of what `swiftpatch init` patches, see [SDK Installation](/sdk/installation). ## Next steps - [Quickstart](/getting-started/quickstart) — the three-command flow end to end. - [SDK Installation](/sdk/installation) — what the CLI patches, and how. - [Your first update](/getting-started/your-first-update) — ship an OTA. --- # What is Swiftpatch? <!-- source: docs/getting-started/overview.md --> Swiftpatch is an over-the-air (OTA) update platform for React Native. You push a JavaScript bundle from the CLI, and within seconds your users are running the new code — no App Store review, no Play Store delay. The SDK + CLI + dashboard are designed around three pillars: - **Brick-proof.** A three-slot architecture plus a boot counter makes it architecturally impossible for a bad bundle to leave your app stuck on a white screen. See [Brick protection](/concepts/brick-protection). - **AI-native.** Every release is risk-scored. Crashes are auto-clustered and root-caused. When F8 is enabled, Swiftpatch opens a draft GitHub PR with a suggested fix straight from a crash cluster. See [AI features](/guides/ai-doctor). - **Developer-first.** `swiftpatch init` patches `AppDelegate`, `MainApplication`, `Info.plist`, and `strings.xml` for you. You write three lines of JavaScript and deploy. ## The 30-second pitch ```bash # 1. Install the SDK in your RN app npm install @swiftpatch/react-native # 2. Wire up everything (one command) npx swiftpatch init # 3. Ship npx swiftpatch deploy -p ios --hermes ``` Wrap your app once: ```tsx title="App.tsx" import { SwiftPatchProvider } from '@swiftpatch/react-native'; export default function Root() { return ( <SwiftPatchProvider config={{ debug: __DEV__ }}> <App /> </SwiftPatchProvider> ); } ``` That's it. Your app now: - Checks for updates on cold start and every resume. - Downloads only the changed bytes (delta patches, typically 60–98% smaller than a full bundle). - Verifies every bundle with an Ed25519 signature. - Falls back to the store-bundled JavaScript if anything goes wrong. - Shows a polite "update ready" banner to the user automatically — or gets out of your way if you want to render your own UI. ## What makes Swiftpatch different | | Swiftpatch | CodePush (retired) | Expo Updates | Shorebird | |---|---|---|---|---| | React Native (bare) | Yes | Retired | Expo only | Flutter only | | New Architecture | Yes (TurboModule) | No | Yes | n/a | | Delta patches | Yes (bsdiff) | No | Partial | Yes | | Signed bundles | Ed25519 | No | No | Yes | | Brick protection | 3-slot + boot counter + mount marker | Basic rollback | Basic rollback | Yes | | Auto-patched native setup | `swiftpatch init` | Manual | Manual | Manual | | AI crash clustering | Yes | No | No | No | | AI PR agent (F8) | Yes | No | No | No | | Expo config plugin | Yes | No | Yes | n/a | ## Architecture, in one diagram ``` ┌─────────────────────┐ ┌──────────────────────────┐ │ @swiftpatch/ │ │ swiftpatch-cli │ │ react-native (SDK) │ │ (developer machine / CI)│ └─────────┬───────────┘ └──────────┬───────────────┘ │ HTTPS │ HTTPS │ (check / download) │ (bundle / sign / upload) ▼ ▼ ┌─────────────────────────────────────────────────────┐ │ api.swiftpatch.io │ │ ┌─────────┐ ┌──────────┐ ┌─────────────────┐ │ │ │ Releases│ │ Delta │ │ Crash clusters │ │ │ │ service │ │ generator│ │ + F8 PR agent │ │ │ └─────────┘ └──────────┘ └─────────────────┘ │ └──────────────┬──────────────────┬───────────────────┘ │ │ ▼ ▼ ┌────────────────┐ ┌────────────────┐ │ Cloudflare R2 │ │ Dashboard │ │ (bundles + │ │ app.swiftpatch│ │ patches) │ │ .io │ └────────────────┘ └────────────────┘ ``` Three services you touch: - **SDK** — a tiny TypeScript + native library linked into your app. - **CLI** — bundles JavaScript, signs it, uploads it, creates the release. - **Dashboard** — view releases, manage rollouts, read crash clusters, install the F8 GitHub App. ## Next steps - [Quickstart](/getting-started/quickstart) — from zero to a live OTA in under 10 minutes. - [SDK installation](/sdk/installation) — how `swiftpatch init` patches your project. - [Brick protection](/concepts/brick-protection) — the architectural guarantee that your users never see a white screen. - [Migrating from CodePush](/guides/migrating-from-codepush) — step-by-step port guide. --- # Project structure <!-- source: docs/getting-started/project-structure.md --> After you run `swiftpatch init`, your project grows a handful of new files and modifications. None of them contain secrets that can't be committed — except for one. ## New files ``` your-app/ ├── .swiftpatch/ # gitignored │ └── keys/ │ ├── private.pem # Ed25519 signing key — NEVER commit │ └── public.pem # Ed25519 verification key — upload to dashboard ├── .swiftpatchrc # commit-safe project config ``` ## Modified files ``` your-app/ ├── .gitignore # adds .swiftpatch/, swiftpatch-artifacts/, swiftpatch-private.pem ├── ios/ │ └── YourApp/ │ ├── AppDelegate.mm # swiftpatch-begin/end block in bundleURL │ └── Info.plist # SwiftPatchDeploymentKey, BGTaskScheduler, UIBackgroundModes └── android/ └── app/ └── src/main/ ├── java/.../MainApplication.kt # swiftpatch-begin/end block in getJSBundleFile └── res/values/strings.xml # swiftpatch_deployment_key ``` ## `.swiftpatchrc` The project's primary config. Commit this. It does **not** contain secrets. ```json { "appId": "app_abc123", "app": "my-app", "platform": "ios", "signingKey": "./.swiftpatch/keys/private.pem" } ``` See [Configuration](/sdk/configuration) for the full field reference. ## Signing keys The Ed25519 private key signs every release. The public key verifies releases on-device. - **Private key** — never commit. `.gitignore` protects you by default. - **Public key** — safe to share. It's embedded in the app binary (`Info.plist` / `strings.xml`) and uploaded to the dashboard. For CI, store the private key as an encrypted secret (e.g., GitHub `secrets.SWIFTPATCH_PRIVATE_KEY`) and write it to a file at pipeline time. See [CI/CD](/cli/ci-cd). ## The marker blocks Every native file Swiftpatch patches is wrapped in `// swiftpatch-begin` / `// swiftpatch-end` markers: ```swift title="AppDelegate.swift" override func bundleURL() -> URL? { // swiftpatch-begin (auto-generated, do not edit between markers) if let url = SwiftPatchModule.getBundleURL() { return url } // swiftpatch-end // Your existing code continues unchanged below #if DEBUG ... #endif } ``` Editing between the markers manually is safe but will be overwritten on the next `swiftpatch init`. To remove Swiftpatch cleanly, run `swiftpatch unlink` — it strips every marked block. ## Next steps - [Quickstart](/getting-started/quickstart) — the three-command flow. - [SDK installation](/sdk/installation) — detailed patch breakdown. - [Configuration](/sdk/configuration) — every option. --- # Quick Start <!-- source: docs/getting-started/quick-start.md --> This page has moved. See **[Quickstart](/getting-started/quickstart)** for the three-command flow: install → `swiftpatch init` → `swiftpatch deploy`. --- # Quickstart <!-- source: docs/getting-started/quickstart.md --> You will get from zero to a live OTA update in three commands. There is no AppDelegate editing, no Info.plist hand-patching, no `pod install` debugging. `swiftpatch init` does all of it for you. **Prerequisites:** - React Native 0.76+ (Hermes recommended) - Node.js 20+ - A [Swiftpatch account](https://app.swiftpatch.io/signup) (free) - CocoaPods for iOS builds ## Step 1 — Install the SDK and CLI ```bash # In your React Native project npm install @swiftpatch/react-native # Global CLI npm install -g swiftpatch-cli ``` Then log in (opens a browser): ```bash swiftpatch login ``` ## Step 2 — Wire everything up ```bash npx swiftpatch init ``` This command does all of the setup for you: - Detects whether your project is bare React Native, Expo bare, or a monorepo. - Creates (or links) an app in your Swiftpatch dashboard. - Generates an Ed25519 signing keypair at `.swiftpatch/keys/`. - Patches `AppDelegate.mm` or `AppDelegate.swift` to load OTA bundles. - Patches `MainApplication.kt` or `MainApplication.java` to load OTA bundles on Android. - Adds `SwiftPatchDeploymentKey` to `Info.plist` and `swiftpatch_deployment_key` to `strings.xml`. - Registers `com.swiftpatch.bg-refresh` in `BGTaskSchedulerPermittedIdentifiers` and adds `fetch` + `processing` to `UIBackgroundModes`. - Writes a project config to `.swiftpatchrc`. - Adds `.swiftpatch/`, `swiftpatch-artifacts/`, and `swiftpatch-private.pem` to `.gitignore`. Every patched file is wrapped in `// swiftpatch-begin` / `// swiftpatch-end` markers. The insertion is additive — your existing code is never rewritten, just wrapped with a block that calls Swiftpatch first and falls through to the default bundle loader if Swiftpatch returns nothing. :::tip Safe to re-run `swiftpatch init` is idempotent. If markers already exist, it skips the patch. If you want to remove everything cleanly, run `swiftpatch unlink`. ::: :::note Manual install If you prefer to copy-paste the snippets yourself, run `swiftpatch init --manual`. The CLI will still generate keys, write `.swiftpatchrc`, and update `.gitignore` — it just prints the native snippets for you to apply by hand. ::: Re-pod on iOS if you changed anything: ```bash cd ios && pod install && cd .. ``` ## Step 3 — Wrap your app ```tsx title="App.tsx" import { SwiftPatchProvider } from '@swiftpatch/react-native'; export default function Root() { return ( <SwiftPatchProvider config={{ debug: __DEV__ }}> <App /> </SwiftPatchProvider> ); } ``` You do not need to pass the deployment key — the SDK reads it from `Info.plist` / `strings.xml` that `swiftpatch init` wrote. The provider ships with sensible defaults: - `autoShowBanner` (default `true`) — renders a polite `<UpdateBanner />` when an update is ready. - `blockOnMandatory` (default `true`) — renders a full-screen `<UpdateBlocker />` if the release is marked mandatory. - No further wiring required. See [SwiftPatchProvider reference](/sdk/provider) for the full prop surface, or [UI components](/sdk/ui-components) if you want to theme the banner. ## Step 4 — Ship your first update Make any JS-only change (fix a typo, change a color, adjust a prop). Then: ```bash swiftpatch deploy -p ios --hermes ``` This command: 1. Runs `react-native bundle` with `--reset-cache` cleanly. 2. Compiles to Hermes bytecode. 3. Signs the bundle with your Ed25519 private key. 4. Uploads to Cloudflare R2. 5. Creates a release at `app.swiftpatch.io`. Within seconds you will see the release appear in the [dashboard](https://app.swiftpatch.io). Relaunch your app — the update downloads in the background and the banner appears when it is ready to apply. :::tip Android too Run the same command with `-p android`. You can deploy both in parallel: ```bash swiftpatch deploy -p ios --hermes & swiftpatch deploy -p android --hermes ``` ::: ## What just happened - Your app cold-started and called the check-for-update API with the device's current bundle hash. - The server responded with a delta patch descriptor (a bsdiff file against your current bundle). - The SDK downloaded the patch (typically 60–98% smaller than the full bundle), verified the Ed25519 signature, applied `bspatch` on-device, and re-verified the resulting SHA-256. - The banner appeared. When you tapped "Apply now," the SDK swapped the active bundle slot and restarted the JS runtime. - Your app is now running the new code. If anything at any step had failed — bad signature, hash mismatch, network error, bsdiff corruption — the SDK would have silently fallen back to a full download, then to the previous stable slot, then to the store-bundled JavaScript. Your users never see a crash. ## Next steps - [SDK installation](/sdk/installation) — understand what `swiftpatch init` patched. - [SDK provider reference](/sdk/provider) — customize the Provider's behavior. - [UI components](/sdk/ui-components) — theme or replace the banner / blocker / onboarding. - [Staged rollouts](/guides/staged-rollouts) — ship to 5% of users first. - [CI/CD integration](/cli/ci-cd) — automate `swiftpatch deploy` from GitHub Actions. --- # Your first update <!-- source: docs/getting-started/your-first-update.md --> You've completed [Quickstart](/getting-started/quickstart) and wrapped your app with `<SwiftPatchProvider>`. Now let's ship a change. ## 1. Make a JS change Edit any JavaScript file. For example, change a button color, update a string, or fix a typo. Anything JavaScript-only works. Native changes still require an app store release. ## 2. Deploy ```bash swiftpatch deploy -p ios --hermes ``` For Android, swap `-p ios` with `-p android`. To deploy both: ```bash swiftpatch deploy -p ios --hermes && swiftpatch deploy -p android --hermes ``` ## 3. See it on-device Launch your app. Within seconds: - The SDK calls the check-for-update API with your device's current bundle hash. - The API responds with a delta patch descriptor. - The SDK downloads the patch, verifies it, applies it, and re-verifies the result. - The built-in `<UpdateBanner>` slides in: "New update available · v1.2.3 · Apply now | Later". Tap **Apply now** — the app restarts on the new JS bundle. Typically fewer than 2 seconds end to end. ## 4. Check the dashboard Open [app.swiftpatch.io](https://app.swiftpatch.io) and find your release. You'll see: - Release id, hash, size. - Real-time adoption graph. - Per-device download / apply status. - Crash rate (zero so far, hopefully). ## Iterate That's the loop. Make a change → `swiftpatch deploy` → users get it within seconds. No App Store review. No TestFlight. No Play Store delay. ## Next steps - [Staged rollouts](/guides/staged-rollouts) — ship to 5% first, then 25%, then 100%. - [CI/CD](/cli/ci-cd) — automate deploys from GitHub Actions. - [UI components](/sdk/ui-components) — customize or replace the built-in banner. - [Error codes](/sdk/error-codes) — know what to look for when things go wrong. --- # AI doctor <!-- source: docs/guides/ai-doctor.md --> `swiftpatch doctor --ai` is a one-shot project analysis that inspects your React Native codebase for OTA readiness issues. It uses Claude to understand context that purely heuristic checks miss — things like "this asset is imported dynamically but not bundled statically," or "Hermes is enabled but `metro.config.js` is excluding TypeScript." ## Quick start ```bash swiftpatch doctor --ai ``` You'll be prompted for consent before any code is read. Once you confirm, the doctor: 1. Scans `package.json` for native dependencies. 2. Inspects your Metro / Hermes config. 3. Reads `AppDelegate` and `MainApplication` for correct bundle-loader wiring. 4. Checks asset imports for dynamic vs. static patterns. 5. Verifies signing-key configuration. 6. Cross-references the RN version against known compatibility issues. 7. Streams findings as they're generated. At the end you get a report with: - **Pass** — things that look good. - **Warn** — things that are probably fine but worth verifying. - **Fail** — things that will break an OTA update. ## Non-AI doctor The plain `swiftpatch doctor` (no `--ai`) runs the deterministic subset of checks. Use this in CI — it's fast, deterministic, and doesn't need a Claude API key: ```bash swiftpatch doctor ``` | Check | What it verifies | |---|---| | `ios-integration` | `AppDelegate` has the `SwiftPatchModule.getBundleURL()` override. | | `android-integration` | `MainApplication` has the `SwiftPatchModule.getJSBundleFile()` override. | | `deployment-key` | Your native config has a deployment key. | | `signing-key` | `.swiftpatch/keys/private.pem` exists. | | `rn-version` | Your React Native version is supported. | | `hermes` | Hermes is enabled (recommended). | | `assets` | Metro's asset resolver isn't excluding your asset types. | Pass `--json` for machine-readable output (exit code non-zero on any `fail`): ```bash swiftpatch doctor --json > doctor.json ``` ## When to use AI mode Use `--ai` when: - You're onboarding a new project and want a thorough audit before first deploy. - A deploy succeeded but the OTA update doesn't apply on-device — the AI can correlate your app's code patterns against common asset/bundler issues. - You're migrating from CodePush and want a gap analysis. - You're debugging a suspected native-JS mismatch. Don't use `--ai` when: - You're in CI — use `swiftpatch doctor --json` instead. - You're on a cold machine without a Claude API key configured. - You need sub-second turnaround — `--ai` typically takes 10–60 seconds. ## Setting up a Claude API key `swiftpatch doctor --ai` requires a Claude API key. Three options: ```bash # Option 1 — encrypted local config (recommended for local dev) swiftpatch config set claudeApiKey sk-ant-xxxxx # Option 2 — environment variable (CI-friendly) export SWIFTPATCH_CLAUDE_API_KEY=sk-ant-xxxxx # Option 3 — Swiftpatch Pro # Platform-provisioned key. No setup needed — the CLI pulls a short-lived # token from the Swiftpatch API when you're on a Pro plan. ``` Get a key from [console.anthropic.com](https://console.anthropic.com). The key is stored encrypted at rest in `~/.swiftpatch/config`. ## Data and privacy Before any local data is read or sent: 1. The CLI shows exactly what data categories will be accessed. 2. You must explicitly consent (or pass `--yes` in CI). 3. Consent can be remembered or revoked at any time: ```bash swiftpatch config delete aiConsentScopes ``` No source code is stored by the Swiftpatch AI service. All transmissions are over TLS. Only the categories you consented to (package.json, native config files, Metro config) are read. Remove your stored API key any time: ```bash swiftpatch config delete claudeApiKey ``` ## Related AI commands The CLI has three other local AI commands: ```bash # Generate a changelog from git log between last tag and HEAD swiftpatch ai explain # Safety-review your JS/TS changes before deploying OTA swiftpatch ai review # Full doctor via the AI path (same as `swiftpatch doctor --ai`) swiftpatch ai doctor ``` All three support `-y/--yes` to skip consent in CI and `--json` for machine-readable output. ## Backend-side AI commands Some AI features query the Swiftpatch backend (no Claude API key needed): ```bash # Ask free-form questions about your releases and crashes swiftpatch ai ask "Why did r_abc crash so much more than r_xyz?" # Get the AI risk assessment for a release swiftpatch ai risk-score r_abc # List AI-clustered crash reports swiftpatch ai crashes -a my-app # View AI-generated insights swiftpatch ai insights -a my-app ``` These use your Swiftpatch authentication — run `swiftpatch login` first. ## Next steps - [F8 PR agent](/guides/github-app-setup) — turn crash clusters into draft PRs. - [Troubleshooting](/sdk/troubleshooting) — the manual equivalents of what the AI doctor checks. - [CLI commands](/cli/commands) — full command reference. --- # CI/CD Integration <!-- source: docs/guides/ci-cd-integration.md --> Automate your OTA deployments so every push to `main` ships an update to your users -- no manual steps required. :::tip For CLI reference, see [CI/CD CLI Reference](/cli/ci-cd). This guide focuses on step-by-step setup. ::: ## How It Works 1. Developer pushes code to `main` 2. CI runs `swiftpatch deploy` 3. Users receive the update on next app launch ## Prerequisites :::info Before you start - SwiftPatch CLI installed in your CI environment - A CI token stored as a secret (`SWIFTPATCH_CI_TOKEN`) ::: ## Step 1: Create a CI Token **Step 1:** Go to your app in the [dashboard](https://app.swiftpatch.io). **Step 2:** Navigate to **Settings > API Keys**. **Step 3:** Click **Create Token** and name it. **Step 4:** Copy immediately -- it is shown only once. :::note CI tokens use prefix `sp_ci_` and are scoped to one app. ::: ## Step 2: Set Up GitHub Actions ```yaml title=".github/workflows/ota-deploy.yml" name: OTA Deploy on: push: branches: [main] paths: - 'src/**' - 'package.json' jobs: deploy: runs-on: ubuntu-latest strategy: matrix: platform: [ios, android] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - run: npm ci - run: npm test - run: npm install -g @swiftpatch/cli - name: Deploy run: | swiftpatch deploy \ -p ${{ matrix.platform }} \ --hermes \ -n "Deploy from commit ${{ github.sha }}" \ --ci-token ${{ secrets.SWIFTPATCH_CI_TOKEN }} ``` ## Step 3: Separate Staging and Production Use different branches and tokens for each environment: ```yaml jobs: staging: if: github.ref == 'refs/heads/develop' steps: - # ... setup - run: swiftpatch deploy -p ios --hermes --ci-token ${{ secrets.SWIFTPATCH_STAGING_CI_TOKEN }} production: if: github.ref == 'refs/heads/main' steps: - # ... setup - run: swiftpatch deploy -p ios --hermes --ci-token ${{ secrets.SWIFTPATCH_CI_TOKEN }} ``` ## Environment Variables | Variable | Description | |----------|-------------| | `SWIFTPATCH_CI_TOKEN` | CI token for authentication | :::warning Always use CI secrets management. Never hardcode tokens in your workflow files. ::: ## Best Practices 1. **Always use `--hermes`** if Hermes is enabled (RN 0.70+ default) 2. **Match `--app-version` exactly** to the native binary version 3. **Run tests before deploying** -- broken code should never reach `swiftpatch deploy` 4. **Ensure clean working directory** -- `deploy` bundles whatever is in the working directory 5. **Use staged rollouts** -- manage rollout percentage from the dashboard 6. **Include commit info** in release descriptions for traceability 7. **Separate staging and production** tokens 8. **Monitor after deploy** -- watch for rollback events in the dashboard 9. **Sign bundles** with `--private-key` for enterprise security --- # Custom Update UI <!-- source: docs/guides/custom-update-ui.md --> Build your own update experience using SwiftPatch hooks. Below are four ready-to-use patterns -- pick the one that fits your app. ## Pattern 1: Update Banner A subtle banner at the top of the screen. Great for optional updates. ```tsx import React from 'react'; import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; import { useSwiftPatch, UpdateStatus } from '@swiftpatch/react-native'; function UpdateBanner() { const { status, availableUpdate, downloadProgress, downloadUpdate, restart } = useSwiftPatch(); if (status === UpdateStatus.UP_TO_DATE || status === UpdateStatus.CHECKING) { return null; } return ( <View style={styles.banner}> {status === UpdateStatus.UPDATE_AVAILABLE && ( <TouchableOpacity onPress={downloadUpdate} style={styles.row}> <Text style={styles.text}> Version {availableUpdate?.version} available </Text> <Text style={styles.action}>Update</Text> </TouchableOpacity> )} {status === UpdateStatus.DOWNLOADING && ( <View style={styles.row}> <Text style={styles.text}> Downloading... {downloadProgress?.percentage}% </Text> <View style={[styles.progressBar, { width: `${downloadProgress?.percentage}%` }]} /> </View> )} {status === UpdateStatus.RESTART_REQUIRED && ( <TouchableOpacity onPress={restart} style={styles.row}> <Text style={styles.text}>Update ready</Text> <Text style={styles.action}>Restart</Text> </TouchableOpacity> )} </View> ); } const styles = StyleSheet.create({ banner: { backgroundColor: '#6366f1', paddingHorizontal: 16, paddingVertical: 12 }, row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, text: { color: 'white', fontSize: 14 }, action: { color: 'white', fontWeight: 'bold', fontSize: 14 }, progressBar: { position: 'absolute', bottom: 0, left: 0, height: 2, backgroundColor: 'white' }, }); ``` ## Pattern 2: Full-Screen Modal A more prominent prompt. Good when you want to encourage users to update. ```tsx import React from 'react'; import { Modal, View, Text, Button, ActivityIndicator, StyleSheet } from 'react-native'; import { useSwiftPatch, UpdateStatus } from '@swiftpatch/react-native'; function UpdateModal() { const { status, availableUpdate, downloadProgress, downloadUpdate, restart } = useSwiftPatch(); const showModal = status === UpdateStatus.UPDATE_AVAILABLE || status === UpdateStatus.DOWNLOADING || status === UpdateStatus.RESTART_REQUIRED; if (!showModal) return null; return ( <Modal visible animationType="slide" transparent> <View style={styles.overlay}> <View style={styles.modal}> <Text style={styles.title}>Update Available</Text> <Text style={styles.version}>Version {availableUpdate?.version}</Text> {availableUpdate?.releaseNote && ( <Text style={styles.description}>{availableUpdate.releaseNote}</Text> )} {status === UpdateStatus.UPDATE_AVAILABLE && ( <Button title="Download Update" onPress={downloadUpdate} /> )} {status === UpdateStatus.DOWNLOADING && ( <View style={styles.progress}> <ActivityIndicator size="large" color="#6366f1" /> <Text style={styles.progressText}>{downloadProgress?.percentage}%</Text> </View> )} {status === UpdateStatus.RESTART_REQUIRED && ( <Button title="Restart Now" onPress={restart} /> )} </View> </View> </Modal> ); } const styles = StyleSheet.create({ overlay: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.5)' }, modal: { backgroundColor: 'white', borderRadius: 16, padding: 24, width: '80%', alignItems: 'center' }, title: { fontSize: 20, fontWeight: 'bold', marginBottom: 8 }, version: { fontSize: 16, color: '#6366f1', marginBottom: 12 }, description: { fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 20 }, progress: { alignItems: 'center', gap: 12 }, progressText: { fontSize: 18, fontWeight: 'bold' }, }); ``` ## Pattern 3: Silent Background Updates No UI at all. The update downloads and installs silently, then applies on the next restart. ```tsx import { useEffect } from 'react'; import { useSwiftPatch, UpdateStatus } from '@swiftpatch/react-native'; function SilentUpdater() { const { status, downloadUpdate, installUpdate } = useSwiftPatch(); useEffect(() => { if (status === UpdateStatus.UPDATE_AVAILABLE) { downloadUpdate() .then(() => installUpdate()) .catch(console.error); } }, [status]); return null; } ``` The update applies on next restart based on `installMode`. :::tip This is the simplest approach. Users never see an update prompt -- changes just appear next time they open the app. ::: ## Pattern 4: Mandatory Update Gate Block the entire app until a mandatory update is applied: ```tsx import { useSwiftPatch, UpdateStatus } from '@swiftpatch/react-native'; function MandatoryUpdateGate({ children }) { const { availableUpdate, isRestartRequired, restart } = useSwiftPatch(); if (availableUpdate?.isMandatory && isRestartRequired) { return ( <View style={styles.fullScreen}> <Text style={styles.title}>Update Required</Text> <Text>A required update has been downloaded.</Text> <Button title="Restart Now" onPress={restart} /> </View> ); } return children; } ``` :::warning Only use this pattern for mandatory updates. Blocking optional updates frustrates users. ::: --- # F8 — GitHub App setup <!-- source: docs/guides/github-app-setup.md --> F8 is the Swiftpatch AI PR agent. Given a crash cluster in the dashboard, F8 analyzes the root cause and opens a draft pull request in your GitHub repo with a suggested fix. You review, tweak, and merge — or close if the fix isn't right. This guide walks you through the one-time setup. ## Prerequisites - A repository on GitHub you have admin access to. - A Swiftpatch organization with F8 enabled (contact `sales@swiftpatch.io` to opt in). - The Swiftpatch CLI installed. ## 1. Install the Swiftpatch GitHub App From the dashboard: 1. Go to **Settings → Integrations → GitHub**. 2. Click **Install GitHub App**. 3. You'll be redirected to GitHub. Pick the organization (or personal account) that owns the repo. 4. Grant the app access to: - **All repositories**, or - **Only select repositories** — pick the repos you want F8 to open PRs in. 5. The required permissions: - **Contents**: Read + write (to create branches and commits). - **Pull requests**: Read + write (to open draft PRs). - **Metadata**: Read (always required). You'll land back on the Swiftpatch dashboard with a green "Connected" badge. ## 2. Link a repo to an app Each Swiftpatch app maps to one GitHub repo. Link them from the dashboard: 1. Go to **Apps → `<your-app>` → Settings → Repository**. 2. Pick the GitHub repo from the dropdown. Only repos the GitHub App has access to appear here. 3. Set the default base branch for PRs (typically `main` or `develop`). You can change the base branch per PR at `swiftpatch pr <clusterId> --base feature-branch`. ## 3. Open your first PR Find a crash cluster in the dashboard. Copy its `clusterId` (or use the CLI to list them): ```bash swiftpatch ai crashes --app my-app ``` Then run: ```bash swiftpatch pr crash_abc123 ``` You'll see: ``` PR opened: https://github.com/your-org/your-repo/pull/42 Branch : swiftpatch/fix/abc12345 PR number : #42 Files : src/reducers/user.ts, src/components/ProfileHeader.tsx Commit : a3f71c9b12 ``` The PR is opened as a **draft** — it won't trigger CI notifications or reviewer pings until you convert it to ready. ## What F8 does Given a crash cluster, F8: 1. Fetches the cluster analysis from the Swiftpatch API (root cause + suggested fix diff). 2. Via the GitHub App, creates a branch named `swiftpatch/fix/<shortHash>` from the base branch. 3. Applies the diff to the branch. 4. Commits with message `auto-fix: <rootCause>` and a `Swiftpatch-Cluster: <clusterId>` trailer. 5. Opens a draft PR with: - Title: `Swiftpatch AI fix: <rootCause>`. - Body: fix summary, affected files, cluster link, confidence score. - Label: `swiftpatch-ai` (if configured). ## Guardrails F8 has bounded authority by design: - **File denylist** — never touches lockfiles (`package-lock.json`, `yarn.lock`, `Podfile.lock`), CI configs (`.github/workflows/*`), secret-adjacent paths. - **Max 500 lines changed per PR**. Larger diffs are rejected with a message asking you to open a manual PR. - **Max 10 PRs per repo per day**. Prevents runaway loops. - **Drafts only**. F8 never opens a ready-for-review PR, never auto-merges. - **Base branch respected**. F8 creates branches off your configured base, not off HEAD of another F8 branch. Override any of these at `swiftpatch pr <clusterId> --<option>` — see `swiftpatch pr --help`. ## Dry-run mode Before touching anything, see what F8 would do: ```bash swiftpatch pr crash_abc123 --dry-run ``` Output: ``` Dry run — nothing pushed. Branch : swiftpatch/fix/abc12345 Base : main Title : Swiftpatch AI fix: null dereference in ProfileHeader Files : src/components/ProfileHeader.tsx ``` No branch is created, no commit made, no PR opened. ## The legacy local flow Before the GitHub App, F8 ran locally via `git apply`. You can still use it with `--local`: ```bash swiftpatch pr crash_abc123 --local ``` This flow: 1. Requires you to be inside the repo's working tree. 2. Creates a branch locally. 3. Applies the patch with `git apply`. 4. Commits. 5. Uses `gh` CLI (if installed) to push and open the PR — or prints the commands for you to run. The GitHub App flow is preferred for CI/CD and team environments. The local flow is useful if you don't want a third-party app installed on the repo. ## Opting out Uninstall the app from GitHub's organization settings → Installed GitHub Apps → Swiftpatch → Uninstall. This immediately revokes all access. From the Swiftpatch dashboard, disconnecting the repo stops F8 from operating on it but leaves the GitHub App installed for other repos. ## Troubleshooting - **"Repository not accessible"** — the GitHub App wasn't granted access to this repo. Re-install the app from GitHub and include the repo. - **"Base branch not found"** — your configured base branch (typically `main`) doesn't exist. Set a different base in the dashboard. - **"Diff stale"** — the cluster's suggested fix was generated against an older commit, and the relevant lines have moved. Regenerate the analysis from the dashboard. - **PR body is truncated** — GitHub has a 65K character limit on PR bodies. F8 trims gracefully; the full analysis is always available in the dashboard. ## Next steps - [AI doctor](/guides/ai-doctor) — project-level AI analysis for OTA readiness. - [Crash detection](/concepts/crash-detection) — how clusters are formed. - [CLI PR command](/cli/commands) — all the flags. --- # Key Rotation <!-- source: docs/guides/key-rotation.md --> Starting in SDK v2.1.0, you can rotate tokens without pushing an app store update. The trick: use a stable **App ID** instead of embedding tokens directly in your binary. :::info SDK v2.3.0+ SwiftPatch uses separate **App Tokens** (`at_` prefix) for the SDK and **Deployment Keys** (`dk_` prefix) for CLI/CI. Each can be rotated independently. ::: ## The Problem Before v2.1.0, the deployment key was hardcoded in native config. Regenerating it meant every existing install stopped receiving updates until users downloaded a new binary from the app store. ## The Solution: App ID The App ID is a stable identifier that never changes. When the SDK uses an App ID, it: 1. Resolves the current app token from the server on first launch 2. Caches the token on device 3. Automatically refreshes the token if it becomes invalid after rotation You can regenerate your app token at any time, and all installs pick up the new token on their next check. ## Step 1: Upgrade to SDK v2.1.0+ ```bash npm install @swiftpatch/react-native@latest cd ios && pod install && cd .. ``` ## Step 2: Replace Deployment Key with App ID Find your App ID in the dashboard under **Settings**. **iOS -- Info.plist:** ```xml <!-- Remove legacy keys --> <!-- <key>SwiftPatchAppToken</key> --> <!-- <string>at_YOUR_APP_TOKEN_HERE</string> --> <!-- Add App ID --> <key>SwiftPatchAppId</key> <string>YOUR_APP_ID</string> ``` **Android -- strings.xml:** ```xml <!-- Remove legacy keys --> <!-- <string name="SwiftPatchAppToken">at_YOUR_APP_TOKEN_HERE</string> --> <!-- Add App ID --> <string name="SwiftPatchAppId">YOUR_APP_ID</string> ``` ## Step 3: Push an App Store Update This is the **last** app store update needed for credential changes. After this, rotations are handled automatically. ## Step 4: Rotate Tokens **Step 1:** Go to your app's **Tokens** page in the dashboard. **Step 2:** Click **Regenerate** next to App Token or Deployment Key. **Step 3:** Confirm. All installs pick up the new token within minutes. :::tip You can rotate the App Token and Deployment Key independently. Rotating one does not affect the other. ::: ## How It Works ``` App Launch -> SDK checks for app token |-- Has deploymentKey in native config? -> Use directly (legacy) +-- Has appId? -> Resolve from server (v2.3.0+) +-- POST /sdk/resolve-key -> returns { appToken, deploymentKey } +-- SDK caches token -> normal update flow ``` **On token rotation:** ``` SDK uses cached (old) token -> Server responds INVALID_DEPLOYMENT_KEY -> SDK clears cache -> resolves new token via appId -> caches new token -> retries update check ``` ## Token Types | Token | Prefix | Purpose | Rotated via | |-------|--------|---------|-------------| | **App Token** | `at_` | SDK runtime (check/download updates) | Dashboard > Tokens | | **Deployment Key** | `dk_` | CLI/CI automation | Dashboard > Tokens | | **App ID** | (none) | Stable identifier, never changes | Cannot be rotated | ## Security - App ID identifies your app but does not grant access alone - The resolve-key endpoint is rate-limited - App tokens grant read-only SDK access only - Setting `bundleIdentifier` in the dashboard adds an extra layer of security ## Backward Compatibility - Apps using `SwiftPatchDeploymentKey` or `SwiftPatchAppToken` continue to work - The server accepts both `at_` and `dk_` tokens in API calls - You can migrate gradually -- all methods work simultaneously ## FAQ **What if the device is offline when the token is rotated?** The app uses the cached token. When it is rejected, the SDK resolves the new one. If still offline, updates pause until connectivity returns. The app itself keeps running. **What is the difference between App Token and Deployment Key?** App Token (`at_`) is for the mobile SDK. Deployment Key (`dk_`) is for CLI/CI. They are separate and rotate independently. **Can I switch back to a hardcoded deployment key?** Yes. Add `SwiftPatchDeploymentKey` to native config and the SDK uses it directly. --- # Mandatory Updates <!-- source: docs/guides/mandatory-updates.md --> Mandatory updates force users to install an update before they can continue using the app. Use them for critical security fixes or breaking API changes. :::warning Use sparingly A broken mandatory update locks users out of your app entirely. Always test on a staging channel first. ::: ## Step 1: Publish a Mandatory Update ### Via CLI ```bash swiftpatch deploy -p ios --hermes --mandatory -n "Critical security fix" ``` ### Via Dashboard **Step 1:** Create a new release. **Step 2:** Check **Mandatory Update**. **Step 3:** Publish. ## Step 2: Handle It in Your App Block the UI until the update is applied: ```tsx import React from 'react'; import { Modal, View, Text, Button, ActivityIndicator } from 'react-native'; import { useSwiftPatch, UpdateStatus } from '@swiftpatch/react-native'; function MandatoryUpdateScreen() { const { availableUpdate, status, downloadProgress, downloadUpdate, restart } = useSwiftPatch(); if (!availableUpdate?.isMandatory) return null; return ( <Modal visible animationType="fade"> <View style={{ flex: 1, justifyContent: 'center', padding: 24 }}> <Text style={{ fontSize: 24, fontWeight: 'bold', textAlign: 'center' }}> Update Required </Text> <Text style={{ textAlign: 'center', marginVertical: 16, color: '#666' }}> A critical update is available. Please update to continue. </Text> {status === UpdateStatus.UPDATE_AVAILABLE && ( <Button title="Update Now" onPress={downloadUpdate} /> )} {status === UpdateStatus.DOWNLOADING && ( <View style={{ alignItems: 'center' }}> <ActivityIndicator size="large" /> <Text style={{ marginTop: 12 }}> Downloading... {downloadProgress?.percentage}% </Text> </View> )} {status === UpdateStatus.RESTART_REQUIRED && ( <Button title="Restart" onPress={restart} /> )} </View> </Modal> ); } ``` ## Best Practices - **Use sparingly** -- only for critical security fixes or breaking API changes - **Explain why** the update is required in the UI - **Show progress** during download so users know something is happening - **Test thoroughly** -- a broken mandatory update locks users out - **Test on staging first** before publishing to production :::danger A broken mandatory update prevents users from accessing your app. There is no way to recover except publishing a fixed update or a new app store binary. Always test on a staging deployment first. ::: --- # Migrating from CodePush <!-- source: docs/guides/migrating-from-codepush.md --> :::tip This guide is for teams migrating from Microsoft CodePush. If you are starting fresh, follow the [Quick Start](/getting-started/quick-start) instead. ::: Microsoft has retired AppCenter and CodePush. This guide walks you through switching to SwiftPatch in six steps. ## Step 1: Install SwiftPatch SDK ```bash npm uninstall react-native-code-push npm install @swiftpatch/react-native cd ios && pod install && cd .. ``` ## Step 2: Create Your App **Step 1:** Sign up at [app.swiftpatch.io](https://app.swiftpatch.io). **Step 2:** Create a new app. **Step 3:** Copy your **App ID** from Settings. ## Step 3: Add Native Credentials **iOS -- Info.plist:** ```xml title="ios/YourApp/Info.plist" <key>SwiftPatchAppId</key> <string>YOUR_APP_ID</string> ``` **Android -- strings.xml:** ```xml title="android/app/src/main/res/values/strings.xml" <string name="SwiftPatchAppId">YOUR_APP_ID</string> ``` ## Step 4: Update Native Code **Android -- MainApplication.kt:** ```diff - import com.microsoft.codepush.react.CodePush + import com.swiftpatch.SwiftPatchModule override fun getJSBundleFile(): String? { - return CodePush.getJSBundleFile() + return SwiftPatchModule.getJSBundleFile(applicationContext) + ?: super.getJSBundleFile() } ``` **iOS -- AppDelegate.swift:** ```diff - import CodePush + import react_native_swiftpatch - func sourceURL(for bridge: RCTBridge!) -> URL! { + override func bundleURL() -> URL? { #if DEBUG return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") #else - return CodePush.bundleURL() + return SwiftPatchModule.getBundleURL() #endif } ``` ## Step 5: Update JavaScript Replace the CodePush HOC: ```diff - import codePush from 'react-native-code-push'; + import { withSwiftPatch } from '@swiftpatch/react-native'; - const App = codePush({ - checkFrequency: codePush.CheckFrequency.ON_APP_RESUME, - })(MyApp); + export default withSwiftPatch(MyApp, { + checkOnResume: true, + }); ``` Replace imperative API calls: ```diff - import codePush from 'react-native-code-push'; + import { useSwiftPatch } from '@swiftpatch/react-native'; - codePush.sync({ - installMode: codePush.InstallMode.ON_NEXT_RESTART, - }); + const { checkForUpdate, downloadUpdate, installUpdate } = useSwiftPatch(); + const update = await checkForUpdate(); + if (update) { + await downloadUpdate(); + await installUpdate(); + } ``` ## Step 6: Install CLI and Deploy ```bash npm install -g @swiftpatch/cli swiftpatch login swiftpatch deploy -p ios --hermes -n "Migration from CodePush" ``` :::tip That is it. Your app is now using SwiftPatch. Deploy your first update and verify it works end-to-end before removing any CodePush references from your codebase. ::: ## API Mapping Use this table as a quick reference when updating your code: | CodePush | SwiftPatch | |----------|-----------| | `codePush(options)(App)` | `withSwiftPatch(App, config)` | | `codePush.sync()` | `useSwiftPatch()` hook | | `codePush.checkForUpdate()` | `checkForUpdate()` | | `codePush.getUpdateMetadata()` | `getCurrentBundle()` | | `codePush.notifyAppReady()` | `markMounted()` (automatic) | | `codePush.restartApp()` | `restart()` | | `InstallMode.IMMEDIATE` | `InstallMode.IMMEDIATE` | | `InstallMode.ON_NEXT_RESTART` | `InstallMode.ON_NEXT_RESTART` | | `InstallMode.ON_NEXT_RESUME` | `InstallMode.ON_NEXT_RESUME` | | `CheckFrequency.ON_APP_RESUME` | `checkOnResume: true` | | `CheckFrequency.MANUAL` | Call `checkForUpdate()` manually | | `appcenter codepush release-react` | `swiftpatch deploy` | --- # Migrating to v2.0 <!-- source: docs/guides/migrating-to-v2.md --> :::info The API is fully compatible with v1.x. The only required change is updating your import paths from `swiftpatch-react-native` to `@swiftpatch/react-native`. ::: ## What is New in v2.0 - 40% faster with New Architecture support - React Native 0.76+ compatibility - TurboModules (faster native module communication) + Fabric renderer (smoother UI rendering) - Smaller bundle sizes - Better TypeScript types - Enhanced security features ## Step 1: Update Package ```bash npm install @swiftpatch/react-native@2.0.0 ``` ## Step 2: Update React Native (if needed) v2.0 requires React Native 0.76+: ```bash npm install react-native@0.76.5 ``` ## Step 3: Update Native Code **iOS:** ```bash cd ios && pod install && cd .. ``` **Android:** No additional steps needed. ## Step 4: Rebuild ```bash npx react-native run-ios npx react-native run-android ``` ## Step 5: (Optional) Enable New Architecture **iOS** (`ios/Podfile`): ```ruby ENV['RCT_NEW_ARCH_ENABLED'] = '1' ``` **Android** (`android/gradle.properties`): ```properties newArchEnabled=true ``` Then reinstall and rebuild: ```bash cd ios && pod install && cd .. npx react-native run-ios npx react-native run-android ``` :::tip Enabling New Architecture is optional but gives you the best performance. You can always enable it later. ::: ## Compatibility | SwiftPatch | React Native | React | iOS | Android | New Arch | |------------|--------------|-------|-----|---------|----------| | v2.0.0 | 0.76.0+ | 18.2.0+ | 13.4+ | API 24+ | Yes | | v1.x | 0.69.0+ | 16.8.0+ | 13.0+ | API 24+ | No | ## Testing Your Migration ### Verify Installation ```bash npx react-native run-ios # Look for: [SwiftPatch] SDK initialized successfully ``` ### Check Architecture ```tsx import { IS_TURBO_MODULE_ENABLED } from '@swiftpatch/react-native'; console.log('TurboModules enabled:', IS_TURBO_MODULE_ENABLED); ``` ## Troubleshooting ### iOS build errors ```bash cd ios rm -rf Pods Podfile.lock build pod deintegrate pod install cd .. ``` ### Android build errors ```bash cd android && ./gradlew clean && ./gradlew --stop && cd .. npx react-native run-android ``` ### Module not found Make sure you are using the scoped package name: ```tsx // Correct (v2.0) import { SwiftPatchProvider } from '@swiftpatch/react-native'; // Old (v1.x) import { SwiftPatchProvider } from 'swiftpatch-react-native'; ``` ## Performance | Operation | v1.x | v2.0 (Legacy) | v2.0 (New Arch) | |-----------|------|---------------|-----------------| | Check Update | 20ms | 15ms | 2ms | | Install Bundle | 500ms | 450ms | 250ms | | Memory Usage | 50MB | 45MB | 32MB | ## Rollback Plan If something goes wrong, you can always go back to v1.x: ```bash npm install @swiftpatch/react-native@1.x.x cd ios && pod install && cd .. ``` ## Next Steps - [New Architecture Guide](/sdk/new-architecture) -- Enable for best performance - [Configuration](/sdk/configuration) -- Explore config options - [Methods](/sdk/methods) -- Full API reference --- # Production Checklist <!-- source: docs/guides/production-checklist.md --> Run through this checklist before releasing your app with SwiftPatch. Every item matters. ## SDK Configuration - [ ] Native credentials set (`SwiftPatchAppId` in Info.plist / strings.xml) - [ ] Debug mode disabled in production - [ ] `withSwiftPatch` wraps your root component ## Native Setup - [ ] **iOS:** AppDelegate returns `SwiftPatchModule.getBundleURL()` in release mode - [ ] **iOS:** Debug mode uses Metro bundler (`#if DEBUG`) - [ ] **iOS:** `pod install` completed - [ ] **Android:** MainApplication returns `SwiftPatchModule.getJSBundleFile()` in release mode - [ ] **Android:** ProGuard rules added (if using ProGuard/R8) ## Testing - [ ] Release build tested on physical iOS device - [ ] Release build tested on physical Android device - [ ] OTA update tested end-to-end (deploy, download, install, restart) - [ ] Rollback tested -- verify auto-rollback on crash - [ ] Offline behavior tested - [ ] Differential patch verified -- second update downloads smaller patch :::tip Do not skip physical device testing. Simulators and emulators can hide real-world issues. ::: ## Security - [ ] App ID in native config (not hardcoded in JavaScript) - [ ] Bundle signing enabled (if required) - [ ] Private key stored securely (not in version control) - [ ] Public key configured in SDK (if using signing) ## CI/CD - [ ] SwiftPatch CLI installed in CI - [ ] CI token created and stored as CI secret - [ ] Deploy command configured with `--hermes` - [ ] Both platforms covered :::warning Never hardcode tokens in your workflow files. Always use your CI platform's secrets management. ::: ## Monitoring - [ ] Dashboard accessible to the team - [ ] Error tracking integrated (Sentry, Bugsnag, etc.) - [ ] Rollback alerts configured - [ ] Team members have appropriate roles ## Release Process - [ ] Rollout strategy defined (start at 5-10%, then ramp) - [ ] Rollback plan documented - [ ] Target app version matches native binary exactly - [ ] Hermes compilation enabled (`--hermes` flag) - [ ] Working directory clean before deploying ## Team - [ ] Team knows how to deploy (`swiftpatch deploy`) - [ ] Team knows how to rollback (dashboard or CLI) - [ ] Team knows how to monitor rollouts (dashboard analytics) :::info Once everything is checked off, you are ready to go. Ship it. ::: --- # Staged Rollouts <!-- source: docs/guides/staged-rollouts.md --> # Staged Rollouts Guide Ship updates safely by rolling them out to a small group first, then widening the audience once you confirm stability. ## Step 1: Deploy Your Update ```bash swiftpatch deploy -p ios --hermes -n "v1.2.0 - New checkout flow" ``` Your release starts at whatever rollout percentage you set (default is 100%). ## Step 2: Set the Rollout Percentage After deploying, adjust the rollout from the dashboard: **Step 1:** Go to your app's **Releases** page. **Step 2:** Click on the release. **Step 3:** Adjust the **Rollout Percentage**. :::info Recommended stages - **1-5%** -- Canary. Monitor for 1-2 hours. - **10-25%** -- Broader test. Monitor for 4-8 hours. - **50%** -- Half of users. Monitor for 24 hours. - **100%** -- Full release. ::: ## Step 3: Monitor Check the dashboard for: - Crash rate - Rollback events - Error logs :::warning If anything looks wrong, pause the rollout immediately. You can always resume later. ::: ## Step 4: Rollback if Needed ```bash swiftpatch rollback -a <app-id> -p ios ``` Or click **Rollback** on the release in the dashboard. ## Automated Deploy with Manual Rollout A common pattern: let CI deploy automatically, then manage rollout manually from the dashboard. ```yaml title=".github/workflows/deploy.yml" name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: npm ci - run: npm install -g @swiftpatch/cli - name: Deploy run: | swiftpatch deploy \ -p ios \ --hermes \ -n "Release from ${{ github.sha }}" \ --ci-token ${{ secrets.SWIFTPATCH_CI_TOKEN }} ``` ## Best Practices - **Start small** (1-5%) every time -- no exceptions - **Wait between stages** for issues to surface - **Monitor crash rates** and user reports actively - **Have a rollback plan** ready before you start - **Communicate with QA** about each stage --- # Testing Updates <!-- source: docs/guides/testing-updates.md --> Before you ship an OTA update to production, test it end-to-end in a development environment. This guide walks you through the process. ## Step 1: Enable Debug Mode Turn on verbose logging so you can see exactly what the SDK is doing: ```tsx export default withSwiftPatch(App, { debug: true, }); ``` ## Step 2: Build a Release Version You need a release build -- debug builds use Metro bundler and skip SwiftPatch entirely. ```bash # iOS npx react-native run-ios --mode Release # Android npx react-native run-android --mode release ``` ## Step 3: Deploy a Test Update ```bash swiftpatch deploy -p ios --hermes -n "Test update" ``` ## Step 4: Verify - Open the app - The SDK detects and downloads the update - Restart to apply - Confirm your changes are visible :::tip Watch the console logs for `[SwiftPatch]` entries to follow the update lifecycle in real time. ::: ## Testing Scenarios ### Rollback **Step 1:** Deploy an update that crashes on launch. **Step 2:** Open the app -- it crashes. **Step 3:** Open again -- the SDK rolls back automatically. **Step 4:** Verify the app runs on the previous version. **Step 5:** Check the dashboard for a `ROLLBACK_PROD` event. ### Mandatory Updates **Step 1:** Deploy with `--mandatory`: ```bash swiftpatch deploy -p ios --hermes --mandatory ``` **Step 2:** Verify `availableUpdate.isMandatory` is `true`. **Step 3:** If using `InstallMode.IMMEDIATE`, the app restarts automatically. ### Offline Behavior **Step 1:** Install the app with network connectivity. **Step 2:** Disable network. **Step 3:** Verify the app works with the current bundle. **Step 4:** Re-enable network and verify update checking resumes. ### Differential Patches **Step 1:** Deploy an initial update (the first one is always a full download). **Step 2:** Make a small change and deploy again. **Step 3:** Check `availableUpdate.isPatch` -- it should be `true`. **Step 4:** Verify the download size is much smaller than the full bundle. ## Debug Logging Check the native logs for detailed SDK output: ```bash # iOS xcrun simctl spawn booted log show --predicate 'subsystem == "com.swiftpatch"' --last 5m # Android adb logcat -s SwiftPatch ``` Expected log output: ``` [SwiftPatch] Checking for updates... [SwiftPatch] Update available: v1.2.0 [SwiftPatch] Downloading patch: 204800 bytes [SwiftPatch] Download complete [SwiftPatch] Bundle hash verified [SwiftPatch] Update installed, restart required ``` --- # Changelog <!-- source: docs/reference/changelog.md --> Every Swiftpatch release. [Subscribe to updates →](https://twitter.com/swiftpatch) ## v1.0.0 — 2026-05 (Public Beta) **Shipped** - Three-layer brick protection: boot counter + three-slot layout + store-bundle floor - Delta patches via bsdiff (60–98% smaller downloads) - Ed25519 bundle signing with rotatable trust root - AI crash root-cause + `swiftpatch pr` GitHub App integration - Opinionated UI: `<UpdateBanner>`, `<UpdateBlocker>`, `<UpdateOnboarding>` - Android WorkManager + iOS BGTaskScheduler background sync - Silent push (APNs + FCM) for instant delivery - Expo config plugin for managed workflow - First-class channels + white-label tenant switching - Sentry / Bugsnag / Crashlytics breadcrumb adapters - CLI: `init`, `deploy`, `doctor`, `status`, `migrate`, `pr`, `unlink`, `gkp` **Migration** - CodePush users: see [Migrating from CodePush](../guides/migrating-from-codepush). - Expo Updates users: run `swiftpatch migrate from expo-updates`. --- # FAQ <!-- source: docs/reference/faq.md --> # Frequently Asked Questions ## General ### What is SwiftPatch? SwiftPatch is an OTA (over-the-air) deployment platform for React Native. It lets you push JavaScript bundle updates directly to your users' devices -- no app store review, no waiting. ### Which platforms are supported? iOS and Android. ### What can I update with OTA? **You can update:** - JavaScript code - Images and assets bundled with JS - JSON configuration files **You cannot update:** - Native modules (Objective-C, Swift, Java, Kotlin) - Native dependencies (CocoaPods for iOS, Gradle for Android) - App icons, splash screens, or other native resources - The React Native version itself :::tip A good rule of thumb: if it is written in JavaScript, you can update it OTA. If it requires `pod install` or a Gradle sync, you need an app store update. ::: ### Is SwiftPatch a CodePush replacement? Yes. SwiftPatch is a modern alternative to Microsoft CodePush, which has been retired with AppCenter. See the [Migration Guide](/guides/migrating-from-codepush). ### Does SwiftPatch comply with App Store guidelines? Yes. Both Apple and Google allow over-the-air JavaScript updates as long as they do not change the app's primary purpose. SwiftPatch only updates the JavaScript bundle, which is explicitly allowed. ## Technical ### How large are updates? SwiftPatch uses differential patching. Updates are typically 98% smaller than the full bundle -- a typical patch is 50KB-500KB. ### How fast are updates delivered? Most updates download in under 1 second on a good connection. The patch applies locally in milliseconds. ### What happens if an update crashes the app? SwiftPatch has built-in crash detection. If the app crashes after an update, it automatically rolls back to the previous working version on the next launch. No user action needed. ### Does SwiftPatch work offline? Yes. The app runs with its current bundle when offline. Updates are checked when connectivity returns. ### Can I target specific app versions? Yes. Each release can specify a target binary version range using semver (e.g., `>=1.0.0 <2.0.0` targets all 1.x versions). ### How does bundle signing work? Bundle signing uses RSA key pairs to verify updates come from an authorized source. The CLI signs bundles with your private key, and the SDK verifies them with your public key. See [Bundle Signing](/concepts/bundle-signing). ## Billing ### What plans are available? | Plan | Price | MAU | Apps | |------|-------|-----|------| | **Free** | $0/mo | 5,000 | 2 | | **Indie** | $19/mo | 25,000 | 5 | | **Team** | $79/mo | 200,000 | 15 | | **Business** | $249/mo | 750,000 | 50 | | **Enterprise** | Custom | Unlimited | Unlimited | See [Limits](/reference/limits) for a full comparison. ### Is there a free plan? Yes. The Free plan includes 2 apps, 5,000 MAU, 3 team members, and 1 GB of storage. No credit card required. ### What counts as MAU? A monthly active user (MAU) is any unique device that checks for or downloads a deploy within a billing cycle. ### What happens if I exceed my plan limits? You receive a warning email at 80% of your MAU quota. At 100%, new deploy checks return "up to date" until the next billing cycle or until you upgrade. Existing installs continue to work normally. ## Support ### How do I get help? - **Documentation**: You are reading it - **Discord**: [Community Discord](https://discord.gg/swiftpatch) - **GitHub Issues**: [Report bugs](https://github.com/codewprincee/react-native-swiftpatch/issues) - **Email**: Indie, Team, and Business plans include email support at support@swiftpatch.io - **Priority Support**: Available on Business and Enterprise plans --- # Limits <!-- source: docs/reference/limits.md --> ## Plan Comparison | Feature | Free | Indie | Team | Business | Enterprise | |---------|------|-------|------|----------|------------| | **Price** | $0/mo | $19/mo | $79/mo | $249/mo | Custom | | Monthly Active Users (MAU) | 5,000 | 25,000 | 200,000 | 750,000 | Unlimited | | Apps | 2 | 5 | 15 | 50 | Unlimited | | Team members | 3 | 3 | 10 | 25 | Unlimited | | Storage | 1 GB | 5 GB | 25 GB | 100 GB | Unlimited | | Analytics retention | 7 days | 30 days | 90 days | 180 days | 365 days | | Bandwidth | 25 GB | 100 GB | 1 TB | 5 TB | Unlimited | | Bundle size | 50 MB | 100 MB | 200 MB | 500 MB | 500 MB | | API requests/min | 60 | 300 | 600 | 1,200 | Custom | ## What These Mean ### Monthly Active Users (MAU) A unique device that checks for or downloads a deploy within a billing cycle. Devices that check in but receive no new deploy still count toward your quota. ### Bundle Size Maximum size of a single JavaScript bundle. Differential patches are much smaller than the full bundle. ### API Rate Limits Per API key, per minute. If you hit the limit, requests return a `429` status until the window resets. ### Analytics Retention How long analytics data is kept. Older releases are archived but bundles remain available. ## Exceeding Limits ### Soft Limits You receive email notifications at 80% and 90% of your monthly MAU quota. ### Hard Limits At 100% of your MAU quota: - New deploy checks return "up to date" - Existing installed deploys continue to work - Dashboard and API remain accessible :::info Your app will not break if you hit the limit. Users just will not receive new updates until the next billing cycle or until you upgrade. ::: ### Upgrading Upgrade at any time in **Settings > Billing**. New limits take effect immediately. ## Enterprise Need higher limits? [Contact us](mailto:enterprise@swiftpatch.io) for a custom Enterprise plan with: - Unlimited everything - Custom SLA - Dedicated support - On-premise deployment option - SSO/SAML integration --- # Security <!-- source: docs/reference/security.md --> ## Infrastructure ### Data in Transit All data is encrypted in transit using TLS 1.3: - API requests and responses - Bundle uploads and downloads - SDK communication - Dashboard access ### Data at Rest All data is encrypted at rest using AES-256: - JavaScript bundles on the CDN - Database records - API keys and secrets - Analytics data ### Hosting - AWS with SOC 2 compliant infrastructure - Multi-region deployment for high availability - Automated backups with point-in-time recovery - DDoS protection via Cloudflare ## Application Security ### Authentication - API keys use cryptographically secure random generation - Dashboard uses OAuth 2.0 with PKCE (a secure login flow that prevents token interception) - Session tokens expire after 24 hours of inactivity - Two-factor authentication (2FA) available ### Authorization - Role-based access control (RBAC) for team members - Per-app access restrictions - API key permissions (read, write, admin) - Deployment keys have read-only access :::tip Always assign the most restrictive role that lets a person do their job. See [Team Management](/dashboard/team) for role details. ::: ## Bundle Security ### Hash Verification Every bundle has a SHA-256 hash computed at upload time. The SDK verifies the hash after downloading and applying patches. If the hash does not match, the update is rejected. ### Bundle Signing (Optional) For additional security, enable RSA bundle signing: - Sign bundles with your private key during publishing - Verify signatures with your public key in the SDK - Prevents unauthorized code execution even if the CDN is compromised See [Bundle Signing](/concepts/bundle-signing) for setup instructions. :::info Hash verification is always active and cannot be disabled. Bundle signing adds an extra layer of trust on top. ::: ## Vulnerability Reporting If you discover a security vulnerability, report it responsibly: - **Email**: security@swiftpatch.io - Do not disclose publicly until we have investigated and fixed the issue We aim to acknowledge reports within 24 hours and provide a fix timeline within 72 hours. ## Compliance - **SOC 2 Type II** -- in progress - **GDPR** -- data processing agreements available - **HIPAA** -- available on Enterprise plans with BAA (Business Associate Agreement) - **App Store Guidelines** -- compliant with Apple and Google OTA update policies ## Best Practices 1. **Rotate API keys regularly** -- do not let keys go stale 2. **Use minimum permissions** for API keys 3. **Enable bundle signing** for production apps 4. **Enable 2FA** for all team members 5. **Monitor audit logs** in the dashboard 6. **Keep the SDK updated** for the latest security patches --- # Android Setup <!-- source: docs/sdk/android-setup.md --> The automatic way is one command. The manual way is a short snippet you paste into `MainApplication`. Both are documented here. ## The automatic path ```bash npx swiftpatch init ``` That's it. Move on to [wrapping your app](/sdk/provider). Under the hood, `swiftpatch init` modifies two Android files. Every change is bounded by `// swiftpatch-begin` / `// swiftpatch-end` markers, so `swiftpatch unlink` reverses them cleanly. ### `MainApplication.kt` (Kotlin — RN 0.73+) ```kotlin title="android/app/src/main/java/.../MainApplication.kt" import com.swiftpatch.SwiftPatchModule class MainApplication : Application(), ReactApplication { override val reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) { // ... your existing packages, dev support, etc. // swiftpatch-begin (auto-generated, do not edit between markers) override fun getJSBundleFile(): String? { return SwiftPatchModule.getJSBundleFile(applicationContext) ?: super.getJSBundleFile() } // swiftpatch-end } } ``` For RN 0.82+ projects using `ReactHost`: ```kotlin override val reactHost: ReactHost by lazy { getDefaultReactHost( context = applicationContext, packageList = PackageList(this).packages, // swiftpatch-begin (auto-generated, do not edit between markers) jsBundleFilePath = SwiftPatchModule.getJSBundleFile(applicationContext), // swiftpatch-end ) } ``` The SDK's override calls `super.getJSBundleFile()` if no OTA bundle is available. This is critical — if Swiftpatch has nothing to serve, the default store bundle loader runs unchanged. ### `MainApplication.java` (legacy) ```java title="android/app/src/main/java/.../MainApplication.java" import com.swiftpatch.SwiftPatchModule; public class MainApplication extends Application implements ReactApplication { private final ReactNativeHost mReactNativeHost = new DefaultReactNativeHost(this) { // ... your existing overrides // swiftpatch-begin (auto-generated, do not edit between markers) @Override protected String getJSBundleFile() { String path = SwiftPatchModule.getJSBundleFile(getApplicationContext()); if (path != null) return path; return super.getJSBundleFile(); } // swiftpatch-end }; } ``` ### `strings.xml` ```xml title="android/app/src/main/res/values/strings.xml" <resources> <string name="app_name">YourApp</string> <!-- swiftpatch-begin (auto-generated, do not edit between markers) --> <string name="swiftpatch_deployment_key">dep_xxx...</string> <!-- swiftpatch-end --> </resources> ``` :::info Key casing Android uses the snake-case form `swiftpatch_deployment_key`. iOS uses the PascalCase form `SwiftPatchDeploymentKey`. The SDK reads from both. ::: ## Manual setup If auto-patching can't run, use `--manual`: ```bash npx swiftpatch init --manual ``` The CLI prints the snippets. Steps to apply by hand: ### 1. Paste the `getJSBundleFile` override From the Kotlin / Java example above. ### 2. Add the deployment key to `strings.xml` ```xml <string name="swiftpatch_deployment_key">dep_xxx...</string> ``` ### 3. (Optional) Wire silent-push-triggered sync If you want FCM data messages to trigger background update checks: ```kotlin override fun onMessageReceived(message: RemoteMessage) { if (message.data["swiftpatch_event"] != null) { SilentPushReceiver.handleData(this, message.data) return } // ... your existing handler } ``` See [Background sync](/sdk/background-sync). ## Minimum Android version API 24 (Android 7.0) or higher. WorkManager (used for scheduled background sync) requires 14+, which is always true on API 24. Modern React Native templates default to API 24. ## ProGuard / R8 The SDK ships its own consumer ProGuard rules. No manual setup required. If you've heavily customized your `proguard-rules.pro`, ensure the following packages are not obfuscated: ```proguard -keep class com.swiftpatch.** { *; } -keepnames class com.swiftpatch.** ``` ## New Architecture Fully supported. Set `newArchEnabled=true` in `android/gradle.properties`. See [New Architecture](/sdk/new-architecture). ## Troubleshooting - **"Cannot resolve symbol SwiftPatchModule"** — autolinking didn't pick up the package. Run `cd android && ./gradlew clean` and rebuild. - **App loads the old bundle after update** — the `getJSBundleFile()` override is missing or misplaced. Run `swiftpatch doctor` to verify. - **WorkManager crash on boot** — you're on a very old minSdkVersion. Raise to 24. For more, see [Troubleshooting](/sdk/troubleshooting). ## Next steps - [iOS setup](/sdk/ios-setup) — the iOS equivalents. - [Provider reference](/sdk/provider) — wrap your app. - [Background sync](/sdk/background-sync) — WorkManager + silent push. --- # Background Sync <!-- source: docs/sdk/background-sync.md --> Swiftpatch can check for updates and download bundles while your app is backgrounded. Two mechanisms are available — use whichever suits your app: - **Periodic background sync** — the SDK wakes up on a schedule (WorkManager on Android, `BGTaskScheduler` on iOS). - **Silent-push-triggered sync** — your backend sends a silent push, the SDK checks + downloads on receipt. Both require zero extra native code once `swiftpatch init` has run. For periodic sync, the permissions and identifiers are already in `Info.plist` and `AndroidManifest.xml`. For silent push, you wire three lines into your existing push handler. ## Periodic background sync ### iOS — `BGTaskScheduler` `swiftpatch init` adds the required entries to `Info.plist`: ```xml <key>BGTaskSchedulerPermittedIdentifiers</key> <array> <string>com.swiftpatch.bg-refresh</string> </array> <key>UIBackgroundModes</key> <array> <string>fetch</string> <string>processing</string> </array> ``` Under the hood the SDK submits a `BGAppRefreshTaskRequest` identified by `com.swiftpatch.bg-refresh`. iOS decides when to actually run it — typically when the device is on Wi-Fi, charging, and not in low-power mode. iOS gives the task ~30 seconds of CPU time. The SDK uses that window to: 1. Call `/sdk/check-update`. 2. If an update is available, download it to the NEW slot. 3. Verify signature and hash. 4. Leave the slot ready. It applies on the next cold start or resume. ### Android — WorkManager On Android, the SDK schedules a `PeriodicWorkRequest` via WorkManager with WiFi + battery-not-low constraints by default. No manifest changes required — `swiftpatch init` added the dependency and the required permissions. Like iOS, the OS decides when to run the work. Typical cadence is once per 15–60 minutes under favorable conditions. ### Triggering from your app The SDK schedules and un-schedules itself based on configuration. You don't need to call it manually in most cases. Background sync activates when: - The SDK has been initialized successfully. - The app has been foregrounded at least once. - The device meets the OS constraints (Wi-Fi + battery on iOS; WiFi-NOT_REQUIRED + battery-not-low on Android by default). To explicitly stop background sync (for example, during a user's logout flow), clear state via the tenant-switch API: ```ts await sp.setDeploymentKey(newKey, { clearCurrentBundle: true }); ``` ## Silent-push-triggered sync When you ship a critical release, you probably don't want to wait for the OS to decide to wake your app. A silent push will do the trick in seconds. ### iOS (APNs) 1. Enable the **Background Modes → Remote notifications** capability in Xcode. 2. `Info.plist` already has `UIBackgroundModes` → `fetch` from `swiftpatch init`. Add `remote-notification`: ```xml <key>UIBackgroundModes</key> <array> <string>fetch</string> <string>processing</string> <string>remote-notification</string> </array> ``` 3. In your `AppDelegate`, forward silent pushes to the SDK: ```swift title="ios/YourApp/AppDelegate.swift" import SwiftPatch func application( _ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void ) { SilentPushHandler.handleRemoteNotification( userInfo, completionHandler: completionHandler ) } ``` 4. Send the payload from your backend with `aps.content-available = 1`: ```json { "aps": { "content-available": 1 }, "swiftpatch": { "event": "check", "appId": "app_xxx", "releaseHash": "abc123..." } } ``` `SilentPushHandler.parsePayload(...)` accepts either the nested `swiftpatch` object or flat top-level keys (`swiftpatch_event`, `swiftpatch_app_id`, `swiftpatch_release_hash`). ### Android (FCM) Swiftpatch does **not** take a hard dependency on `firebase-messaging`. Forward from your existing `FirebaseMessagingService`: ```kotlin title="MyFirebaseService.kt" import com.swiftpatch.SilentPushReceiver override fun onMessageReceived(message: RemoteMessage) { if (message.data["swiftpatch_event"] != null) { SilentPushReceiver.handleData(this, message.data) return } // Your existing handler } ``` FCM data-message keys: | Key | Value | |---|---| | `swiftpatch_event` | `check` or `new-release` | | `swiftpatch_app_id` | Your app id (optional verification). | | `swiftpatch_release_hash` | Target release hash (optional, for hints). | ## Which should I use? | | Periodic sync | Silent push | |---|---|---| | Delivery time | Minutes to hours | Seconds | | OS interference | High — OS decides | Low | | Requires a push server | No | Yes (APNs + FCM) | | Good for | Keeping devices current quietly | Critical releases | | Dependencies | None | APNs certs + FCM | Many apps run both: periodic sync keeps most users current passively, silent push pokes everyone on the rare critical release. ## Telemetry Background sync fires the same lifecycle events as foreground checks. In your breadcrumb stream (see [Integrations](/sdk/integrations)) you'll see a `type: 'silentPush'` or `type: 'bgRefresh'` tag on the originating event. ## Troubleshooting - **iOS periodic task never fires** — iOS is extremely conservative. Ensure the device has been on Wi-Fi and charging for several hours. You can force-run the task via Xcode's scheme editor for testing. - **Android periodic task never fires** — verify battery saver / Doze isn't suppressing the worker. WorkManager's `PeriodicWorkRequest` minimum is 15 minutes; anything shorter is ignored. - **Silent push not received on iOS** — confirm the payload includes `"aps": { "content-available": 1 }` AND you've enabled "Remote notifications" background mode. - **Silent push not received on Android** — verify `onMessageReceived` is called with data messages (not notification messages). Notification messages are not delivered in the background. ## Next steps - [Integrations](/sdk/integrations) — forward background-sync events to your crash reporter. - [Channels](/sdk/channels) — background sync respects the device's active channel. - [Events](/sdk/events) — subscribe to lifecycle events fired from background contexts. --- # Channels <!-- source: docs/sdk/channels.md --> A **channel** is a named release track. Each device subscribes to exactly one channel; releases published to a channel are only visible to devices on that channel. Think of channels as concurrent, independent release lines. ## Default channels `swiftpatch init` creates three channels for you: | Channel | Purpose | Typical source | |---|---|---| | `dev` | Developer builds, noisy, no rollout gates | Local laptops | | `staging` | QA + internal testers | CI on merge to `develop` | | `production` | Real users | CI on tagged release | You can rename, delete, or add more at any time. Most teams add `beta` for external testers and short-lived channels for major feature rollouts. ## Setting the channel at runtime ```ts import { setChannel, getChannel, onChannelChanged } from '@swiftpatch/react-native'; // Persist to native storage — survives cold starts await setChannel('beta'); // Read the current channel const channel = await getChannel(); // Subscribe to changes const unsub = onChannelChanged((channel) => { console.log('Channel is now:', channel); }); ``` The channel is included in every `checkForUpdate` request. The server responds with releases targeted at that channel only. ## Setting the channel at build time If you bundle channel-specific binaries (for example, an alpha APK that always runs the `alpha` channel), gate `setChannel` behind a flag: ```ts import { setChannel } from '@swiftpatch/react-native'; if (Config.BUILD_CHANNEL) { await setChannel(Config.BUILD_CHANNEL); } ``` ## Promotion between channels The most common flow is staging → production. Do it from the CLI: ```bash # Ship to staging swiftpatch deploy --channel staging -p ios --hermes # After QA signs off, promote the exact release bytes to production swiftpatch releases promote r_abc123 --from staging --to production --rollout 10 ``` Promotion does **not** rebundle. The bytes that were tested on staging are the bytes that reach production. This eliminates "works on staging, breaks on prod" entirely. ## Per-device channel switching (beta program) Build a beta opt-in UI in your app's settings: ```tsx import { setChannel } from '@swiftpatch/react-native'; import { Button } from 'react-native'; function JoinBeta() { return ( <Button title="Join the beta program" onPress={async () => { await setChannel('beta'); Alert.alert('You are now on the beta channel. Beta updates will arrive automatically.'); }} /> ); } ``` The next `checkForUpdate` call (triggered on resume or by your UI) will look for beta releases. No restart required. ## Channel policies Each channel can have policies attached from the dashboard. Example: | Policy | Meaning | |---|---| | `require-approvers` | Number of dashboard approvers required before a release goes active. | | `default-rollout` | If `--rollout` is not specified on deploy, use this percent. | | `auto-pause-crash-rate` | Percent crash-rate delta above baseline that triggers an automatic rollout pause. | | `allow-mandatory` | Whether releases on this channel may set `--mandatory`. | | `max-size` | Reject bundles larger than N MB. | ## Channel isolation - Deployment keys are **channel-scoped**. A key for `staging` cannot read `production`. - CI tokens can be scoped to specific channels: `swiftpatch ci-tokens create --channels staging,beta`. - Releases do not leak across channels. ## Common patterns ### Feature-flag channel for a launch Ship a new, experimental version behind a channel named after the feature: ```bash swiftpatch channels create checkout-v2 -a my-app swiftpatch deploy --channel checkout-v2 -p ios --hermes ``` In-app, users who have opted in run: ```ts await setChannel('checkout-v2'); ``` When ready for everyone: ```bash swiftpatch releases promote r_xyz --from checkout-v2 --to production --rollout 5 ``` ### Emergency rollback via channel If production has a bad release and you need to freeze updates for a subset of users: ```bash # Create a temporary "hold" channel swiftpatch channels create production-hold -a my-app # Have your app switch the stuck users over via a feature flag # await setChannel('production-hold'); # Ship the fix to production-hold swiftpatch deploy --channel production-hold -p ios --hermes # Once the fix is verified, promote to production swiftpatch releases promote r_fix --from production-hold --to production ``` ## Next steps - [Rollouts](/concepts/rollouts) — percent-based gradual rollouts. - [White-label](/sdk/white-label) — switch the entire deployment key at runtime for multi-tenant apps. - [CLI channels](/cli/commands) — `swiftpatch channels create`, `list`, `delete`. --- # Configuration <!-- source: docs/sdk/configuration.md --> Swiftpatch reads configuration from three places, in precedence order: 1. **Props passed to `<SwiftPatchProvider config={...}>` in JavaScript** (highest precedence). 2. **Native config** — `Info.plist` on iOS, `strings.xml` on Android. Written by `swiftpatch init`. 3. **Defaults** — sensible for 95% of apps. You usually only need to touch the Provider config. Native values come from `swiftpatch init`. ## The minimum config ```tsx <SwiftPatchProvider config={{ debug: __DEV__ }}> <App /> </SwiftPatchProvider> ``` That's it. `deploymentKey` is optional when `swiftpatch init` has written it to the native config. ## `SwiftPatchUserConfig` — the three public options ```ts interface SwiftPatchUserConfig { deploymentKey?: string; debug?: boolean; onUpdate?: (release: ReleaseInfo) => void; } ``` | Option | Default | Description | |---|---|---| | `deploymentKey` | `null` | Channel-scoped read key. Omit if `swiftpatch init` embedded it in native config. | | `debug` | `false` | Emits verbose SDK logs via `console.log` / native logcat / NSLog. Do not enable in production. | | `onUpdate` | `undefined` | Called once per new release with the `ReleaseInfo` payload. Useful for triggering custom UI or analytics. | ## Advanced options (full `SwiftPatchConfig`) Expose these through the Provider's `config` prop. All are optional and internal-but-stable: ```ts interface SwiftPatchConfig extends SwiftPatchUserConfig { appId?: string; serverUrl?: string; checkOnResume?: boolean; checkInterval?: number; installMode?: InstallMode; mandatoryInstallMode?: InstallMode; customHeaders?: Record<string, string>; publicKey?: string; autoStabilizeAfterLaunches?: number; crashDetectionWindowMs?: number; maxCrashesBeforeRollback?: number; autoStagingInDev?: boolean; } ``` | Option | Default | Description | |---|---|---| | `appId` | `null` | Stable app identifier from the dashboard. When set, the SDK auto-resolves (and auto-rotates) the deployment key from the server. | | `serverUrl` | `https://api.swiftpatch.io/api/v1` | Override the backend (self-hosted or staging). | | `checkOnResume` | `true` | Check for updates every time the app foregrounds. | | `checkInterval` | `0` | Minimum ms between checks. `0` means check on every resume. | | `installMode` | `ON_NEXT_RESUME` | When to apply a ready non-mandatory update. See [install modes](#install-modes). | | `mandatoryInstallMode` | `IMMEDIATE` | When to apply a ready mandatory update. | | `publicKey` | `null` (reads from `Info.plist` / `strings.xml`) | Override the Ed25519 verifier public key. | | `autoStabilizeAfterLaunches` | `2` | Promote `NEW` slot to `STABLE` after N crash-free cold starts. | | `crashDetectionWindowMs` | `10_000` | How long after launch a crash counts against the freshly-applied bundle. | | `maxCrashesBeforeRollback` | `2` | Number of crashes before auto-rollback triggers. | | `autoStagingInDev` | `false` | Force the `STAGE` environment when `__DEV__` is true. | ## Install modes ```ts enum InstallMode { IMMEDIATE = 'immediate', // Restart now ON_NEXT_RESTART = 'onNextRestart', // Apply on next cold start ON_NEXT_RESUME = 'onNextResume', // Apply when app foregrounds } ``` Defaults are tuned for UX: - **Non-mandatory** updates use `ON_NEXT_RESUME` — the user sees the update the next time they come back. - **Mandatory** updates use `IMMEDIATE` — restart the moment the download finishes. ## Native config `swiftpatch init` writes these into your native files automatically. ### iOS — `Info.plist` ```xml <key>SwiftPatchDeploymentKey</key> <string>dep_xxx...</string> <key>BGTaskSchedulerPermittedIdentifiers</key> <array> <string>com.swiftpatch.bg-refresh</string> </array> <key>UIBackgroundModes</key> <array> <string>fetch</string> <string>processing</string> </array> ``` Optional extras: | Key | Purpose | |---|---| | `SwiftPatchProjectId` | Project ID. Optional. | | `SwiftPatchServerUrl` | Custom API endpoint. | | `SwiftPatchPublicKey` | Public key for signature verification. | | `SwiftPatchAppId` | Stable app id for key auto-resolution. | ### Android — `strings.xml` ```xml <resources> <string name="swiftpatch_deployment_key">dep_xxx...</string> <!-- Optional --> <string name="SwiftPatchProjectId">proj_xxx</string> <string name="SwiftPatchServerUrl">https://api.swiftpatch.io/api/v1</string> <string name="SwiftPatchPublicKey">...</string> </resources> ``` ## `.swiftpatchrc` Per-project config for the CLI. Commit this file — it does **not** contain secrets. ```json title=".swiftpatchrc" { "appId": "app_abc123", "app": "my-app", "platform": "ios", "signingKey": "./.swiftpatch/keys/private.pem" } ``` | Field | Description | |---|---| | `appId` | Stable app id from the dashboard. | | `app` | App slug for human-friendly logs. | | `platform` | `ios` \| `android` — default platform for `swiftpatch deploy`. | | `signingKey` | Path to the Ed25519 private key. | :::warning Keep the private key out of Git `.swiftpatch/` is added to `.gitignore` by `swiftpatch init`. Never commit `private.pem`. For CI, store the key as an encrypted secret — see [CI/CD](/cli/ci-cd). ::: ## Verifying the native config ```ts import { getNativeConfig } from '@swiftpatch/react-native'; const cfg = await getNativeConfig(); // { // projectId: string | null, // appToken: string | null, // appId: string | null, // deploymentKey: string | null, // serverUrl: string | null, // publicKey: string | null, // } ``` Use this in a `useEffect` to verify `swiftpatch init` ran correctly. ## Next steps - [Provider reference](/sdk/provider) — opinionated UI props (`autoShowBanner`, `theme`, ...). - [Methods](/sdk/methods) — the imperative `Swiftpatch.*` surface. - [CLI CI/CD](/cli/ci-cd) — configure production deploys. <!-- swiftpatch-diagram-begin --> ## Architecture The SDK is a state machine. Each transition maps to a public method or an internal verification step — and every failure path returns to `Idle` without touching the running bundle. ```mermaid stateDiagram-v2 [*] --> Idle Idle --> Checking: checkForUpdate() Checking --> Idle: no update / incompatible Checking --> Downloading: update available Downloading --> Verifying: bytes complete Verifying --> Idle: signature fail -> rollback Verifying --> Ready: hash + sig verified Ready --> Applying: applyNow() or auto Ready --> Idle: dismissUpdate() Applying --> Restarted: restart() Restarted --> Idle: markMounted() ``` <!-- swiftpatch-diagram-end --> --- # Error Codes <!-- source: docs/sdk/error-codes.md --> The Swiftpatch SDK emits errors with stable, namespaced codes prefixed `SWIFTPATCH_*`. Every code is stable across minor versions — you can safely build dashboards, alerting rules, and user-facing error maps against them. ## Detecting programmatically ```ts import { SwiftPatch, getKnownErrors, getErrorDescription } from '@swiftpatch/react-native'; // Static — safe to call before the Provider mounts const codes = SwiftPatch.getKnownErrors(); // ['SWIFTPATCH_DOWNLOAD_FAILED', 'SWIFTPATCH_HASH_MISMATCH', ...] // Human-readable recovery copy const info = getErrorDescription('SWIFTPATCH_SIGNATURE_INVALID'); // { category, description, troubleshooting } ``` Subscribe to runtime errors via the event bus: ```ts import { Swiftpatch } from '@swiftpatch/react-native'; Swiftpatch.onError((error) => { const { code, message, details } = error; const info = getErrorDescription(code); reportToSentry({ code, message, category: info.category }); }); ``` :::tip CI check Fail CI if your error-mapping table drops a code the new SDK started emitting: ```ts const mapped = new Set(Object.keys(MY_USER_FACING_MESSAGES)); for (const code of SwiftPatch.getKnownErrors()) { if (!mapped.has(code)) throw new Error(`Unmapped SDK error: ${code}`); } ``` ::: ## Network / download ### `SWIFTPATCH_DOWNLOAD_FAILED` Generic download failure (DNS, TCP reset, TLS handshake). **Fires when** the bundle or patch URL cannot be fetched. **Recovery** Retry with backoff. The SDK retries automatically on the next resume check. ### `SWIFTPATCH_DOWNLOAD_HTTP_ERROR` Server returned a non-2xx HTTP status. **Fires when** `DownloadManager` (Android) / `URLSession` (iOS) receives an error response code. **Recovery** Check the URL signature hasn't expired. If using a CDN, verify the release is published. ### `SWIFTPATCH_DOWNLOAD_EMPTY` Downloaded file has zero bytes. **Fires when** the server responded 200 OK but the body was empty. **Recovery** Re-check the release upload on the server side. ### `SWIFTPATCH_DOWNLOAD_CANCELLED` User or SDK called `cancelDownload()` while the transfer was in flight. **Fires when** a check is superseded by a newer one, or on component unmount. **Recovery** Usually not user-facing; surface as a progress-cancelled event. ### `SWIFTPATCH_RANGE_NOT_SUPPORTED` Server refused a `Range:` resume request. **Fires when** the SDK tried to resume an interrupted download and the server responded without `Accept-Ranges: bytes`. **Recovery** The SDK falls back to a full re-download automatically. ### `SWIFTPATCH_NETWORK_UNREACHABLE` No network connectivity. **Fires when** the JS layer detects `NetInfo.isConnected === false` before a check. **Recovery** Wait for connectivity and retry on `appState === 'active'`. ## Signature / integrity ### `SWIFTPATCH_HASH_MISMATCH` SHA-256 of the downloaded bundle does not match the expected hash. **Fires when** the bundle has been truncated, corrupted, or tampered with. **Recovery** The SDK deletes the temp file and retries on the next check. ### `SWIFTPATCH_ARCHIVE_HASH_MISMATCH` Zip archive hash (before extraction) does not match the manifest value. **Fires when** verifying the outer zip against the release manifest. **Recovery** Same as `SWIFTPATCH_HASH_MISMATCH` — retry. ### `SWIFTPATCH_FILE_HASH_MISMATCH` A single file inside the extracted archive has an unexpected hash. **Fires when** manifest-level per-file hashes don't match the unpacked bytes. **Recovery** Delete the partially extracted slot and redownload. ### `SWIFTPATCH_FILE_SIZE_MISMATCH` A file inside the archive has an unexpected byte length. **Fires when** zip-entry size disagrees with the manifest declaration. **Recovery** Treat as tampering; redownload and alert the SDK channel. ### `SWIFTPATCH_SIGNATURE_INVALID` Ed25519 signature verification of the bundle or manifest failed. **Fires when** the signed payload does not validate under the configured `publicKey`. **Recovery** Verify the signing key on the server matches the `SwiftPatchPublicKey` configured in the app's `Info.plist` / `strings.xml`. ### `SWIFTPATCH_MANIFEST_INVALID` `manifest.json` failed to parse or declares unsupported fields. **Fires when** the SDK cannot interpret the archive descriptor. **Recovery** The release was produced by an incompatible CLI/SDK pair — rebuild the release. ### `SWIFTPATCH_MANIFEST_MISSING` `manifest.json` is not present inside the archive. **Fires when** the zip does not contain the manifest file. **Recovery** The release is malformed; re-publish it with a supported CLI. ### `SWIFTPATCH_TEMP_HASH_MISSING` Pending bundle file referenced by state is absent on disk. **Fires when** iOS launches and the staged bundle has been purged (e.g. iOS wiped caches). **Recovery** SDK auto-clears pending state; a fresh download happens on the next check. ### `SWIFTPATCH_TEMP_HASH_CORRUPT` Pending bundle hash on disk no longer matches its recorded hash. **Fires when** the staged file was truncated after staging (disk error). **Recovery** SDK purges the pending slot and refetches. ## Patching (delta) ### `SWIFTPATCH_PATCH_VERIFY_FAILED` After applying a bsdiff patch, the output hash does not match the target. **Fires when** the base bundle used to patch wasn't actually the declared `fromHash` — base drift. **Recovery** SDK falls back to a full-bundle download automatically. ### `SWIFTPATCH_BSPATCH_FAILED` bsdiff returned a non-zero exit code while applying. **Fires when** the patch stream is corrupt or truncated. **Recovery** Redownload the patch; if it persists, the CLI that produced the release has a bug — fall back to a full bundle. ### `SWIFTPATCH_BASE_MISSING` Base bundle file the patch needs to apply against is not on disk. **Fires when** a patch is delivered for a bundle the device has never had. **Recovery** SDK requests a full bundle instead. ### `SWIFTPATCH_PATCH_OUTPUT_MISSING` bsdiff completed but did not produce an output file. **Fires when** the bsdiff process exits 0 but the output path is empty (filesystem failure). **Recovery** Retry; escalate to full-bundle if repeated. ## Slot / filesystem ### `SWIFTPATCH_DISK_FULL` Not enough free disk for download + extraction. **Fires when** writing to the staging directory fails with `ENOSPC`. **Recovery** Surface a user-facing prompt to free disk space. SDK will retry on next resume. ### `SWIFTPATCH_SLOT_CORRUPT` An existing slot directory has inconsistent metadata. **Fires when** the slot manager detects missing files referenced by prefs. **Recovery** SDK wipes the slot and re-downloads the stable bundle. ### `SWIFTPATCH_UNZIP_FAILED` Zip extraction failed (generic). **Fires when** the unzip library errors mid-stream. **Recovery** Delete staging dir and retry. ### `SWIFTPATCH_ZIP_SLIP` A zip entry path escaped the extraction root (path traversal). **Fires when** an archive contains `../` style entries. **Recovery** The archive is malicious or misbuilt — discard it and alert. ### `SWIFTPATCH_COMMIT_FAILED` Atomic rename of the staging directory into the final slot failed. **Fires when** filesystem permissions or cross-device rename blocks the swap. **Recovery** Retry; if persistent, the app sandbox is in a bad state — reinstall the app. ### `SWIFTPATCH_FILE_MISSING` A file expected by the manifest is missing from the extracted archive. **Fires when** manifest references `index.android.bundle` but it's absent. **Recovery** Release is malformed; re-publish. ## Runtime / state ### `SWIFTPATCH_BUSY` A conflicting operation is already in flight. **Fires when** two `downloadUpdate` calls collide, or `stabilize` runs during install. **Recovery** Wait for the current operation's promise to resolve and retry. ### `SWIFTPATCH_BLOCKED_HASH` Requested hash has been blocklisted (rolled back from earlier). **Fires when** an app tries to re-install a bundle that has been marked bad. **Recovery** Fetch a newer release; do not retry the same hash. ### `SWIFTPATCH_ALREADY_APPLIED` Pending bundle matches the currently running bundle. **Fires when** the JS layer calls `installUpdate` for a no-op hash. **Recovery** Treat as success — already on the target version. ### `SWIFTPATCH_NO_PENDING_INSTALL` `confirmInstall()` was called without a staged update. **Fires when** there is no bundle in the NEW slot to promote. **Recovery** Run a check + download first. ### `SWIFTPATCH_NATIVE_VERSION_MISMATCH` Native SDK version does not match the release's minimum native version. **Fires when** an OTA update requires a newer native binary than the app ships. **Recovery** Prompt the user to update via the app store; skip the OTA. ### `SWIFTPATCH_NO_BUNDLE_TO_STABILIZE` `stabilize()` called but the NEW slot is empty. **Fires when** the JS layer invokes stabilize after a rollback already cleared it. **Recovery** No-op; log at debug level. ### `SWIFTPATCH_NOT_INITIALIZED` SDK was used before `init()` resolved. **Fires when** a method on `SwiftPatch` is called before configuration. **Recovery** Always `await sp.init()` before other methods. ## Config / auth ### `SWIFTPATCH_INVALID_DEPLOYMENT_KEY` Configured deployment key was rejected by the server. **Fires when** the check API returns 401/403 citing the key. **Recovery** Verify `SwiftPatchDeploymentKey` / `swiftpatch_deployment_key` in your native config. Rotate the key in the dashboard if compromised. ### `SWIFTPATCH_INVALID_ENVIRONMENT` `switchEnvironment()` got an unknown value. **Fires when** the caller passes anything other than `PROD` or `STAGE`. **Recovery** Use the `EnvironmentMode` enum from the SDK. ### `SWIFTPATCH_JWT_EXPIRED` Signed URL or auth JWT in the release descriptor has expired. **Fires when** download starts after the URL TTL has elapsed. **Recovery** Re-run the check to receive a fresh signed URL. ### `SWIFTPATCH_CONFIG_INVALID` A required field in `initialize()` is missing or malformed. **Fires when** `deploymentKey` is empty, or `serverUrl` is not a valid URL. **Recovery** Fix the config passed to `withSwiftPatch` / `SwiftPatchProvider`. ## Next steps - [Troubleshooting](/sdk/troubleshooting) — debugging common setup issues. - [Events](/sdk/events) — how errors are dispatched. - [Integrations](/sdk/integrations) — forward errors to Sentry / Bugsnag. --- # Events <!-- source: docs/sdk/events.md --> Swiftpatch emits a small set of strongly-typed events for every phase of the update lifecycle. The same events drive the built-in UI, the hooks, and any custom listeners you register. ## Subscribing ```ts import { Swiftpatch } from '@swiftpatch/react-native'; const unsub = Swiftpatch.on('update-ready', (event) => { console.log('Ready:', event.update.version); }); // Later unsub(); ``` Every subscription returns an unsubscribe function. Listeners run in registration order. A throwing listener never breaks siblings — the bus catches and swallows exceptions so one bad consumer cannot brick your event pipeline. The bus is SSR-safe. Subscriptions in non-React-Native environments (server rendering, unit tests) become no-ops with a noop unsubscribe. ## The eight events ```ts type SwiftpatchEvent = | { type: 'update-available'; update: UpdateInfo } | { type: 'download-started'; hash: string; sizeBytes: number } | { type: 'download-progress'; hash: string; loaded: number; total: number } | { type: 'download-complete'; hash: string } | { type: 'update-ready'; update: UpdateInfo } | { type: 'update-applied'; update: UpdateInfo } | { type: 'update-dismissed'; update: UpdateInfo } | { type: 'rollback'; reason: string } | { type: 'error'; error: SwiftPatchError }; ``` ### `update-available` Fires once per release id when a `checkForUpdate` call finds a new release. Runs before the download starts. ```ts Swiftpatch.onUpdateAvailable((update) => { analytics.track('ota.update_available', { version: update.version }); }); ``` ### `download-started` Fires when the download begins. Carries the target hash and the total size in bytes. ```ts Swiftpatch.on('download-started', ({ hash, sizeBytes }) => { console.log(`Starting ${sizeBytes} byte download for ${hash}`); }); ``` ### `download-progress` Fires periodically during the download. Use it to drive a progress bar. ```ts Swiftpatch.onProgress(({ hash, loaded, total }) => { const pct = total > 0 ? Math.round((loaded / total) * 100) : 0; setProgress(pct); }); ``` ### `download-complete` Fires when bytes finish and the file passes SHA-256 verification. Always precedes `update-ready`. ### `update-ready` Fires when a downloaded bundle has passed signature + hash verification and is sitting in the `NEW` slot, ready to apply. ```ts Swiftpatch.onUpdateReady((update) => { showToast(`Update ready: v${update.version}`); }); ``` ### `update-applied` Fires when `applyNow()` (or auto-apply) commits the update and triggers a restart. Note: because the process restarts, this event is observable from within the same JS session that called `applyNow()`, but not on the next session. ### `update-dismissed` Fires when the user calls `Swiftpatch.dismissUpdate()` or taps Later on the built-in banner. The dismissal persists keyed on the release hash. ### `rollback` Fires when the device rolls back to the previous stable slot. `reason` describes why: | Reason | Meaning | |---|---| | `boot_loop_limit` | The freshly applied bundle crashed too many times in a row. | | `server_rollback` | The server reports the applied release has been rolled back. | | `signature_invalid` | A downloaded bundle failed signature verification. | | `user_initiated` | Your app called `rollback()` explicitly. | ### `error` Fires when any error surfaces in the SDK. The payload is a `SwiftPatchError`: ```ts interface SwiftPatchError { code: SwiftPatchErrorCode; // 'NETWORK_ERROR' | 'DOWNLOAD_ERROR' | ... message: string; details?: unknown; } ``` For the full list of error codes, see [Error codes](/sdk/error-codes). ## Wildcard listener Subscribe to every event with `'*'`: ```ts Swiftpatch.on('*', (event) => { telemetry.log('swiftpatch_event', event); }); ``` ## Event lifecycle diagram ``` ┌─────────────┐ │ check │──► update-available └──────┬──────┘ │ ▼ ┌─────────────┐ download-started │ download │──► download-progress (many) └──────┬──────┘ download-complete │ ▼ ┌─────────────┐ │ ready │──► update-ready └──────┬──────┘ │ user taps Apply user taps Later │ │ ▼ ▼ update-applied update-dismissed │ ▼ restart() ``` At any point during download or apply, a failure triggers an `error` event. If apply fails repeatedly and the boot counter trips, a `rollback` event fires on the next cold start. ## The `useSwiftpatchUpdate` hook Under the hood the hooks subscribe to these events and call `setState`. If you're building React UI, prefer the hook — you'll get re-renders for free. If you're building non-React UI or integrating with existing state (Redux, Zustand, MobX), subscribe via `Swiftpatch.on(...)`. ## Next steps - [Methods](/sdk/methods) — imperative verbs that emit these events. - [Hooks](/sdk/hooks) — React-friendly wrappers. - [Integrations](/sdk/integrations) — pipe lifecycle events to Sentry / Bugsnag / Crashlytics. --- # Expo Projects <!-- source: docs/sdk/expo.md --> Swiftpatch works seamlessly with both Expo bare and Expo managed workflows. The [Expo config plugin](https://docs.expo.dev/config-plugins/introduction/) wires AppDelegate, MainApplication, Info.plist, and AndroidManifest automatically at prebuild time. ## Managed workflow Add the plugin to `app.json` (or `app.config.js`): ```json title="app.json" { "expo": { "plugins": [ ["@swiftpatch/react-native", { "deploymentKey": "dep_xxx" }] ] } } ``` Then regenerate the native projects: ```bash npx expo prebuild --clean ``` That's it. The plugin injects: - `SwiftPatchModule.getJSBundleFile()` override in `MainApplication.kt`. - `bundleURL` override in `AppDelegate.swift`. - `SwiftPatchDeploymentKey` in `Info.plist`. - `swiftpatch_deployment_key` in `strings.xml`. - `pod 'SwiftPatch'` in the `Podfile` (when using custom pods). Once prebuild completes, build with EAS or run locally: ```bash npx expo run:ios # or eas build --platform ios ``` ## Omitting the deployment key If you prefer to set the key at runtime (for example, per-environment config), omit it from the plugin options: ```json { "expo": { "plugins": ["@swiftpatch/react-native"] } } ``` Then pass it via the provider: ```tsx <SwiftPatchProvider config={{ deploymentKey: process.env.DEPLOYMENT_KEY }}> <App /> </SwiftPatchProvider> ``` ## Bare workflow For bare Expo projects (you've already run `expo prebuild` or started with `expo-template-bare-minimum`), use the regular CLI flow: ```bash npm install @swiftpatch/react-native npx swiftpatch init ``` `swiftpatch init` detects the `ios/` and `android/` directories and patches them directly. If you later re-run `npx expo prebuild --clean`, the native directories will be regenerated and you'll need to re-run `swiftpatch init` to re-apply the patches. :::tip Use the config plugin in bare projects too For the cleanest CI flow in a bare Expo project, also add the config plugin to `app.json`. That way `npx expo prebuild --clean` regenerates your natives with Swiftpatch already wired in, and you don't need to run `swiftpatch init` after each prebuild. ::: ## Expo Updates coexistence Swiftpatch and `expo-updates` are mutually exclusive — both try to own the JS bundle loader. Pick one. If you're migrating from `expo-updates`: 1. Remove the `expo-updates` package. 2. Remove `expo-updates` from the `plugins` array in `app.json`. 3. Add `@swiftpatch/react-native` to `plugins`. 4. Run `npx expo prebuild --clean`. See [Migrating from Expo Updates](/guides/migrating-from-codepush) for a full port guide. ## EAS Build + CI/CD Swiftpatch deploys don't touch the native binary — they only ship JS bundles. So EAS Build is for **store releases** (the binary), and `swiftpatch deploy` is for **OTA releases** (JS). Typical flow: ```yaml title=".github/workflows/ota.yml" - name: Deploy OTA env: SWIFTPATCH_CI_TOKEN: ${{ secrets.SWIFTPATCH_CI_TOKEN }} run: | npx swiftpatch-cli deploy -p ios --hermes npx swiftpatch-cli deploy -p android --hermes ``` See [CI/CD](/cli/ci-cd) for full examples. ## Troubleshooting ### Prebuild fails with "plugin not found" Ensure the SDK is installed: ```bash npm install @swiftpatch/react-native ``` ### Prebuild succeeds but OTA updates don't work Verify the plugin ran. After `expo prebuild`, check that: - `ios/YourApp/AppDelegate.swift` contains `SwiftPatchModule.getBundleURL()`. - `android/app/src/main/java/.../MainApplication.kt` contains `SwiftPatchModule.getJSBundleFile`. - `ios/YourApp/Info.plist` contains `SwiftPatchDeploymentKey`. - `android/app/src/main/res/values/strings.xml` contains `swiftpatch_deployment_key`. If any are missing, re-run `npx expo prebuild --clean` and watch the plugin logs. ### Custom AppDelegate doesn't work with the plugin The plugin assumes a stock Expo AppDelegate layout. If you've heavily customized, use the config plugin with `deploymentKey` for the `Info.plist` entry and apply the AppDelegate changes manually — see [iOS setup](/sdk/ios-setup). ## Next steps - [Provider reference](/sdk/provider) — wrap your Expo app. - [CLI commands](/cli/commands) — deploy OTA updates. - [CI/CD](/cli/ci-cd) — automate OTA deploys from EAS-adjacent pipelines. --- # Hooks <!-- source: docs/sdk/hooks.md --> The SDK ships four opinionated hooks for React components. Pick the one that matches the level of control you need: | Hook | Use when | |---|---| | `useSwiftpatchUpdate()` | You want the full state machine — phase, pending release, progress, error, plus `applyNow`/`dismiss`. | | `useSwiftpatchProgress()` | You only need a progress bar during download. | | `useSwiftpatchReady(callback)` | You want to fire a callback the moment an update is ready, without subscribing to other events. | | `useOnResumeApply()` | You want updates to silently apply when the user foregrounds the app. One-line drop-in. | All hooks are SSR-safe. They must be used inside a `<SwiftPatchProvider>`. ## `useSwiftpatchUpdate()` The canonical hook. Returns everything you need to render update UI. ```tsx import { useSwiftpatchUpdate } from '@swiftpatch/react-native'; function UpdateBar() { const { phase, pendingUpdate, progress, error, applyNow, dismiss, clearError } = useSwiftpatchUpdate(); if (phase === 'idle') return null; if (phase === 'downloading' && progress) { return <Text>Downloading… {progress.percent}%</Text>; } if (phase === 'ready' && pendingUpdate) { return ( <View> <Text>Update ready: v{pendingUpdate.version}</Text> <Button title="Apply now" onPress={applyNow} /> <Button title="Later" onPress={dismiss} /> </View> ); } if (phase === 'error' && error) { return ( <View> <Text>Update failed: {error.message}</Text> <Button title="Dismiss" onPress={clearError} /> </View> ); } return null; } ``` **Return shape:** ```ts interface UseSwiftpatchUpdateReturn { state: UpdateState; phase: 'idle' | 'checking' | 'downloading' | 'ready' | 'applying' | 'error'; pendingUpdate: UpdateInfo | null; progress: { loaded: number; total: number; percent: number } | null; error: SwiftPatchError | null; applyNow: () => Promise<void>; dismiss: () => Promise<void>; clearError: () => void; } ``` - `phase` mirrors the orchestrator's lifecycle. - `applyNow()` installs the ready update and restarts the app. - `dismiss()` writes a dismissal flag keyed on the release hash, so the same release will not prompt again. A newer release surfaces normally. ## `useSwiftpatchProgress()` A lean hook for progress bars only. ```tsx import { useSwiftpatchProgress } from '@swiftpatch/react-native'; function DownloadBar() { const { percent, loaded, total, active } = useSwiftpatchProgress(); if (!active) return null; return ( <View style={{ height: 4, backgroundColor: '#222' }}> <View style={{ height: 4, width: `${percent}%`, backgroundColor: '#2563eb', }} /> <Text>{Math.round(loaded / 1024)} / {Math.round(total / 1024)} KB</Text> </View> ); } ``` **Return shape:** ```ts interface UseSwiftpatchProgressReturn { loaded: number; total: number; percent: number; // 0–100 hash: string; active: boolean; // true while a download is in flight } ``` ## `useSwiftpatchReady(callback)` Fire a callback exactly once when each new update becomes ready. Useful for custom notifications. ```tsx import { useSwiftpatchReady } from '@swiftpatch/react-native'; function NotifyWhenReady() { useSwiftpatchReady((update) => { Toast.show(`Update ready: v${update.version}`); }); return null; } ``` The callback is stable across re-renders (the hook pins it in a ref), so you can pass inline functions without subscription churn. ## `useOnResumeApply()` Drop this into a component once and updates silently apply when the user foregrounds the app. ```tsx import { useOnResumeApply } from '@swiftpatch/react-native'; function SilentAutoApply() { useOnResumeApply({ enabled: true }); return null; } ``` **Props:** ```ts interface Options { enabled?: boolean; // default: true } ``` The hook: 1. Listens for `update-ready` and tracks the latest ready release. 2. On `AppState` transition to `active`, calls `Swiftpatch.applyNow()` if a ready release exists. 3. Respects your active `UpdatePolicy` — if policy is `silent` and nothing is ready, it no-ops. ## Can I use the legacy `useSwiftPatch()` hook? Yes. It's still exported and returns the full state + actions object used by older integrations: ```tsx import { useSwiftPatch } from '@swiftpatch/react-native'; const { status, availableUpdate, downloadProgress, checkForUpdate, downloadUpdate, installUpdate, restart, rollback, /* ... */ } = useSwiftPatch(); ``` The opinionated hooks above are the recommended path for new code. ## Next steps - [UI components](/sdk/ui-components) — `<UpdateBanner>`, `<UpdateBlocker>`, `<UpdateOnboarding>`. - [Methods](/sdk/methods) — the imperative `Swiftpatch.*` API. - [Events](/sdk/events) — every event the SDK emits. --- # Installation <!-- source: docs/sdk/installation.md --> The Swiftpatch SDK is a single npm package. The `swiftpatch init` command handles all of the native setup — no hand-editing of `AppDelegate`, `MainApplication`, `Info.plist`, or `strings.xml`. ## Prerequisites | Dependency | Minimum | |---|---| | React Native | 0.76.0 | | React | 18.2.0 | | iOS | 13.4 | | Android | API 24 (Android 7.0) | | Node.js | 20.0 | ## 1. Install the package ```bash npm2yarn npm install @swiftpatch/react-native ``` Install the CLI globally (or invoke with `npx`): ```bash npm install -g swiftpatch-cli ``` ## 2. Run `swiftpatch init` ```bash npx swiftpatch init ``` The CLI patches your project end-to-end. Every file it touches is bounded by `// swiftpatch-begin` / `// swiftpatch-end` markers so `swiftpatch unlink` can cleanly reverse the operation. ### What gets patched | File | Change | |---|---| | `ios/<App>/AppDelegate.mm` or `.swift` | Injects a `bundleURL` override that calls Swiftpatch first and falls through to the default loader on nil. | | `ios/<App>/Info.plist` | Adds `SwiftPatchDeploymentKey`, `BGTaskSchedulerPermittedIdentifiers` entry (`com.swiftpatch.bg-refresh`), and `UIBackgroundModes` entries (`fetch`, `processing`). | | `android/app/src/main/java/.../MainApplication.kt` or `.java` | Injects `getJSBundleFile()` / `jsBundleFilePath` override. | | `android/app/src/main/res/values/strings.xml` | Adds `<string name="swiftpatch_deployment_key">…</string>`. | | `.swiftpatchrc` | Project config (app id, platform, signing-key path). Commit-safe — no secrets. | | `.swiftpatch/keys/private.pem` + `public.pem` | Generated Ed25519 keypair. Private key is gitignored; upload the public key in the dashboard. | | `.gitignore` | Appends `.swiftpatch/`, `swiftpatch-artifacts/`, `swiftpatch-private.pem`. | ### Monorepo + Expo support - **Monorepo:** `swiftpatch init` discovers every app under `apps/*` and patches each one. - **Expo bare workflow:** Detected automatically. The CLI patches the generated `ios/` and `android/` directories. If you later run `npx expo prebuild --clean`, re-run `swiftpatch init`. - **Expo managed workflow:** Use the [Expo config plugin](/sdk/expo) instead. ### Re-running safely `swiftpatch init` is idempotent. Running it twice is a no-op — existing markers are detected and skipped. If you want to remove everything cleanly: ```bash swiftpatch unlink ``` That deletes the blocks between the markers, restoring the default React Native bundle loader. ## 3. Re-pod on iOS ```bash cd ios && pod install && cd .. ``` ## 4. Wrap your app ```tsx title="App.tsx" import { SwiftPatchProvider } from '@swiftpatch/react-native'; export default function Root() { return ( <SwiftPatchProvider config={{ debug: __DEV__ }}> <App /> </SwiftPatchProvider> ); } ``` No JS-side deployment key needed — the SDK reads it from the native config `swiftpatch init` wrote. ## Manual install (fallback) If you cannot run the automated patcher (monorepo quirks, custom AppDelegate, strict code review policies), run it in manual mode: ```bash npx swiftpatch init --manual ``` The CLI still generates keys, creates your app in the dashboard, writes `.swiftpatchrc`, and updates `.gitignore`. It then prints the exact snippets to paste into your native files. See [iOS setup](/sdk/ios-setup) and [Android setup](/sdk/android-setup) for the full manual reference. ## Verify the install Start your app with `npx react-native run-ios` (or `run-android`) and check the debug logs. You should see: ``` SwiftPatch initialized (v2.x with dual-slot architecture) ``` Or call `getNativeConfig()` from JS to confirm the native keys are present: ```tsx import { getNativeConfig } from '@swiftpatch/react-native'; const cfg = await getNativeConfig(); console.log('Deployment key:', cfg.deploymentKey); console.log('Project ID:', cfg.projectId); ``` If the keys are null, `swiftpatch init` has not yet been run in this project — re-run it. ## Next steps - [Configuration](/sdk/configuration) — every Provider prop and `.swiftpatchrc` field. - [Provider reference](/sdk/provider) — `autoShowBanner`, `blockOnMandatory`, `theme`, `onUpdate`. - [iOS setup](/sdk/ios-setup) — the native file changes explained. - [Android setup](/sdk/android-setup) — same, for Android. - [Expo projects](/sdk/expo) — use the config plugin in managed or bare workflows. --- # Integrations <!-- source: docs/sdk/integrations.md --> Swiftpatch emits lifecycle breadcrumbs (update-check, download, signature verify, slot commit, rollback, patch apply, boot counter) that you can forward to any crash-reporting or observability tool. The SDK ships one-line adapters for the three popular vendors and a vendor-neutral breadcrumb API for everything else. ## Sentry ```ts import * as Sentry from '@sentry/react-native'; import { installSentrySink } from '@swiftpatch/react-native/integrations/sentry'; // During your app bootstrap Sentry.init({ dsn: '...' }); installSentrySink(Sentry); ``` Every Swiftpatch lifecycle event is forwarded to Sentry as a breadcrumb with the correct `category`, `level`, `data`, and `timestamp`. When a Sentry error is reported, you'll see the full Swiftpatch context leading up to it. ## Bugsnag ```ts import Bugsnag from '@bugsnag/react-native'; import { installBugsnagSink } from '@swiftpatch/react-native/integrations/bugsnag'; Bugsnag.start({ apiKey: '...' }); installBugsnagSink(Bugsnag); ``` Uses `Bugsnag.leaveBreadcrumb` with type `'manual'` (the safe catch-all). ## Firebase Crashlytics ```ts import crashlytics from '@react-native-firebase/crashlytics'; import { installCrashlyticsSink } from '@swiftpatch/react-native/integrations/fabric'; installCrashlyticsSink(crashlytics()); ``` Each breadcrumb lands in the Crashlytics timeline via `log()`. Error-level breadcrumbs additionally call `recordError()` so they appear in the non-fatal dashboard. ## Custom breadcrumb sink The three vendor adapters are thin wrappers around a generic API. For any other telemetry backend, register your own sink: ```ts import { SwiftPatch } from '@swiftpatch/react-native'; const sp = new SwiftPatch({ deploymentKey: 'dep_xxx' }); await sp.init(); const unsub = sp.addBreadcrumbSink((crumb) => { myTelemetry.log({ category: crumb.category, // e.g. 'swiftpatch.download' level: crumb.level, // 'debug' | 'info' | 'warning' | 'error' message: crumb.message, data: crumb.data, // { key: string | number | boolean } ts: crumb.timestamp, // ms since epoch }); }); // Later — e.g. on logout unsub(); ``` Emit your own breadcrumbs to correlate app events with Swiftpatch's feed: ```ts sp.breadcrumb('info', 'user_tapped_update_now', { releaseId: 'r_abc' }); ``` ## Breadcrumb categories Every crumb is tagged with a category so you can filter or route them: | Category | Emitted when | |---|---| | `swiftpatch.init` | SDK initialization, config load, native-module startup. | | `swiftpatch.check` | Update-check request / response. | | `swiftpatch.download` | Download start, progress ticks, completion. | | `swiftpatch.verify` | Signature verification, hash check. | | `swiftpatch.patch` | Delta-patch apply, fallback-to-full. | | `swiftpatch.slot` | Slot commit, promotion, stabilization. | | `swiftpatch.rollback` | Auto-rollback, manual rollback. | | `swiftpatch.boot` | Boot counter, crash detection, mount marker. | ## What gets forwarded Every event on the event bus is mirrored as a breadcrumb. For full event details, see [Events](/sdk/events). High-value crumbs to watch for in your crash reports: - `swiftpatch.verify` with level `error` — signature verification failed, bundle rejected. - `swiftpatch.rollback` — device reverted to the previous slot. - `swiftpatch.boot` — boot counter tripped, fallback to binary slot. - `swiftpatch.patch` with `bytesSaved` — delta-patch success (useful for bandwidth savings dashboards). - `swiftpatch.patch` with `PATCH_FALLBACK_TO_FULL` — delta path failed, fell back to full. ## Crash correlation pattern When a user crashes, you want to see what Swiftpatch did in the minutes leading up to it. The breadcrumb trail gives you that context: ``` [19:42:01] swiftpatch.check check-for-update hit [19:42:02] swiftpatch.check new release: r_xyz (v1.2.3) [19:42:02] swiftpatch.download download-started hash=abc123 size=412KB [19:42:04] swiftpatch.verify signature OK [19:42:04] swiftpatch.verify hash OK [19:42:04] swiftpatch.slot commit NEW slot [19:42:04] user tapped apply_now [19:42:05] swiftpatch.boot mount marker set [19:42:07] ERROR TypeError: Cannot read property 'x' of undefined ``` The crash landed on bundle `r_xyz`. The boot counter will notice and auto-rollback on the next cold start. The incident in the dashboard will group this error under the release. ## Next steps - [Events](/sdk/events) — the full event bus, including error events. - [Error codes](/sdk/error-codes) — every code the SDK can emit. - [Crash detection](/concepts/crash-detection) — how the SDK detects, clusters, and auto-heals. --- # iOS Setup <!-- source: docs/sdk/ios-setup.md --> The automatic way is one command. The manual way is a short snippet you paste into `AppDelegate`. Both are documented here. ## The automatic path ```bash npx swiftpatch init cd ios && pod install && cd .. ``` That's it. Move on to [wrapping your app](/sdk/provider). Under the hood, `swiftpatch init` modifies three iOS files. Every change is bounded by `// swiftpatch-begin` / `// swiftpatch-end` markers, so `swiftpatch unlink` reverses them cleanly. ### `AppDelegate.mm` or `.m` (Objective-C / Objective-C++) Inserted at the top of `bundleURL` (or `sourceURLForBridge`): ```objc title="ios/YourApp/AppDelegate.mm" - (NSURL *)bundleURL { // swiftpatch-begin (auto-generated, do not edit between markers) NSURL *swiftpatchURL = [SwiftPatchModule getBundleURL]; if (swiftpatchURL != nil) { return swiftpatchURL; } // swiftpatch-end // Your existing code continues unchanged below #if DEBUG return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; #else return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif } ``` The inserted block calls the SDK first. If it returns `nil` (no OTA bundle, missing class, or error), control falls through to the original implementation untouched. This is why `swiftpatch unlink` and upgrades are safe. ### `AppDelegate.swift` (RN 0.77+) ```swift title="ios/YourApp/AppDelegate.swift" override func bundleURL() -> URL? { // swiftpatch-begin (auto-generated, do not edit between markers) if let url = SwiftPatchModule.getBundleURL() { return url } // swiftpatch-end #if DEBUG return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") #else return Bundle.main.url(forResource: "main", withExtension: "jsbundle") #endif } ``` ### `Info.plist` ```xml title="ios/YourApp/Info.plist" <!-- swiftpatch-begin (auto-generated, do not edit between markers) --> <key>SwiftPatchDeploymentKey</key> <string>dep_xxx...</string> <!-- swiftpatch-end --> <!-- Background sync task registration --> <key>BGTaskSchedulerPermittedIdentifiers</key> <array> <string>com.swiftpatch.bg-refresh</string> </array> <key>UIBackgroundModes</key> <array> <string>fetch</string> <string>processing</string> </array> ``` The `BGTaskSchedulerPermittedIdentifiers` and `UIBackgroundModes` entries power [Background sync](/sdk/background-sync). The SDK handles task registration; you just need these keys present. ## Manual setup If auto-patching can't run (monorepo quirks, custom AppDelegate, strict review policy), run: ```bash npx swiftpatch init --manual ``` The CLI still creates your app in the dashboard, generates keys, writes `.swiftpatchrc`, and updates `.gitignore`. It then prints the exact snippets to paste. The steps to apply by hand: ### 1. Install the pod ```bash cd ios && pod install && cd .. ``` ### 2. Paste the `bundleURL` override Copy the snippet from the relevant `AppDelegate` section above into your `bundleURL` method (Swift) or `sourceURLForBridge` method (Objective-C). ### 3. Add `Info.plist` keys ```xml <key>SwiftPatchDeploymentKey</key> <string>dep_xxx...</string> <key>BGTaskSchedulerPermittedIdentifiers</key> <array> <string>com.swiftpatch.bg-refresh</string> </array> <key>UIBackgroundModes</key> <array> <string>fetch</string> <string>processing</string> </array> ``` ### 4. (Optional) Wire silent-push-triggered sync Only needed if you want silent pushes to trigger background update checks. See [Background sync](/sdk/background-sync). ```swift import SwiftPatch func application( _ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void ) { SilentPushHandler.handleRemoteNotification(userInfo, completionHandler: completionHandler) } ``` ## Minimum iOS version iOS 13.4+. The SDK uses `BGTaskScheduler` (iOS 13+) and `URLSession` resumable downloads. The default `IPHONEOS_DEPLOYMENT_TARGET` in modern React Native templates already satisfies this. ## New Architecture Swiftpatch ships a TurboModule spec and is fully compatible with Fabric + the New Architecture. See [New Architecture](/sdk/new-architecture). ## Troubleshooting - **"SwiftPatchModule not found"** — you forgot `pod install` after running `swiftpatch init`. Run `cd ios && pod install`. - **App loads the old bundle after an update** — check that your `bundleURL` override is returning the OTA URL. Call `getNativeConfig()` from JS and verify `deploymentKey` is non-null. - **Build error about duplicate `UIBackgroundModes`** — your existing `Info.plist` already has `UIBackgroundModes`; `swiftpatch init` merges by default, but a hand-edited plist may trip the parser. Remove the duplicate block and re-run `init`. For more, see [Troubleshooting](/sdk/troubleshooting). ## Next steps - [Android setup](/sdk/android-setup) — the Android equivalents. - [Provider reference](/sdk/provider) — wrap your app. - [Background sync](/sdk/background-sync) — iOS `BGTaskScheduler` + silent push. --- # Methods <!-- source: docs/sdk/methods.md --> The SDK exposes two imperative surfaces: - **`Swiftpatch`** — a tree-shakable namespace facade. Use this for event subscriptions and orchestrator verbs from anywhere in your app. - **`SwiftPatch`** — a class for environments with no React provider (headless JS, background tasks, native callbacks). Most apps should stick with `Swiftpatch` plus hooks + provider. The `SwiftPatch` class is for advanced use cases. ## The `Swiftpatch` facade ```ts import { Swiftpatch } from '@swiftpatch/react-native'; ``` ### Event subscriptions Every `Swiftpatch.on*()` call returns an unsubscribe function. Safe to call before the provider mounts — subscriptions are remembered. ```ts const unsub1 = Swiftpatch.onUpdateAvailable((update) => { console.log('Check found:', update.version); }); const unsub2 = Swiftpatch.onUpdateReady((update) => { console.log('Downloaded + ready:', update.version); }); const unsub3 = Swiftpatch.onUpdateApplied((update) => { console.log('Running the new bundle:', update.version); }); const unsub4 = Swiftpatch.onUpdateDismissed((update) => { console.log('User tapped Later on:', update.version); }); const unsub5 = Swiftpatch.onError((error) => { console.warn('Swiftpatch error:', error.code, error.message); }); const unsub6 = Swiftpatch.onProgress(({ loaded, total }) => { console.log(`${loaded} / ${total} bytes`); }); const unsub7 = Swiftpatch.onRollback((reason) => { console.warn('Rolled back:', reason); }); // Wildcard — every event type const unsub8 = Swiftpatch.on('*', (event) => { console.log(event.type, event); }); ``` See [Events](/sdk/events) for the full list of event types and payloads. ### Orchestrator verbs Once the provider is mounted (or `new SwiftPatch().init()` has completed), these are safe to call from anywhere. ```ts // Apply the ready update + restart await Swiftpatch.applyNow(); // Persist a per-release dismissal. A newer release will still surface. await Swiftpatch.dismissUpdate(); // Snapshot of the current lifecycle state const state = await Swiftpatch.getCurrentState(); // { phase: 'idle' | 'checking' | 'downloading' | 'ready' | 'applying' | 'error', // current: BundleInfo | null, // pending: UpdateInfo | null, // lastCheckedAt: Date | null, // lastError: SwiftPatchError | null } // Persist a user-facing policy await Swiftpatch.setUpdatePolicy({ prompt: 'mandatory-only', // 'always' | 'mandatory-only' | 'silent' applyTiming: 'on-resume', // 'immediate' | 'on-resume' | 'on-next-launch' }); const policy = await Swiftpatch.getUpdatePolicy(); // After a failed apply, retry once await Swiftpatch.retryFailedUpdate(); // Check whether a given release should prompt (honors policy + dismissals) const should = await Swiftpatch.shouldPromptFor(release); // Check whether a release was previously dismissed const dismissed = await Swiftpatch.isReleaseDismissed(release); ``` ## Channels Channels let you ship to a subset of users (for example, `beta`) before promoting globally. ```ts import { setChannel, getChannel, onChannelChanged } from '@swiftpatch/react-native'; // Set the active channel — persists to native storage. await setChannel('beta'); // Read the current channel (hydrates from native on first call) const current = await getChannel(); // 'production' | 'beta' | ... // Subscribe to changes const unsub = onChannelChanged((channel) => { console.log('Channel is now:', channel); }); ``` Every `checkForUpdate` call passes the channel. Default is `production`. Custom channel names are allowed — configure them in the dashboard. See [Channels guide](/sdk/channels). ## Custom breadcrumbs Forward Swiftpatch lifecycle events to your crash-reporting tool. ```ts import { SwiftPatch } from '@swiftpatch/react-native'; const sp = new SwiftPatch({ deploymentKey: 'dep_xxx' }); await sp.init(); const unsub = sp.addBreadcrumbSink((crumb) => { myTelemetry.log(crumb.message, { level: crumb.level, category: crumb.category, data: crumb.data, }); }); // Or emit your own crumb sp.breadcrumb('info', 'user_tapped_apply', { releaseId: 'r_abc' }); ``` One-liner adapters are shipped for the three popular vendors. See [Integrations](/sdk/integrations): ```ts import { installSentrySink } from '@swiftpatch/react-native/integrations/sentry'; import * as Sentry from '@sentry/react-native'; installSentrySink(Sentry); ``` ## White-label — runtime tenant switching Use one binary for many brands. Rotate the deployment key at runtime without restarting. ```ts const sp = new SwiftPatch({ deploymentKey: initialKey }); await sp.init(); // Soft switch: next checkForUpdate uses the new tenant's release line. await sp.setDeploymentKey(tenantKey); // Hard switch: roll back to the store bundle and wipe all slots, so the user // boots clean on the new tenant. await sp.setDeploymentKey(tenantKey, { clearCurrentBundle: true }); // Read the persisted key const key = sp.getDeploymentKey(); // Subscribe to changes — for example, re-theme the app after auth. sp.onTenantChanged((key) => console.log('Tenant switched:', key)); ``` See [White-label guide](/sdk/white-label). ## The `SwiftPatch` class — non-React usage For headless JS, background tasks, or native callbacks, construct a `SwiftPatch` instance directly: ```ts import { SwiftPatch } from '@swiftpatch/react-native'; const sp = new SwiftPatch({ deploymentKey: 'dep_xxx', debug: __DEV__, }); await sp.init(); // Check const release = await sp.checkForUpdate(); if (release) { // Download (uses delta-patch pipeline automatically when a patch is offered) await sp.download(release); // Install await sp.install(release); // Or do both in one call // await sp.downloadAndInstall(release); // Restart sp.restart(release.id); } // Channels await sp.setChannel('beta'); const channel = await sp.getChannel(); // Rollback to the previous bundle await sp.rollback(); // Bundle info const bundle = await sp.getCurrentBundle(); // Test the backend connection (no init required) const ping = await sp.testConnection(); // { connected: true, app: {...}, organization: {...} } ``` ## Static helpers ```ts import { SwiftPatch, getKnownErrors, getErrorDescription, testConnection, getNativeConfig, restart, } from '@swiftpatch/react-native'; // Static — no init, no native call. Safe to call at module load. const codes = SwiftPatch.getKnownErrors(); // ['SWIFTPATCH_DOWNLOAD_FAILED', 'SWIFTPATCH_HASH_MISMATCH', ...] // Human-readable error copy const info = getErrorDescription('SWIFTPATCH_SIGNATURE_INVALID'); // Quick connection check without full SDK init const ping = await testConnection({ deploymentKey: 'dep_xxx' }); // Read native config const cfg = await getNativeConfig(); // Force a JS reload (rarely needed — use applyNow instead) restart(); ``` ## Next steps - [Events](/sdk/events) — what fires when. - [Types](/sdk/types) — the full type surface. - [Error codes](/sdk/error-codes) — the 33-code taxonomy. - [Integrations](/sdk/integrations) — Sentry, Bugsnag, Crashlytics. --- # New Architecture <!-- source: docs/sdk/new-architecture.md --> # New Architecture Support Swiftpatch ships as a TurboModule out of the box. Enable the New Architecture on your project and autolinking does the rest — no codegen invocation required from you. ## Enable the New Architecture ### iOS ```ruby title="ios/Podfile" ENV['RCT_NEW_ARCH_ENABLED'] = '1' ``` Then re-pod: ```bash cd ios && pod install && cd .. ``` ### Android ```properties title="android/gradle.properties" newArchEnabled=true ``` Then: ```bash cd android && ./gradlew clean && cd .. ``` ## Verifying you're on TurboModules The SDK exposes a runtime flag: ```tsx import { IS_TURBO_MODULE_ENABLED } from '@swiftpatch/react-native'; console.log('Using TurboModules:', IS_TURBO_MODULE_ENABLED); ``` When this is `true`, every native call goes through JSI directly — no bridge serialization. ## Performance TurboModule calls skip JSON serialization and the bridge. Typical operation costs on a mid-range device: | Operation | Legacy bridge | TurboModule | |---|---|---| | `checkForUpdate()` round trip | 15 ms | 2 ms | | `getCurrentBundle()` | 8 ms | 1 ms | | `getSlotMetadata()` | 10 ms | 2 ms | The SDK also uses the TurboModule's `addListener`/`removeListeners` bookkeeping, so native events (download progress, rollback, version-changed) are delivered with minimal overhead. ## Codegen The SDK ships a codegen spec at `src/native/NativeSwiftPatchSpec.ts`. React Native's codegen consumes it during your app build and generates the native interfaces for iOS and Android. You don't invoke codegen yourself — it runs as part of `pod install` on iOS and Gradle's `generateCodegenArtifactsFromSchema` task on Android. The spec uses only the types codegen allows: - Primitives (`string`, `number`, `boolean`). - `Object`, `Array<Object>`. - Nullable primitives (`T | null`). - `Promise<T>`. - No string-literal unions, no `Pick`/`Omit`, no imported types on method signatures. Rich types (`CheckResult`, `UpdateDescriptor`, `PatchDescriptor`, `SlotMetadataShape`) live alongside the spec but are not consumed by codegen — they narrow the native return type inside the TypeScript wrapper. ## `codegenConfig` Your app's `package.json` does **not** need a `codegenConfig` entry for Swiftpatch — the SDK's own `package.json` declares it: ```json title="@swiftpatch/react-native/package.json" { "codegenConfig": { "name": "RNSwiftPatchSpec", "type": "modules", "jsSrcsDir": "src/native", "ios": { "componentProvider": "" } } } ``` React Native autolinking picks this up and generates the TurboModule stubs in the correct location. ## Troubleshooting New Architecture builds - **"Cannot find module 'NativeSwiftPatch'"** — codegen didn't run. Re-run `pod install` (iOS) or `./gradlew clean build` (Android). - **Duplicate native module** — you have both old-arch and new-arch artifacts. Clean everything: `cd ios && rm -rf Pods Podfile.lock && pod install && cd ..` and `cd android && ./gradlew clean`. - **`TurboModuleRegistry.getEnforcing('SwiftPatch') fails`** — New Architecture is not actually enabled. Verify `RCT_NEW_ARCH_ENABLED=1` (iOS) and `newArchEnabled=true` (Android). ## Fallback to the legacy bridge If the New Architecture isn't enabled, the SDK automatically falls back to the legacy bridge module. The JavaScript API is identical. You won't notice a difference at the call-site. ## Next steps - [iOS setup](/sdk/ios-setup) / [Android setup](/sdk/android-setup) — platform setup. - [Methods](/sdk/methods) — the imperative API (identical on TurboModule and legacy). - [Troubleshooting](/sdk/troubleshooting) — general setup issues. --- # SDK Overview <!-- source: docs/sdk/overview.md --> The SwiftPatch SDK pushes over-the-air updates to your React Native app -- no app store review required. Install it, wrap your root component, and deploy from the CLI. Your users get changes in seconds. :::info What is an OTA update? An OTA update replaces your app's JavaScript bundle at runtime. Native code changes still require an app store release. The SDK handles downloading, verifying, and applying bundle updates automatically. ::: ## Get Started in 3 Steps ### Step 1: Install ```bash npm install @swiftpatch/react-native ``` ### Step 2: Wrap Your Root Component ```tsx import { withSwiftPatch } from '@swiftpatch/react-native'; function App() { return <YourApp />; } export default withSwiftPatch(App); ``` ### Step 3: Deploy ```bash swiftpatch deploy -p ios --hermes ``` That's it! Your app now checks for updates on every launch and resume, downloads them in the background, and applies them on the next restart. :::tip Zero config by default The SDK reads credentials from native config files. You do not need to pass options to `withSwiftPatch` unless you want to customize behavior. ::: ## How It Works ``` ┌─────────────────────┐ ┌──────────────────────┐ │ Your React Native │ │ SwiftPatch API │ │ App │────────>│ api.swiftpatch.io │ │ │ check │ │ │ ┌───────────────┐ │ └──────────────────────┘ │ │ SwiftPatch SDK│ │ │ │ │ │ │ │ manifest │ │ - Check │ │ v │ │ - Download │ │ ┌──────────────────────┐ │ │ - Verify │<─────────│ SwiftPatch CDN │ │ │ - Install │ │ bundle │ cdn.swiftpatch.io │ │ │ - Stabilize │ │ └──────────────────────┘ │ └───────────────┘ │ └─────────────────────┘ ``` ### Update Lifecycle 1. **Check** -- Query the API on launch and resume for new deployments. 2. **Download** -- Fetch the bundle from CDN. Uses differential patches (bsdiff) when possible, reducing size by 10-50x. 3. **Verify** -- Validate with SHA-256 (a cryptographic function that creates a unique fingerprint for any file) hash. Optionally verify JWT (JSON Web Token -- a compact, signed data format) RS256 signature. 4. **Install** -- Stage the bundle for activation (immediately or on next restart). 5. **Stabilize** -- After 2 successful launches, promote to "stable" status. 6. **Rollback** -- If a crash is detected before stabilization, revert automatically. ## Key Features - **Automatic updates** -- Checks on every launch and resume. Customizable via [Configuration](./configuration.md). - **Crash detection and rollback** -- Automatically reverts to the last stable version if the new bundle crashes. - **Differential patches** -- Downloads only what changed. Updates are 10-50x smaller than the full bundle. - **Hermes bytecode** -- Works with precompiled Hermes (React Native's JavaScript engine that runs precompiled bytecode) bundles on both platforms. - **New Architecture** -- Full TurboModules (faster native module communication, replacing the old bridge) and Fabric (React Native's new rendering engine for smoother UI) support. Auto-detects at runtime. - **Code signing** -- Optional RSA (a widely used encryption algorithm that uses a pair of keys: a private key to sign and a public key to verify) signing to verify updates have not been tampered with. :::info TypeScript included The SDK ships with full TypeScript definitions for all options, hooks, methods, and types. ::: ## Package Info | Property | Value | |----------|-------| | Package | `@swiftpatch/react-native` | | Version | **2.1.8** | | React Native | >= 0.76.0 | | React | >= 18.2.0 | | iOS | >= 13.4 | | Android | SDK 24+ (Android 7.0+) | | Node.js | >= 18.0.0 | | License | MIT | ## Next Steps | Page | What you find | |------|---------------| | [Installation](./installation.md) | Step-by-step setup for iOS and Android | | [Configuration](./configuration.md) | All SDK options -- install modes, check frequency, code signing | | [Hooks](./hooks.md) | React hooks: `useSwiftPatch`, `useSwiftPatchUpdate`, `useSwiftPatchModal` | | [Methods](./methods.md) | Imperative API for manual update control | :::tip Where to start Adding SwiftPatch to an existing app? Start with [Installation](./installation.md). Already installed? Jump to [Configuration](./configuration.md). ::: --- # SwiftPatchProvider <!-- source: docs/sdk/provider.md --> # `<SwiftPatchProvider>` Wrap your app once. The provider initializes the native module, arms crash detection, polls for updates on launch and resume, and renders the update banner when a release is ready. ## The two-line integration ```tsx title="App.tsx" import { SwiftPatchProvider } from '@swiftpatch/react-native'; export default function Root() { return ( <SwiftPatchProvider config={{ debug: __DEV__ }}> <App /> </SwiftPatchProvider> ); } ``` With zero extra props the provider: - Reads your deployment key from `Info.plist` / `strings.xml`. - Checks for updates on cold start and on resume. - Renders `<UpdateBanner />` at the bottom of the screen when a release is ready. - Renders `<UpdateBlocker />` as a full-screen modal for mandatory releases. - Matches the OS light/dark theme. ## Props ```tsx interface SwiftPatchProviderProps { config: SwiftPatchUserConfig | SwiftPatchConfig; children: ReactNode; // Optional opinionated DX layer autoShowBanner?: boolean; blockOnMandatory?: boolean; showOnboardingOnFirstUpdate?: boolean; onUpdate?: (update: ReleaseInfo) => void; theme?: 'auto' | 'light' | 'dark' | Partial<Theme>; } ``` | Prop | Default | What it does | |---|---|---| | `config` | required | SDK config. See [Configuration](/sdk/configuration). | | `autoShowBanner` | `true` | Render `<UpdateBanner />` when an update is ready. Set `false` to render your own UI. | | `blockOnMandatory` | `true` | Render `<UpdateBlocker />` full-screen for mandatory releases. | | `showOnboardingOnFirstUpdate` | `false` | One-time walkthrough after the first OTA applies. | | `onUpdate` | — | Fires when an update becomes available. Complements `config.onUpdate`. | | `theme` | `'auto'` | `'auto'` follows the OS. Pass a partial `Theme` to override. | ## Recipes ### Silent mode — render your own UI ```tsx <SwiftPatchProvider config={{ debug: __DEV__ }} autoShowBanner={false} blockOnMandatory={false} > <App /> </SwiftPatchProvider> ``` Lifecycle events still fire. Subscribe with `useSwiftpatchUpdate()` or `Swiftpatch.onUpdateReady()` to drive your own banner. See [Hooks](/sdk/hooks) and [Methods](/sdk/methods). ### Custom theme ```tsx <SwiftPatchProvider config={{ debug: __DEV__ }} theme={{ accent: '#ff3366', background: '#0a0a0f', surface: '#14141f', text: '#ffffff', textMuted: '#8a8aa0', }} > <App /> </SwiftPatchProvider> ``` The full `Theme` interface and per-component overrides live in [UI components](/sdk/ui-components). ### Fire analytics on every update ```tsx <SwiftPatchProvider config={{ debug: __DEV__ }} onUpdate={(update) => { analytics.track('ota.update_available', { version: update.version, isPatch: update.isPatch, sizeKb: Math.round(update.downloadSize / 1024), }); }} > <App /> </SwiftPatchProvider> ``` ### Force-update gate with onboarding ```tsx <SwiftPatchProvider config={{ debug: __DEV__ }} blockOnMandatory showOnboardingOnFirstUpdate > <App /> </SwiftPatchProvider> ``` The blocker shows for any release flagged mandatory. Onboarding shows exactly once, after the first OTA applies successfully. ## What happens on mount 1. Initializes the native module with your deployment key, crash window, and threshold. 2. Reads `Info.plist` / `strings.xml` for any field you didn't pass in JS. 3. Calls `markMounted()` to arm the crash watchdog and clear the boot-loop flag. 4. Loads the current bundle info and slot metadata. 5. Transitions to `RESTART_REQUIRED` if a pending install exists. 6. Fires the initial `checkForUpdate()`. 7. Starts the event-polling loop with adaptive backoff — pauses when backgrounded. 8. Subscribes to `AppState` to re-check on resume. :::warning One provider per app Mounting multiple providers corrupts the crash watchdog and double-counts events. Wrap your app once at the root. ::: ## Do you need the provider? If you want the built-in banner, blocker, onboarding, or any hook: **yes**. For headless background tasks or non-React surfaces, drive updates through the imperative `Swiftpatch` class — see [Methods](/sdk/methods). ## Next steps - [Hooks](/sdk/hooks) — `useSwiftpatchUpdate`, `useSwiftpatchProgress`, `useSwiftpatchReady` - [UI components](/sdk/ui-components) — theme or replace the banner, blocker, and onboarding - [Methods](/sdk/methods) — the imperative `Swiftpatch.*` facade --- # Troubleshooting <!-- source: docs/sdk/troubleshooting.md --> Common issues and how to fix them. For the full 33-code error taxonomy, see [Error codes](/sdk/error-codes). ## Setup ### `swiftpatch init` says "React Native not detected" The CLI walks up from your CWD looking for `package.json` with `react-native` in `dependencies`. Run it from your project root, not from `ios/` or `android/`. ### `pod install` fails with "unable to find SwiftPatch" Autolinking needs your `node_modules` freshly installed: ```bash rm -rf node_modules ios/Pods ios/Podfile.lock npm install cd ios && pod install && cd .. ``` ### Android build: "cannot resolve symbol SwiftPatchModule" Gradle cached the old autolinking output. Clean and rebuild: ```bash cd android && ./gradlew clean && cd .. npx react-native run-android ``` ### "SwiftPatch initialized" log never appears The native module couldn't initialize. Causes, in order of likelihood: 1. `swiftpatch init` wasn't run. Run it. 2. `pod install` wasn't re-run after init. Run `cd ios && pod install`. 3. You're on an unsupported RN version (< 0.76). Upgrade, or use the manual install. ## Bundle loading ### App loads the old bundle after an update The `bundleURL` / `getJSBundleFile` override isn't wired. Run: ```bash npx swiftpatch doctor ``` This checks every native patch and prints what's missing. You can also verify from JS: ```ts import { getNativeConfig } from '@swiftpatch/react-native'; const cfg = await getNativeConfig(); console.log(cfg.deploymentKey); // should be non-null ``` ### App crashes on boot after apply This is what the [brick protection](/concepts/brick-protection) system prevents. If you see a crash loop, the boot counter should roll the device back to the previous stable bundle within 2–3 crashes. If it doesn't: - Confirm you're calling `markMounted()` — the provider does this automatically; the `withSwiftPatch` HOC does this automatically. If you're using manual wiring, call `Swiftpatch.markMounted()` after your first render. - Check the `RollbackEvent` in the dashboard. - Pull device logs — the native layer prints the exact reason for the rollback. ### "Bundle won't load" — `ENOENT` or "file not found" One of: - The download was interrupted and the temp file was purged. The SDK retries on the next resume. - The slot was corrupted. Call `await rollback()` to revert to the previous stable slot. - The bundle hash doesn't match (tampering or corruption). Check for `SWIFTPATCH_HASH_MISMATCH` in the error event. ## Downloads & network ### Updates never download - Verify the deployment key is valid — call `testConnection()`: ```ts import { testConnection } from '@swiftpatch/react-native'; const ping = await testConnection({ deploymentKey: 'dep_xxx' }); // { connected: true, app: {...}, organization: {...} } ``` - Check your network. The SDK skips checks when `NetInfo` reports no connectivity. - Look for `SWIFTPATCH_INVALID_DEPLOYMENT_KEY` in the error event — your key may have been rotated in the dashboard. ### "Signature mismatch" errors The bundle was signed with a different private key than the one your app's public key validates against. Either: - The public key in `Info.plist` / `strings.xml` is stale. Update it. - The release was signed with the wrong key in CI. Verify your CI has the right `private.pem`. See [Bundle signing](/concepts/bundle-signing). ## Assets ### Images / fonts are missing after an OTA The Swiftpatch CLI bundles assets alongside the JS bundle. If the assets aren't present in your OTA archive, the bundler didn't pick them up. Common causes: - You moved files after `swiftpatch deploy` started bundling. Re-run. - You're using `require('../assets/...')` with a path that resolves differently in release mode. Normalize paths. - Your bundler config excludes some file types. Check `metro.config.js`. The CLI prints a per-asset manifest during `swiftpatch deploy` — verify your assets appear in that list. ## Runtime ### "update-ready" fires but banner never appears Either: - The user previously dismissed this release hash. The banner stays hidden for the same hash. - You set `autoShowBanner={false}` — render your own UI via `useSwiftpatchUpdate()`. - The release's `isMandatory` is true and `blockOnMandatory` is on — you'll see `<UpdateBlocker />` instead. ### Background sync never fires - On iOS: verify `BGTaskSchedulerPermittedIdentifiers` contains `com.swiftpatch.bg-refresh` and `UIBackgroundModes` contains `fetch` + `processing`. `swiftpatch init` adds these. - On Android: WorkManager schedules are best-effort — the OS picks when to run them based on battery + connectivity. - See [Background sync](/sdk/background-sync). ## Detecting errors programmatically Every error emits on the bus: ```ts import { Swiftpatch, getErrorDescription } from '@swiftpatch/react-native'; Swiftpatch.onError((error) => { const desc = getErrorDescription(error.code); console.warn(`[${error.code}]`, error.message); console.warn('Recovery:', desc.troubleshooting); }); ``` `getErrorDescription` returns `{ description, troubleshooting, category }` for every known code. See [Error codes](/sdk/error-codes). ## Running the doctor The CLI has a one-shot diagnostic tool: ```bash npx swiftpatch doctor ``` It inspects: - Whether the native patches are present. - Whether your signing key exists. - Whether the deployment key is live. - Whether your RN version, Hermes config, and asset pipeline are correctly wired. For deeper analysis with AI: ```bash npx swiftpatch doctor --ai ``` See [AI doctor](/guides/ai-doctor). ## Still stuck? - [Error codes](/sdk/error-codes) — every code the SDK emits. - Email `sdk@swiftpatch.io` — include a `swiftpatch doctor --json` output if possible. - [GitHub issues](https://github.com/swiftpatch). ## Next steps - [Error codes](/sdk/error-codes) — full taxonomy. - [Brick protection](/concepts/brick-protection) — why bad bundles don't brick your app. - [AI doctor](/guides/ai-doctor) — automated project analysis. --- # Types <!-- source: docs/sdk/types.md --> Every type the Swiftpatch SDK exports. Fully documented and stable across minor versions. ## Config ### `SwiftPatchUserConfig` The simple config most apps use. Passed to `<SwiftPatchProvider config={...}>` or `new SwiftPatch(...)`. ```ts interface SwiftPatchUserConfig { deploymentKey?: string; debug?: boolean; onUpdate?: (release: ReleaseInfo) => void; } ``` ### `SwiftPatchConfig` The full config surface. Every field optional; see [Configuration](/sdk/configuration) for defaults and recommended values. ```ts interface SwiftPatchConfig extends SwiftPatchUserConfig { appId?: string; serverUrl?: string; checkOnResume?: boolean; checkInterval?: number; installMode?: InstallMode; mandatoryInstallMode?: InstallMode; customHeaders?: Record<string, string>; publicKey?: string; autoStabilizeAfterLaunches?: number; crashDetectionWindowMs?: number; maxCrashesBeforeRollback?: number; autoStagingInDev?: boolean; } ``` ## Enums ### `InstallMode` ```ts enum InstallMode { IMMEDIATE = 'immediate', ON_NEXT_RESTART = 'onNextRestart', ON_NEXT_RESUME = 'onNextResume', } ``` ### `UpdateStatus` The lifecycle state exposed on the `useSwiftPatch()` hook (legacy). ```ts enum UpdateStatus { CHECKING = 'checking', UP_TO_DATE = 'upToDate', UPDATE_AVAILABLE = 'updateAvailable', DOWNLOADING = 'downloading', READY_TO_INSTALL = 'readyToInstall', INSTALLING = 'installing', RESTART_REQUIRED = 'restartRequired', ERROR = 'error', } ``` ### `SwiftPatchErrorCode` High-level error categories on the `SwiftPatchError` type. See [Error codes](/sdk/error-codes) for the full 33-code taxonomy. ```ts enum SwiftPatchErrorCode { NETWORK_ERROR = 'NETWORK_ERROR', DOWNLOAD_ERROR = 'DOWNLOAD_ERROR', VERIFICATION_ERROR = 'VERIFICATION_ERROR', PATCH_ERROR = 'PATCH_ERROR', INSTALL_ERROR = 'INSTALL_ERROR', CONFIG_ERROR = 'CONFIG_ERROR', STABILIZE_ERROR = 'STABILIZE_ERROR', ENVIRONMENT_ERROR = 'ENVIRONMENT_ERROR', UNKNOWN_ERROR = 'UNKNOWN_ERROR', } ``` ## Release + bundle types ### `ReleaseInfo` The release descriptor the server returns from a check. ```ts interface ReleaseInfo { id: string; version: string; // Semver, e.g., '1.2.3' releaseNote: string | null; isMandatory: boolean; bundleHash: string; // SHA-256 of the bundle downloadUrl: string; // Signed URL downloadSize: number; // Bytes isPatch: boolean; // True when delta patch, false when full bundle signature?: string; // Ed25519 signature (base64) releasedAt: string; // ISO 8601 timestamp } ``` `UpdateInfo` is re-exported as an alias for `ReleaseInfo`: ```ts type UpdateInfo = ReleaseInfo; ``` ### `BundleInfo` The currently installed bundle. ```ts interface BundleInfo { hash: string; // SHA-256 version: string; installedAt: string; isOriginal: boolean; // True when the store-bundled JS is active slot: SlotState; environment: EnvironmentMode; } ``` ### `DownloadProgress` ```ts interface DownloadProgress { downloadedBytes: number; totalBytes: number; percentage: number; // 0–100 } ``` ## Orchestrator types ### `UpdatePhase` ```ts type UpdatePhase = | 'idle' | 'checking' | 'downloading' | 'ready' | 'applying' | 'error'; ``` ### `UpdateState` ```ts interface UpdateState { phase: UpdatePhase; current?: BundleInfo | null; pending?: UpdateInfo | null; lastCheckedAt?: Date | null; lastError?: SwiftPatchError | null; } ``` ### `UpdatePolicy` ```ts interface UpdatePolicy { prompt: 'always' | 'mandatory-only' | 'silent'; applyTiming: 'immediate' | 'on-resume' | 'on-next-launch'; } ``` ## Event types ### `SwiftpatchEvent` Discriminated union of every event on the bus. See [Events](/sdk/events) for details. ```ts type SwiftpatchEvent = | { type: 'update-available'; update: UpdateInfo } | { type: 'download-started'; hash: string; sizeBytes: number } | { type: 'download-progress'; hash: string; loaded: number; total: number } | { type: 'download-complete'; hash: string } | { type: 'update-ready'; update: UpdateInfo } | { type: 'update-applied'; update: UpdateInfo } | { type: 'update-dismissed'; update: UpdateInfo } | { type: 'rollback'; reason: string } | { type: 'error'; error: SwiftPatchError }; type SwiftpatchEventType = SwiftpatchEvent['type']; type Unsubscribe = () => void; ``` ## Error shapes ### `SwiftPatchError` ```ts interface SwiftPatchError { code: SwiftPatchErrorCode; message: string; details?: unknown; } ``` ## Breadcrumb types ### `Breadcrumb` / `BreadcrumbSink` / `BreadcrumbLevel` ```ts type BreadcrumbLevel = 'debug' | 'info' | 'warning' | 'error'; interface Breadcrumb { level: BreadcrumbLevel; category: string; // e.g. 'swiftpatch.download' message: string; data?: Record<string, string | number | boolean>; timestamp: number; // ms since epoch } type BreadcrumbSink = (crumb: Breadcrumb) => void; ``` ## Native config ```ts interface NativeConfig { projectId: string | null; appToken: string | null; appId: string | null; deploymentKey: string | null; serverUrl: string | null; publicKey: string | null; } ``` ## Next steps - [Methods](/sdk/methods) — how to call the APIs that use these types. - [Events](/sdk/events) — what triggers `SwiftpatchEvent` payloads. - [Error codes](/sdk/error-codes) — the full 33-code taxonomy behind `SwiftPatchError.code`. --- # UI Components <!-- source: docs/sdk/ui-components.md --> The SDK ships three React Native components for the full OTA lifecycle. They're opinionated, accessible, and themable. Drop them in and you're done — or use them as a starting point for a fully custom UI. All three are auto-rendered by `<SwiftPatchProvider>` when their corresponding prop is enabled. You can also import and render them yourself for fine-grained control. ## `<UpdateBanner />` A subtle, non-modal banner that slides in when an update is ready to apply. ```tsx import { UpdateBanner } from '@swiftpatch/react-native'; <UpdateBanner variant="subtle" position="bottom" behavior="dismissible" /> ``` **Rendered state:** ``` ┌───────────────────────────────────────────────┐ │ New update available │ │ v1.2.3 · Perf fixes + new feature [Later] │ │ [Apply now] │ └───────────────────────────────────────────────┘ ``` ### Props ```tsx interface UpdateBannerProps { variant?: 'subtle' | 'bold' | 'pulse'; position?: 'top' | 'bottom'; behavior?: 'dismissible' | 'persistent'; theme?: ThemeProp; title?: string; applyLabel?: string; dismissLabel?: string; onApply?: () => void; onDismiss?: () => void; style?: ViewStyle; disabled?: boolean; } ``` | Prop | Default | Description | |---|---|---| | `variant` | `'subtle'` | `subtle` uses the theme surface color. `bold` uses the accent. `pulse` adds a gentle scale loop on the CTA. | | `position` | `'bottom'` | `'top'` or `'bottom'`. | | `behavior` | `'dismissible'` | Show or hide the "Later" button. Set `'persistent'` to force an apply. | | `theme` | `'auto'` | `'auto'`, `'light'`, `'dark'`, or a partial `Theme` object. | | `title`, `applyLabel`, `dismissLabel` | — | Copy overrides. | | `onApply`, `onDismiss` | — | Fire after the respective verbs complete. | | `style` | — | Override the container style. | | `disabled` | `false` | Never render. | ### Accessibility The banner is announced via `accessibilityLiveRegion="polite"` and respects `prefers-reduced-motion` — it skips the slide/fade animation when the OS flag is on. ## `<UpdateBlocker />` A full-screen modal that appears when a mandatory release is pending. The user cannot dismiss it — the only action is Apply. ```tsx import { UpdateBlocker } from '@swiftpatch/react-native'; <UpdateBlocker logo={require('./assets/logo.png')} accentColor="#ff3366" title="Critical update" body="A security fix is installing. This takes a few seconds." applyLabel="Install now" /> ``` **Rendered state:** ``` ┌───────────────────────────────────────────────┐ │ │ │ [LOGO] │ │ │ │ Critical update │ │ │ │ A security fix is installing. │ │ This takes a few seconds. │ │ │ │ Downloading… 42% │ │ ████████░░░░░░░░░░░░░░ │ │ │ │ [ Install now ] │ │ │ └───────────────────────────────────────────────┘ ``` ### Props ```tsx interface UpdateBlockerProps { logo?: ImageSourcePropType; accentColor?: string; title?: string; body?: string; applyLabel?: string; theme?: ThemeProp; style?: ViewStyle; disabled?: boolean; } ``` While the download is in progress the blocker shows a progress bar. Once the bundle is ready, the Apply CTA becomes active. On press, the SDK installs and restarts. ## `<UpdateOnboarding />` A three-step carousel shown exactly once, after the very first OTA is applied. Use it to educate new users about how updates work. ```tsx import { UpdateOnboarding } from '@swiftpatch/react-native'; <UpdateOnboarding steps={[ { title: 'Your app is now faster', description: 'We updated in the background — no download, no wait.', }, { title: 'What is new', description: 'Dark mode, better performance, and 3 bug fixes.', }, { title: 'You are all set', description: 'Future updates will apply the same way.', }, ]} /> ``` ### Props ```tsx interface UpdateOnboardingProps { steps?: UpdateOnboardingStep[]; onDone?: () => void; theme?: ThemeProp; disabled?: boolean; continueLabel?: string; skipLabel?: string; style?: ViewStyle; } interface UpdateOnboardingStep { title: string; description: string; } ``` The "already shown" flag is persisted in native preferences. It never reappears on the same install, even after cold starts. ## Theming All three components share a single `Theme` type: ```ts interface Theme { background: string; surface: string; text: string; textMuted: string; accent: string; accentText: string; danger: string; border: string; } type ThemeProp = 'auto' | 'light' | 'dark' | Partial<Theme>; ``` Default themes: ```ts export const LIGHT_THEME: Theme = { background: '#ffffff', surface: '#f3f4f6', text: '#0f172a', textMuted: '#64748b', accent: '#2563eb', accentText: '#ffffff', danger: '#dc2626', border: '#e2e8f0', }; export const DARK_THEME: Theme = { background: '#0f0f23', surface: '#1a1a3e', text: '#ffffff', textMuted: '#94a3b8', accent: '#3b82f6', accentText: '#ffffff', danger: '#ef4444', border: '#252550', }; ``` ### Override a few colors ```tsx <UpdateBanner theme={{ accent: '#ff3366', accentText: '#ffffff' }} /> ``` ### Override the full theme ```tsx import { LIGHT_THEME } from '@swiftpatch/react-native'; const MY_THEME = { ...LIGHT_THEME, accent: '#ff3366', background: '#fafafa', }; <SwiftPatchProvider config={{ debug: __DEV__ }} theme={MY_THEME}> <App /> </SwiftPatchProvider> ``` ### Force light / dark ```tsx <UpdateBanner theme="dark" /> ``` ## Replacing the built-in UI Turn it all off and render your own: ```tsx <SwiftPatchProvider config={{ debug: __DEV__ }} autoShowBanner={false} blockOnMandatory={false} > <App /> <MyCustomUpdateBanner /> </SwiftPatchProvider> ``` Your custom component uses `useSwiftpatchUpdate()`: ```tsx import { useSwiftpatchUpdate } from '@swiftpatch/react-native'; function MyCustomUpdateBanner() { const { phase, pendingUpdate, applyNow, dismiss } = useSwiftpatchUpdate(); if (phase !== 'ready' || !pendingUpdate) return null; return ( <MyBrandedToast title={`v${pendingUpdate.version} is ready`} primaryAction={{ label: 'Apply', onPress: applyNow }} secondaryAction={{ label: 'Later', onPress: dismiss }} /> ); } ``` See [Hooks](/sdk/hooks) for the full hook API. ## Next steps - [Hooks](/sdk/hooks) — `useSwiftpatchUpdate`, `useSwiftpatchProgress`, `useSwiftpatchReady`. - [Provider reference](/sdk/provider) — `autoShowBanner`, `blockOnMandatory`, `theme`. - [Methods](/sdk/methods) — the imperative verbs the UI components call under the hood. --- # White-label <!-- source: docs/sdk/white-label.md --> # White-label & multi-tenant If you ship a single binary to multiple brands (white-label SaaS, B2B workspaces, multi-tenant apps), you don't want to rebuild the app for every tenant. Swiftpatch lets you rotate the deployment key at runtime — the next update check targets the new tenant's release line. ## The core API ```ts import { SwiftPatch } from '@swiftpatch/react-native'; const sp = new SwiftPatch({ deploymentKey: 'dep_default' }); await sp.init(); // Soft switch: next checkForUpdate uses the new tenant's releases. // Current bundle keeps running. await sp.setDeploymentKey('dep_tenant_a'); // Hard switch: roll back to the store bundle and wipe all slots. // User boots clean on the new tenant. await sp.setDeploymentKey('dep_tenant_b', { clearCurrentBundle: true }); // Read the persisted key const current = sp.getDeploymentKey(); // string | null // Subscribe to changes — for example, re-theme the app after auth. const unsub = sp.onTenantChanged((key) => { console.log('Tenant switched to:', key); reloadTenantTheme(); }); ``` The key is persisted in native preferences (UserDefaults on iOS, SharedPreferences on Android). It survives cold starts. ## Soft vs. hard switch ### Soft switch (default) ```ts await sp.setDeploymentKey('dep_tenant_a'); ``` - The current bundle keeps running. - The **next** `checkForUpdate` call targets the new tenant. - When a release is ready, the user sees the update banner. Use this when tenants share a compatible codebase and you just want to route to a different release line. ### Hard switch ```ts await sp.setDeploymentKey('dep_tenant_b', { clearCurrentBundle: true }); ``` - Rolls back to the store-embedded bundle. - Wipes `NEW`, `STABLE`, and temp slots. - User reboots on the factory JavaScript. Use this when the previous tenant's bundle is fundamentally incompatible — different UI shell, different backend, different behavior. You're effectively resetting the app to its shipped state, then letting the new tenant's releases take over. ## Integration patterns ### Pattern 1 — tenant from auth ```tsx function App() { const { user } = useAuth(); useEffect(() => { if (!user?.tenantKey) return; const sp = SwiftPatch.instance; // your shared instance sp.setDeploymentKey(user.tenantKey).catch((err) => { console.warn('Tenant switch failed:', err); }); }, [user?.tenantKey]); return <YourApp />; } ``` ### Pattern 2 — sign-out with clean slate ```tsx async function signOut() { await auth.signOut(); await sp.setDeploymentKey(DEFAULT_KEY, { clearCurrentBundle: true }); // User now running the shipped bundle, ready to re-auth into any tenant. } ``` ### Pattern 3 — per-tenant branded UI Listen for tenant changes and re-fetch theme tokens: ```ts sp.onTenantChanged(async (key) => { const tenantTheme = await fetchTenantTheme(key); themeStore.setTheme(tenantTheme); }); ``` ## CI/CD per tenant Each tenant gets its own deployment key in the dashboard. In CI, deploy to the right key: ```yaml title=".github/workflows/deploy-tenant.yml" strategy: matrix: tenant: [acme, contoso, fabrikam] steps: - name: Deploy OTA env: SWIFTPATCH_CI_TOKEN: ${{ secrets[format('TOKEN_{0}', matrix.tenant)] }} run: | npx swiftpatch-cli deploy \ -a ${{ matrix.tenant }}-app \ -p ios --hermes ``` The deployment key the binary holds determines which tenant's releases it picks up. Once you rotate the key at runtime (via `setDeploymentKey`), the same binary consumes a different release line. ## Considerations ### Crypto keys per tenant If each tenant has its own signing key, the binary must hold the matching public key. Two options: - **Ship every tenant's public key** in a map, select by tenant id. Slightly larger binary, no extra network. - **Fetch public keys from a trusted endpoint** on first use, cache them. Smaller binary, extra network dependency. The SDK supports overriding `publicKey` at init time. For runtime switching, re-initialize with the new key: ```ts // After setDeploymentKey, refresh the SDK to pick up the new public key. // Only needed if each tenant signs with a different key. ``` For teams using a single signing key across tenants (a common pattern when tenants share the same engineering team), no extra work is needed. ### Channel + tenant Channels and tenants compose — each tenant can have its own `production`, `staging`, `beta`. Set the channel per-tenant: ```ts await sp.setDeploymentKey(tenantKey); await setChannel(user.isBetaTester ? 'beta' : 'production'); ``` ## Next steps - [Channels](/sdk/channels) — channel switching within a single tenant. - [Methods](/sdk/methods) — the full `SwiftPatch` class API. - [CI/CD](/cli/ci-cd) — deploy to multiple tenants from one pipeline.