claude-toolkit 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -0
- package/docs/README.md +1 -0
- package/docs/best-practices/capacitor/README.md +64 -0
- package/docs/best-practices/capacitor/capacitor-8.md +60 -0
- package/docs/best-practices/capacitor/capgo-setup.md +151 -0
- package/docs/best-practices/capacitor/channels-and-rollouts.md +101 -0
- package/docs/best-practices/capacitor/live-updates-ota.md +75 -0
- package/docs/best-practices/capacitor/security-encryption.md +68 -0
- package/docs/stacks/capacitor-ota.md +145 -0
- package/package.json +1 -1
- package/src/detect.ts +12 -0
- package/src/types.ts +1 -0
- package/stacks/capacitor/skills/ct-capacitor-ota/SKILL.md +178 -0
- package/stacks/capacitor/stack.json +64 -0
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -92,6 +92,7 @@ This keeps your config aligned as your project evolves — stacks you add or rem
|
|
|
92
92
|
| `i18n-typesafe` | typesafe-i18n internationalization |
|
|
93
93
|
| `playwright` | Playwright E2E testing, Page Objects, fixtures, CI/CD |
|
|
94
94
|
| `storybook` | Storybook interaction testing, CSF 3, visual regression |
|
|
95
|
+
| `capacitor` | Capacitor 8 native runtime, Capgo OTA live updates, channels |
|
|
95
96
|
|
|
96
97
|
## Core Features (always included)
|
|
97
98
|
|
package/docs/README.md
CHANGED
|
@@ -50,3 +50,4 @@ Stack-specific skills activated based on your `claude-toolkit.config.ts` configu
|
|
|
50
50
|
| [ct-i18n-typesafe](stacks/i18n-typesafe.md) | `i18n-typesafe` | Type-safe internationalization with compile-time key checking |
|
|
51
51
|
| [ct-playwright-patterns](stacks/playwright-patterns.md) | `playwright` | E2E testing with Page Objects, fixtures, auth, network mocking, CI/CD |
|
|
52
52
|
| [ct-storybook-patterns](stacks/storybook-patterns.md) | `storybook` | Interaction testing, CSF 3, play functions, a11y, visual regression |
|
|
53
|
+
| [ct-capacitor-ota](stacks/capacitor-ota.md) | `capacitor` | Capacitor 8 native runtime and Capgo OTA live updates, channels, encryption |
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Capacitor & OTA Best Practices
|
|
2
|
+
|
|
3
|
+
> A curated collection of best practices for Capacitor 8 native apps and Capgo over-the-air (OTA) live updates, sourced from official Capacitor documentation, the Capgo docs and blog, and the plugin maintainers as of June 2026.
|
|
4
|
+
|
|
5
|
+
Capacitor wraps a web app (your `webDir` bundle) in a native iOS/Android shell. **OTA** lets you replace that web bundle on installed devices in minutes — without an app store review cycle. Capgo (`@capgo/capacitor-updater`) is the live-update layer that delivers, verifies, and rolls back those bundles.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────┐
|
|
9
|
+
│ Native shell (store) │ ← Capacitor core + plugins; ships via App Store / Play
|
|
10
|
+
│ ┌───────────────────────┐ │ Changes here REQUIRE a new binary.
|
|
11
|
+
│ │ Web bundle (OTA) │ │ ← JS / HTML / CSS; swapped by Capgo in minutes.
|
|
12
|
+
│ │ notifyAppReady() ✓ │ │ Changes here ship over the air.
|
|
13
|
+
│ └───────────────────────┘ │
|
|
14
|
+
└─────────────────────────────┘
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## The Two Layers — and the Golden Rule
|
|
18
|
+
|
|
19
|
+
| Layer | What it is | How it ships | Review latency |
|
|
20
|
+
| -------------- | ------------------------------------------- | --------------------------- | ------------------- |
|
|
21
|
+
| **Native** | Capacitor core, plugins, native deps | App Store / Google Play | Hours to days |
|
|
22
|
+
| **Web (OTA)** | Your compiled `webDir` (JS/HTML/CSS/assets) | Capgo channel swap | Minutes |
|
|
23
|
+
|
|
24
|
+
**Golden rule:** OTA the web layer only. If a change needs a native API the installed shell doesn't have — a new plugin, a native dependency, a Capacitor major bump — it **must** go through the store first. OTA-ing a native contract mismatch crashes the app. Every other rule in this guide follows from this one.
|
|
25
|
+
|
|
26
|
+
## When to Use Which Channel
|
|
27
|
+
|
|
28
|
+
| Question | Answer |
|
|
29
|
+
| ----------------------------------------------------------------- | --------------------------------------- |
|
|
30
|
+
| Did I change native code, add a plugin, or bump Capacitor major? | **Store binary** (then OTA on top) |
|
|
31
|
+
| Bug fix / copy change / styling / feature in JS only? | **OTA** to `production` |
|
|
32
|
+
| Needs QA sign-off before users see it? | **OTA** to `staging`, promote later |
|
|
33
|
+
| Testing on dev/emulator builds? | **OTA** to `development` |
|
|
34
|
+
| Need to undo a bad release right now? | **Channel rollback** (crown icon) |
|
|
35
|
+
|
|
36
|
+
## Technology Versions (as of June 2026)
|
|
37
|
+
|
|
38
|
+
| Tool | Version | Notes |
|
|
39
|
+
| -------------------------- | ------- | ---------------------------------------------------------------- |
|
|
40
|
+
| `@capacitor/core` & CLI | ^8.3.x | 8.3.1 (2026-04-16); Node 22+ required |
|
|
41
|
+
| `@capacitor/ios` | ^8.3.x | iOS 15.0 target, Xcode 26+, SPM default for new projects |
|
|
42
|
+
| `@capacitor/android` | ^8.3.x | minSdk 24, compileSdk/targetSdk 36, Gradle 8.14.3, Kotlin 2.2.20 |
|
|
43
|
+
| `@capgo/capacitor-updater` | ^8.x | Major tracks Capacitor major; v8 stores channel locally |
|
|
44
|
+
| `@capgo/cli` | latest | `npx @capgo/cli@latest …` — pin in CI |
|
|
45
|
+
|
|
46
|
+
## Shared Principles
|
|
47
|
+
|
|
48
|
+
1. **`notifyAppReady()` is non-negotiable.** Call it at the top of bootstrap. Without it, every OTA bundle auto-rolls back after `appReadyTimeout`.
|
|
49
|
+
2. **Match the bundle to the shell.** A web bundle is only valid against the native version that shipped it. Use channel version gates to prevent mismatches.
|
|
50
|
+
3. **Stage every rollout.** Canary to ~10%, widen over 24h. Instant rollback beats instant deploy.
|
|
51
|
+
4. **Sign your bundles.** End-to-end encryption (RSA-2048 + AES-256) means only your users can read an update — keep the private key out of git.
|
|
52
|
+
5. **Keep the cloud in control of production.** Omit `defaultChannel` from production binaries so the dashboard default governs rollout and rollback.
|
|
53
|
+
6. **Monitor before you widen.** Watch the `appReady` / `downloadFailed` ratio; a spike is your signal to roll back.
|
|
54
|
+
7. **Pin tooling.** Unpinned `@capgo/cli@latest` in CI can change upload behavior between runs.
|
|
55
|
+
|
|
56
|
+
## Guides
|
|
57
|
+
|
|
58
|
+
| Guide | Summary |
|
|
59
|
+
| ------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
60
|
+
| [Capacitor 8 Platform](capacitor-8.md) | Version floor, breaking changes, SPM-by-default, SystemBars, upgrade checklist. |
|
|
61
|
+
| [Live Updates & OTA Strategy](live-updates-ota.md) | What to OTA vs ship native, performance, delta updates, scheduling. |
|
|
62
|
+
| [Capgo Setup & API](capgo-setup.md) | Plugin install, config, `notifyAppReady`, autoUpdate modes, manual flow, CLI. |
|
|
63
|
+
| [Channels & Staged Rollouts](channels-and-rollouts.md) | Channel ladder, precedence, canary rollouts, rollback, monitoring. |
|
|
64
|
+
| [Security & Encryption](security-encryption.md) | E2E encryption (v2), key management, code signing, store compliance. |
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Capacitor 8 Platform
|
|
2
|
+
|
|
3
|
+
> Capacitor 8 raised the platform floor across the board. Get the toolchain right before touching OTA — a mismatched native baseline is the first thing that breaks live updates.
|
|
4
|
+
|
|
5
|
+
Latest at time of writing: **8.3.1** (2026-04-16). 8.3.0 (2026-03-25) and 8.2.0 (2026-03-06) precede it.
|
|
6
|
+
|
|
7
|
+
## Minimum Requirements
|
|
8
|
+
|
|
9
|
+
| Area | Requirement |
|
|
10
|
+
| ------- | --------------------------------------------------------------------------- |
|
|
11
|
+
| Node | **22+** (latest LTS recommended) |
|
|
12
|
+
| iOS | Xcode **26.0+**, deployment target **15.0** |
|
|
13
|
+
| Android | Android Studio Otter (2025.2.1)+, `minSdk 24`, `compileSdk 36`, `targetSdk 36` |
|
|
14
|
+
| Gradle | Android Gradle plugin **8.13.0**, wrapper **8.14.3**, Kotlin **2.2.20** |
|
|
15
|
+
|
|
16
|
+
## Headline Changes
|
|
17
|
+
|
|
18
|
+
### Swift Package Manager is the default (iOS)
|
|
19
|
+
|
|
20
|
+
New iOS projects scaffold with **SPM instead of CocoaPods**. Existing CocoaPods projects keep working — the ecosystem is migrating, not forcing a cutover. 8.3.0 added experimental config for `swift-tools-version` and support for SPM package traits in the generated `Package.swift`.
|
|
21
|
+
|
|
22
|
+
### Edge-to-edge via the SystemBars plugin (Android)
|
|
23
|
+
|
|
24
|
+
An internal `SystemBars` plugin now handles status/navigation bar appearance for immersive layouts automatically, with a public API for fine control. It version-gates behavior and coexists with `@capacitor/status-bar`. Consequently the old config flag **`android.adjustMarginsForEdgeToEdge` was removed** — use SystemBars instead.
|
|
25
|
+
|
|
26
|
+
## Breaking Changes Checklist
|
|
27
|
+
|
|
28
|
+
When upgrading to Capacitor 8:
|
|
29
|
+
|
|
30
|
+
- [ ] **Node 22+** on every dev machine and CI runner.
|
|
31
|
+
- [ ] **Android config syntax:** Gradle properties now require `=` (e.g. `compileSdk = 36`, not `compileSdk 36`).
|
|
32
|
+
- [ ] **Android layout rename:** `bridge_layout_main.xml` → `capacitor_bridge_layout_main.xml`.
|
|
33
|
+
- [ ] **Android resize:** add `density` to `configChanges` in `AndroidManifest.xml` to stop the WebView reloading on resize.
|
|
34
|
+
- [ ] **Remove `android.adjustMarginsForEdgeToEdge`** from `capacitor.config` — replaced by SystemBars.
|
|
35
|
+
- [ ] **iOS `appendUserAgent`:** the fixed concatenation may need a leading space to preserve your intended user-agent string.
|
|
36
|
+
- [ ] **iOS lifecycle:** the framework now emits `viewDidAppear` / `viewWillTransition` — delete any custom extensions that duplicated this.
|
|
37
|
+
|
|
38
|
+
## 8.2–8.3 Notable Fixes
|
|
39
|
+
|
|
40
|
+
These land automatically on patch upgrades but are worth knowing because they touch OTA-adjacent behavior:
|
|
41
|
+
|
|
42
|
+
- **Android `server.url` with paths** is now parsed correctly (8.3.0) — relevant if you point the WebView at a custom/self-hosted update host.
|
|
43
|
+
- **Android `isNewBinary()`** handles a null `versionName` (8.3.1) — the check that decides whether a native update invalidates OTA bundles.
|
|
44
|
+
- **HTTP `fetch`** handles `URL` objects and form-data boundary extraction (8.3.0/8.3.1).
|
|
45
|
+
- **CLI** inlines CSS sourcemaps alongside JS (8.3.0) and respects `CAPACITOR_COCOAPODS_PATH` (8.3.1).
|
|
46
|
+
|
|
47
|
+
## Keeping Native and OTA in Lockstep
|
|
48
|
+
|
|
49
|
+
The single most important interaction: **a native upgrade invalidates incompatible OTA bundles.** `resetWhenUpdate: true` (the Capgo default) wipes downloaded bundles when the native app updates, so users fall back to the bundle baked into the new binary rather than running a stale OTA bundle against new native code. Keep it on unless you have a specific reason not to.
|
|
50
|
+
|
|
51
|
+
When you bump Capacitor or add a plugin:
|
|
52
|
+
|
|
53
|
+
1. Ship a new store binary with the updated native layer.
|
|
54
|
+
2. Bump your bundle's version so old shells don't pull a bundle meant for the new native contract.
|
|
55
|
+
3. Use channel version gates (`--disable-auto-update major|minor`) to enforce the boundary.
|
|
56
|
+
|
|
57
|
+
## See Also
|
|
58
|
+
|
|
59
|
+
- [Live Updates & OTA Strategy](live-updates-ota.md) — what's safe to OTA on top of a given binary
|
|
60
|
+
- [Channels & Staged Rollouts](channels-and-rollouts.md) — version gates that enforce the native/web contract
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Capgo Setup & API
|
|
2
|
+
|
|
3
|
+
> `@capgo/capacitor-updater` ships in three modes: fully managed (Capgo Cloud), self-hosted (your own update server), or manual (you drive every download/apply in JS). This guide covers the managed path and the manual escape hatch.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i @capgo/capacitor-updater && npx cap sync
|
|
9
|
+
npx @capgo/cli@latest init <API_KEY>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`init` is an interactive onboarding: it registers the app in Capgo Cloud, injects the `notifyAppReady()` call, builds, uploads the first bundle, and verifies the update round-trips. Run it once per app.
|
|
13
|
+
|
|
14
|
+
## notifyAppReady() — Read This First
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { CapacitorUpdater } from "@capgo/capacitor-updater";
|
|
18
|
+
|
|
19
|
+
// As early as possible in bootstrap, before heavy async work.
|
|
20
|
+
CapacitorUpdater.notifyAppReady();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The plugin arms a timer (`appReadyTimeout`, default **10000ms**) the moment a bundle loads. Call `notifyAppReady()` before it fires and the bundle is confirmed; miss it and the plugin **auto-rolls back** to the previous bundle. This is the mechanism that makes a broken release self-heal — never remove it, and don't bury it behind slow startup code.
|
|
24
|
+
|
|
25
|
+
## Configuration Reference
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// capacitor.config.ts
|
|
29
|
+
import type { CapacitorConfig } from "@capacitor/cli";
|
|
30
|
+
|
|
31
|
+
const config: CapacitorConfig = {
|
|
32
|
+
appId: "com.example.app",
|
|
33
|
+
appName: "Example",
|
|
34
|
+
webDir: "dist",
|
|
35
|
+
plugins: {
|
|
36
|
+
CapacitorUpdater: {
|
|
37
|
+
autoUpdate: "atBackground",
|
|
38
|
+
appReadyTimeout: 10000,
|
|
39
|
+
responseTimeout: 20,
|
|
40
|
+
autoDeleteFailed: true,
|
|
41
|
+
autoDeletePrevious: true,
|
|
42
|
+
resetWhenUpdate: true,
|
|
43
|
+
// defaultChannel: "Development", // TEST/QA BUILDS ONLY
|
|
44
|
+
// publicKey: "...", // injected by `key create` for E2E encryption
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default config;
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| Setting | Default | Purpose |
|
|
53
|
+
| -------------------- | -------------------------------------- | ------------------------------------------------------------ |
|
|
54
|
+
| `autoUpdate` | `"atBackground"` | When to apply downloaded updates (see table below) |
|
|
55
|
+
| `appReadyTimeout` | `10000` (ms) | Grace period for `notifyAppReady()` before rollback |
|
|
56
|
+
| `responseTimeout` | `20` (s) | Update-server API call timeout |
|
|
57
|
+
| `periodCheckDelay` | `600` (s) | Poll interval; **cannot be < 600** |
|
|
58
|
+
| `autoDeleteFailed` | `true` | Remove bundles that failed to apply |
|
|
59
|
+
| `autoDeletePrevious` | `true` | Remove the prior bundle after a successful update |
|
|
60
|
+
| `resetWhenUpdate` | `true` | Wipe OTA bundles when the native app updates |
|
|
61
|
+
| `defaultChannel` | _(unset)_ | Pin a channel — **test builds only** |
|
|
62
|
+
| `allowModifyUrl` | `false` | Let JS change update/stats/channel URLs at runtime |
|
|
63
|
+
| `publicKey` | _(unset)_ | RSA public key for end-to-end encryption (v2) |
|
|
64
|
+
| `updateUrl` | `https://plugin.capgo.app/updates` | Update-check endpoint (override for self-hosted) |
|
|
65
|
+
| `statsUrl` | `https://plugin.capgo.app/stats` | Stats endpoint |
|
|
66
|
+
| `channelUrl` | `https://plugin.capgo.app/channel_self`| Channel self-assignment endpoint |
|
|
67
|
+
|
|
68
|
+
> `directUpdate` is deprecated. Use the `autoUpdate` string modes instead.
|
|
69
|
+
|
|
70
|
+
## autoUpdate Modes
|
|
71
|
+
|
|
72
|
+
| Value | Behavior |
|
|
73
|
+
| ---------------------------- | -------------------------------------------------------------- |
|
|
74
|
+
| `false` / `"off"` | No automatic updates — you drive the lifecycle from JS |
|
|
75
|
+
| `"atBackground"` *(default)* | Download silently; apply when the app moves to the background |
|
|
76
|
+
| `"atInstall"` | Apply immediately after a fresh install / native update |
|
|
77
|
+
| `"onLaunch"` | Apply immediately on next launch, then revert to background |
|
|
78
|
+
| `"always"` | Apply immediately whenever an update check runs |
|
|
79
|
+
| `"onlyDownload"` | Download but never auto-apply; emits `updateAvailable` |
|
|
80
|
+
|
|
81
|
+
**Default lifecycle:** on launch Capgo checks the channel → downloads any new bundle silently → waits for background/close → user gets the update on next launch. Seamless, no spinner.
|
|
82
|
+
|
|
83
|
+
## Manual Mode
|
|
84
|
+
|
|
85
|
+
Set `autoUpdate: false` to control everything yourself — useful for custom UX (e.g. "Update available" prompts) or applying only on explicit user action.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { CapacitorUpdater } from "@capgo/capacitor-updater";
|
|
89
|
+
import { App } from "@capacitor/app";
|
|
90
|
+
import { SplashScreen } from "@capacitor/splash-screen";
|
|
91
|
+
|
|
92
|
+
CapacitorUpdater.addListener("download", ({ percent }) => {
|
|
93
|
+
console.log(`Downloading: ${percent}%`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
App.addListener("appStateChange", async ({ isActive }) => {
|
|
97
|
+
if (isActive) {
|
|
98
|
+
const latest = await CapacitorUpdater.getLatest();
|
|
99
|
+
if (latest.url) {
|
|
100
|
+
const bundle = await CapacitorUpdater.download({
|
|
101
|
+
version: latest.version,
|
|
102
|
+
url: latest.url,
|
|
103
|
+
});
|
|
104
|
+
await CapacitorUpdater.next({ id: bundle.id }); // queue for next background
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Core methods
|
|
111
|
+
|
|
112
|
+
| Method | Purpose |
|
|
113
|
+
| ----------------- | -------------------------------------------------------------- |
|
|
114
|
+
| `notifyAppReady()`| Confirm the current bundle booted (always required) |
|
|
115
|
+
| `getLatest()` | Query the server for the latest bundle metadata |
|
|
116
|
+
| `download()` | Download a bundle by `{ version, url }` |
|
|
117
|
+
| `next({ id })` | Queue a bundle to apply on next background |
|
|
118
|
+
| `set({ id })` | Apply a bundle now — **destroys the JS context** (hard reload) |
|
|
119
|
+
| `current()` | Inspect the active bundle + native version |
|
|
120
|
+
| `reload()` | Apply a pending update without backgrounding |
|
|
121
|
+
| `reset()` | Revert to the bundle baked into the native binary |
|
|
122
|
+
| `setChannel()` | Switch the device's channel at runtime (v8+, self-assign) |
|
|
123
|
+
|
|
124
|
+
### Events
|
|
125
|
+
|
|
126
|
+
`download` · `downloadComplete` · `updateAvailable` · `downloadFailed` · `appReady` · `majorAvailable`
|
|
127
|
+
|
|
128
|
+
Subscribe to `downloadFailed` and `appReady` for monitoring; subscribe to `majorAvailable` to detect a bundle that requires a native update you can't OTA.
|
|
129
|
+
|
|
130
|
+
## CLI Cheat Sheet
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npx @capgo/cli@latest login <API_KEY> # store key (--local to scope to repo)
|
|
134
|
+
npx @capgo/cli@latest app add com.example.app # register an app
|
|
135
|
+
npx @capgo/cli@latest bundle upload --channel=production # build → upload → assign
|
|
136
|
+
npx @capgo/cli@latest bundle upload --key-v2 --channel=production # encrypted upload
|
|
137
|
+
npx @capgo/cli@latest bundle upload --delta-only # require delta-capable clients
|
|
138
|
+
npx @capgo/cli@latest bundle compatibility -c production # check native compatibility first
|
|
139
|
+
npx @capgo/cli@latest channel set production -s default # set the cloud default channel
|
|
140
|
+
npx @capgo/cli@latest key create # generate the E2E key pair
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Useful `bundle upload` flags: `--channel`, `--key-v2` / `--no-key`, `--delta` / `--delta-only`, `--external` (link your own storage), `--tus` (resumable upload), `--encrypted-checksum`.
|
|
144
|
+
|
|
145
|
+
**Pin the CLI version in CI** (`@capgo/cli@8.x` rather than `@latest`) so upload behavior doesn't change between pipeline runs.
|
|
146
|
+
|
|
147
|
+
## See Also
|
|
148
|
+
|
|
149
|
+
- [Channels & Staged Rollouts](channels-and-rollouts.md) — what `--channel` and `setChannel` actually do
|
|
150
|
+
- [Security & Encryption](security-encryption.md) — the `--key-v2` flow in full
|
|
151
|
+
- [Live Updates & OTA Strategy](live-updates-ota.md) — when to use each `autoUpdate` mode
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Channels & Staged Rollouts
|
|
2
|
+
|
|
3
|
+
> A channel is a pointer to one JS bundle. Devices subscribe to a channel; you change which bundle the channel points to. That indirection is what makes rollout and rollback instant and rebuild-free.
|
|
4
|
+
|
|
5
|
+
## The Channel Ladder
|
|
6
|
+
|
|
7
|
+
Mirror your release pipeline with channels, and version bundles with semver pre-release tags so relationships are obvious:
|
|
8
|
+
|
|
9
|
+
| Channel | Bundle version | Audience |
|
|
10
|
+
| ------------- | -------------- | ------------------------- |
|
|
11
|
+
| `development` | `1.2.3-dev.1` | dev / emulator builds |
|
|
12
|
+
| `qa` | `1.2.3-qa.1` | QA verification |
|
|
13
|
+
| `staging` | `1.2.3-rc.1` | production-like sign-off |
|
|
14
|
+
| `production` | `1.2.3` | end users (store version) |
|
|
15
|
+
|
|
16
|
+
Promote a build up the ladder by uploading (or re-pointing) it to the next channel — no native rebuild between stages.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @capgo/cli@latest bundle upload --channel=staging # ship to staging
|
|
20
|
+
# ...QA signs off...
|
|
21
|
+
npx @capgo/cli@latest bundle upload --channel=production # promote to production
|
|
22
|
+
npx @capgo/cli@latest channel set production -s default # ensure it's the cloud default
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Channel Precedence
|
|
26
|
+
|
|
27
|
+
When a device checks for updates, Capgo resolves the channel in this order (highest wins):
|
|
28
|
+
|
|
29
|
+
1. **Forced device mapping** — pin specific device IDs (testing/exception layer).
|
|
30
|
+
2. **Cloud per-device override** — dashboard/API change for one device (up to ~2 min to propagate).
|
|
31
|
+
3. **Config `defaultChannel`** — set in `capacitor.config` (baked into the binary).
|
|
32
|
+
4. **Cloud Default Channel** — the fallback ~99% of users follow.
|
|
33
|
+
|
|
34
|
+
**Production rule:** ship production binaries **without** `defaultChannel`. If you bake a channel into the binary, those users stop following the cloud default — which is exactly the lever you use for instant rollout and rollback. Reserve `defaultChannel` for builds you hand directly to testers.
|
|
35
|
+
|
|
36
|
+
## Runtime Switching with setChannel()
|
|
37
|
+
|
|
38
|
+
Let users opt into a beta from inside the app:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { CapacitorUpdater } from "@capgo/capacitor-updater";
|
|
42
|
+
|
|
43
|
+
await CapacitorUpdater.setChannel({ channel: "beta", triggerAutoUpdate: true });
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- Requires **"Allow device self-assignment"** enabled on the target channel; otherwise the call fails.
|
|
47
|
+
- In **plugin v8+**, `setChannel` stores the choice **locally on the device** and takes effect on the next update check — no 2-minute backend replication lag. The backend still validates the self-assignment permission at check time.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx @capgo/cli@latest channel set beta --self-assign # enable opt-in
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Version Gates: Enforce the Native/Web Contract
|
|
54
|
+
|
|
55
|
+
Channels can refuse updates that cross a version boundary the native shell can't honor:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Block cross-major OTA on production (e.g. don't push a 2.x bundle to 1.x shells)
|
|
59
|
+
npx @capgo/cli@latest channel set production --disable-auto-update major
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| `--disable-auto-update` | Allows |
|
|
63
|
+
| ----------------------- | --------------------------------------------------- |
|
|
64
|
+
| `major` | minor + patch within the same major |
|
|
65
|
+
| `minor` | patch only within the same major.minor |
|
|
66
|
+
| `patch` | patch increments only |
|
|
67
|
+
| `metadata` | requires minimum version metadata on bundles |
|
|
68
|
+
| `none` | all semver-compatible updates |
|
|
69
|
+
|
|
70
|
+
Also available: **disable auto-downgrade** (don't send a bundle older than the device's native version) and per-platform targeting (`--ios`, `--android`). These gates are how you keep a web bundle from landing on a shell that can't run it.
|
|
71
|
+
|
|
72
|
+
## Staged Rollout Playbook
|
|
73
|
+
|
|
74
|
+
1. **Canary** — point the channel at the new bundle for ~10% of users (or a dedicated `canary` channel).
|
|
75
|
+
2. **Observe** — watch the success signal for 1–2 hours (next section).
|
|
76
|
+
3. **Widen** — ramp to 100% over ~24h if healthy.
|
|
77
|
+
4. **Schedule** — time heavy rollouts for **1–4 AM local**, Wi-Fi-preferred, so downloads don't compete with active use.
|
|
78
|
+
|
|
79
|
+
**Benchmarks:** aim for **~95% adoption within 24h** and **~82% global success rate**. Falling short is a signal to pause and investigate, not to widen.
|
|
80
|
+
|
|
81
|
+
## Rollback
|
|
82
|
+
|
|
83
|
+
Instant and rebuild-free — the core OTA payoff:
|
|
84
|
+
|
|
85
|
+
1. Open the channel in the Capgo dashboard.
|
|
86
|
+
2. Find the previous good build and click the **crown icon**.
|
|
87
|
+
3. Confirm. The channel now points at the old bundle; devices pick it up on next check-in.
|
|
88
|
+
|
|
89
|
+
Because rollback is this cheap, bias toward shipping small and reverting fast over holding releases for exhaustive pre-launch testing.
|
|
90
|
+
|
|
91
|
+
## Monitoring
|
|
92
|
+
|
|
93
|
+
- **`appReady` vs `downloadFailed`** — the health ratio. A `downloadFailed` spike, or `appReady` events not keeping pace with downloads, means bundles are failing to boot → roll back.
|
|
94
|
+
- **Capgo stats dashboard** — adoption curve and per-version success rate. Compare against the 95% / 82% benchmarks.
|
|
95
|
+
- **Alert, then act** — wire a threshold alert so a bad canary pages you before you widen.
|
|
96
|
+
|
|
97
|
+
## See Also
|
|
98
|
+
|
|
99
|
+
- [Live Updates & OTA Strategy](live-updates-ota.md) — why staged rollout is the whole point
|
|
100
|
+
- [Capacitor 8 Platform](capacitor-8.md) — what makes a bundle native-incompatible
|
|
101
|
+
- [Security & Encryption](security-encryption.md) — signing the bundles a channel serves
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Live Updates & OTA Strategy
|
|
2
|
+
|
|
3
|
+
> OTA is a delivery mechanism for the **web layer**, not a way around the app stores. The strategy is mostly about discipline: knowing what you can ship over the air, keeping bundles small, and rolling out so a mistake is recoverable.
|
|
4
|
+
|
|
5
|
+
## What You Can — and Cannot — Ship Over the Air
|
|
6
|
+
|
|
7
|
+
| Change | OTA? | Why |
|
|
8
|
+
| ----------------------------------------------- | ---- | ----------------------------------------------------------- |
|
|
9
|
+
| Bug fix in JS/TS | ✅ | Pure web layer |
|
|
10
|
+
| Copy, translations, styling | ✅ | Static assets in the bundle |
|
|
11
|
+
| New feature built on existing native APIs | ✅ | No new native contract |
|
|
12
|
+
| Web-only dependency upgrade | ✅ | Compiles into the bundle |
|
|
13
|
+
| New Capacitor **plugin** (native code) | ❌ | Native shell lacks the implementation → crash |
|
|
14
|
+
| Native dependency / SDK upgrade | ❌ | Native binary change |
|
|
15
|
+
| Capacitor **major** version bump | ❌ | Native runtime change |
|
|
16
|
+
| Permission / entitlement / manifest change | ❌ | Declared in the native binary |
|
|
17
|
+
|
|
18
|
+
The failure mode is concrete: a bundle that calls `SomePlugin.doThing()` on a shell where that plugin isn't compiled in throws at runtime. **Match every bundle to the native version that can run it.**
|
|
19
|
+
|
|
20
|
+
## Store Compliance
|
|
21
|
+
|
|
22
|
+
OTA of interpreted code is explicitly permitted:
|
|
23
|
+
|
|
24
|
+
- **Apple** — Developer Agreement **§3.3.2** allows OTA of JavaScript and assets executed by the system WebView (since iOS 4.3).
|
|
25
|
+
- **Google Play** — the restriction on non-Play code updates **does not apply** to code running in a VM with limited API access, i.e. JavaScript in a WebView.
|
|
26
|
+
|
|
27
|
+
What gets apps pulled is shipping *native* executable code OTA. Stay in the web layer and you stay compliant.
|
|
28
|
+
|
|
29
|
+
## Performance: Keep Bundles Small
|
|
30
|
+
|
|
31
|
+
Smaller bundles download faster, fail less, and adopt quicker.
|
|
32
|
+
|
|
33
|
+
- **Delta / differential updates.** Capgo transfers only the files that changed between versions (auto-enabled with direct/instant updates). The win depends on file-level stability — see below.
|
|
34
|
+
- **Deterministic builds.** Strip build timestamps and pin your build tooling so unchanged source produces byte-identical output. Otherwise every file looks "changed" and the delta degrades to a full download.
|
|
35
|
+
- **Compression.** Capgo applies ZSTD compression to full-image updates; you don't configure it, but deterministic builds are what let delta + compression actually shrink the payload.
|
|
36
|
+
- **Prioritize the critical path.** Load auth and main navigation eagerly; lazy-load analytics, settings, animations, and heavy media after first paint. A small critical bundle reaches `notifyAppReady()` faster, which matters because of the rollback timeout.
|
|
37
|
+
|
|
38
|
+
## Update Timing & Scheduling
|
|
39
|
+
|
|
40
|
+
- **Off-peak rollouts.** Schedule heavy rollouts for **1–4 AM local time** and prefer Wi-Fi so you're not competing with active use or burning cellular data.
|
|
41
|
+
- **Background application.** The default `atBackground` mode downloads silently and applies when the app backgrounds, so users never watch a spinner. Reserve `onLaunch` / `always` for updates that must take effect immediately (e.g. a hotfix).
|
|
42
|
+
- **Respect `periodCheckDelay`.** Minimum 600s (10 min). Don't try to poll faster — it's clamped.
|
|
43
|
+
|
|
44
|
+
## Roll Out So You Can Roll Back
|
|
45
|
+
|
|
46
|
+
The whole point of OTA is that mistakes are cheap to undo. Treat every release as staged:
|
|
47
|
+
|
|
48
|
+
1. **Canary** to ~10% of users.
|
|
49
|
+
2. **Watch** the success signal (`appReady` vs `downloadFailed`, Capgo stats) for 1–2 hours.
|
|
50
|
+
3. **Widen** to 100% over ~24h if healthy.
|
|
51
|
+
4. **Roll back instantly** from the channel dashboard if not — devices pick up the previous bundle on next check-in.
|
|
52
|
+
|
|
53
|
+
Healthy benchmarks: **~95% adoption within 24h**, **~82% global success rate**. Below that, investigate before widening.
|
|
54
|
+
|
|
55
|
+
## The notifyAppReady() Safety Net
|
|
56
|
+
|
|
57
|
+
This is what makes the above safe. The plugin starts a timer (`appReadyTimeout`, default 10s) when a new bundle loads. If your JS calls `notifyAppReady()` before it fires, the bundle is "confirmed." If it doesn't — because the bundle white-screened or threw on boot — the plugin **automatically reverts** to the last good bundle. A broken OTA release self-heals on the next launch even before you notice.
|
|
58
|
+
|
|
59
|
+
Call it as early as possible, before heavy async work, so a slow boot doesn't trip the timeout on an otherwise-fine bundle.
|
|
60
|
+
|
|
61
|
+
## Anti-Patterns
|
|
62
|
+
|
|
63
|
+
| Anti-pattern | Consequence |
|
|
64
|
+
| ------------------------------------------- | ----------------------------------------------------------------- |
|
|
65
|
+
| OTA-ing a native change | Runtime crash on the mismatched shell |
|
|
66
|
+
| Non-deterministic builds | Delta updates degrade to full downloads |
|
|
67
|
+
| 100% rollout, no canary | A bad bundle hits the entire user base at once |
|
|
68
|
+
| Eager-loading everything | Slow boot risks tripping `appReadyTimeout` → false rollback |
|
|
69
|
+
| Polling faster than 600s | Silently clamped; wasted assumption |
|
|
70
|
+
|
|
71
|
+
## See Also
|
|
72
|
+
|
|
73
|
+
- [Capacitor 8 Platform](capacitor-8.md) — keeping native and OTA in lockstep
|
|
74
|
+
- [Capgo Setup & API](capgo-setup.md) — config and the manual update lifecycle
|
|
75
|
+
- [Channels & Staged Rollouts](channels-and-rollouts.md) — the mechanics of canarying and rollback
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Security & Encryption
|
|
2
|
+
|
|
3
|
+
> An OTA bundle travels over the network and lands on devices you don't control. End-to-end encryption with code signing makes each update unreadable in transit and verifiable on arrival — only your users can decrypt it, and only your signed bundles are accepted.
|
|
4
|
+
|
|
5
|
+
## Why Encrypt
|
|
6
|
+
|
|
7
|
+
A channel controls *who is eligible* for an update; it does **not** keep the bundle secret. An unencrypted bundle should be treated as a public asset. If your web layer contains anything you don't want readable — proprietary logic, embedded config — encryption is the only thing that keeps it private end to end, including from Capgo itself.
|
|
8
|
+
|
|
9
|
+
## How v2 Encryption Works (Hybrid RSA + AES)
|
|
10
|
+
|
|
11
|
+
RSA can't efficiently encrypt large payloads, so Capgo uses a hybrid scheme:
|
|
12
|
+
|
|
13
|
+
1. A **random AES-256 key** is generated for every upload and encrypts the bundle.
|
|
14
|
+
2. Your **private RSA-2048 key** encrypts (signs) the AES key and the bundle checksum.
|
|
15
|
+
3. The app holds your **public RSA key** (injected at build) and decrypts the AES key + signature.
|
|
16
|
+
4. The app decrypts the bundle with the AES key, recomputes the checksum, and compares it against the decrypted signature.
|
|
17
|
+
|
|
18
|
+
The result: the bundle is confidential (AES) **and** authenticated (RSA signature over the checksum). Tampering breaks the checksum match; a bundle not signed by your private key won't validate.
|
|
19
|
+
|
|
20
|
+
> Industry baseline this matches: **AES-256** for payloads, **RSA-2048** for key exchange, **SHA-256** for integrity.
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @capgo/cli@latest key create # generate the RSA key pair
|
|
26
|
+
npx @capgo/cli@latest bundle upload --key-v2 --channel=production
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`key create` writes two files and injects the public key into your Capacitor config:
|
|
30
|
+
|
|
31
|
+
| File | Sensitivity | Rule |
|
|
32
|
+
| --------------------- | ----------- | ---------------------------------------------------------- |
|
|
33
|
+
| `.capgo_key_v2` | **Private** | **Never commit.** Store as a CI secret. Loss = can't sign. |
|
|
34
|
+
| `.capgo_key_v2.pub` | Public | Safe to commit — it's a backup of the embedded public key. |
|
|
35
|
+
|
|
36
|
+
Add `.capgo_key_v2` to `.gitignore` immediately. The private key only ever lives on trusted dev machines and in CI secret storage; it's used at upload time to sign, never shipped to devices.
|
|
37
|
+
|
|
38
|
+
## Strict Enforcement
|
|
39
|
+
|
|
40
|
+
Once encryption is configured, the plugin **requires** a valid signature on every update — this is enforced, not advisory. An unsigned or tampered bundle is **rejected**, not applied. Practical consequences:
|
|
41
|
+
|
|
42
|
+
- Every CI upload must run with `--key-v2` and access to the private key, or devices will reject the bundle.
|
|
43
|
+
- Rotating keys requires shipping a new native binary with the new public key; plan it as a store release, not an OTA.
|
|
44
|
+
- `--no-key` (unencrypted) and encrypted uploads don't mix on the same channel for the same audience — pick one posture per channel.
|
|
45
|
+
|
|
46
|
+
## Integrity Beyond Encryption
|
|
47
|
+
|
|
48
|
+
- **Checksums** verify the decrypted bundle matches what you signed — corruption or tampering fails closed.
|
|
49
|
+
- **`notifyAppReady()`** is the runtime backstop: even a validly-signed bundle that crashes on boot rolls back automatically (see [Capgo Setup](capgo-setup.md)). Signing proves *authenticity*; `notifyAppReady` proves *it actually runs*. You want both.
|
|
50
|
+
|
|
51
|
+
## Store Compliance
|
|
52
|
+
|
|
53
|
+
Encryption doesn't change what you're allowed to ship — it just protects the payload. The compliance rule is unchanged: OTA of **JavaScript/HTML/CSS** is permitted (Apple Developer Agreement §3.3.2; Google Play's webview/JS-VM exemption). Encrypting that web bundle is fine. Encrypting and shipping *native* executable code OTA is still not allowed — encryption is not a loophole around the native/web boundary.
|
|
54
|
+
|
|
55
|
+
## Checklist
|
|
56
|
+
|
|
57
|
+
- [ ] `npx @capgo/cli key create` run; `.capgo_key_v2` added to `.gitignore`.
|
|
58
|
+
- [ ] Private key stored as a CI secret; `--key-v2` used on every production upload.
|
|
59
|
+
- [ ] `.capgo_key_v2.pub` committed as a backup.
|
|
60
|
+
- [ ] `notifyAppReady()` present so signed-but-broken bundles still self-heal.
|
|
61
|
+
- [ ] Key rotation planned as a native binary release, not an OTA.
|
|
62
|
+
- [ ] Only web-layer code shipped OTA — never native code, encrypted or not.
|
|
63
|
+
|
|
64
|
+
## See Also
|
|
65
|
+
|
|
66
|
+
- [Capgo Setup & API](capgo-setup.md) — the upload flow and `publicKey` config
|
|
67
|
+
- [Channels & Staged Rollouts](channels-and-rollouts.md) — per-channel encryption posture
|
|
68
|
+
- [Live Updates & OTA Strategy](live-updates-ota.md) — what's legal to ship over the air
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Capacitor OTA (Capgo) Patterns
|
|
2
|
+
|
|
3
|
+
> Capacitor 8 native runtime and Capgo over-the-air live updates: channels, encryption, staged rollouts, and store-safe delivery.
|
|
4
|
+
|
|
5
|
+
**Type:** Stack Skill (requires `capacitor` stack)
|
|
6
|
+
**Source:** [`stacks/capacitor/skills/ct-capacitor-ota/SKILL.md`](../../stacks/capacitor/skills/ct-capacitor-ota/SKILL.md)
|
|
7
|
+
**Directory Mappings:** `capacitor.config.ts`, `capacitor.config.json`, `ios/`, `android/`
|
|
8
|
+
**File Extensions:** _(none — keyed on config + content)_
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Capacitor wraps a web app in a native iOS/Android shell. Capgo's `@capgo/capacitor-updater` lets you replace the web bundle that shell loads, shipping JS/HTML/CSS fixes in minutes instead of waiting on store review.
|
|
13
|
+
|
|
14
|
+
**Golden rule:** OTA updates the **web layer only**. Native code changes (new plugin, native dependency, Capacitor major bump) require a new store binary. Never OTA a bundle whose native contract differs from the installed shell.
|
|
15
|
+
|
|
16
|
+
## Versions (as of June 2026)
|
|
17
|
+
|
|
18
|
+
| Package | Version | Notes |
|
|
19
|
+
| -------------------------- | ------- | ---------------------------------------------------------------- |
|
|
20
|
+
| `@capacitor/core` & CLI | ^8.3.x | 8.3.1 (2026-04-16); Node 22+, SPM default for new iOS apps |
|
|
21
|
+
| `@capgo/capacitor-updater` | ^8.x | Major version tracks Capacitor major; v8 stores channels locally |
|
|
22
|
+
| `@capgo/cli` | latest | `npx @capgo/cli@latest …` — pin in CI |
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm i @capgo/capacitor-updater && npx cap sync
|
|
28
|
+
npx @capgo/cli@latest init <API_KEY> # adds app, injects notifyAppReady, builds, uploads, tests
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Critical: notifyAppReady()
|
|
32
|
+
|
|
33
|
+
Call it as early as possible after the bundle boots. If the plugin doesn't hear it within `appReadyTimeout` (default 10s), it assumes the bundle is broken and **auto-rolls back** to the previous one — the safety net that makes OTA reversible.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { CapacitorUpdater } from "@capgo/capacitor-updater";
|
|
37
|
+
|
|
38
|
+
CapacitorUpdater.notifyAppReady(); // top of bootstrap, before heavy async work
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Configuration
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// capacitor.config.ts
|
|
45
|
+
import type { CapacitorConfig } from "@capacitor/cli";
|
|
46
|
+
|
|
47
|
+
const config: CapacitorConfig = {
|
|
48
|
+
appId: "com.example.app",
|
|
49
|
+
appName: "Example",
|
|
50
|
+
webDir: "dist",
|
|
51
|
+
plugins: {
|
|
52
|
+
CapacitorUpdater: {
|
|
53
|
+
autoUpdate: "atBackground", // apply when app backgrounds
|
|
54
|
+
appReadyTimeout: 10000,
|
|
55
|
+
autoDeleteFailed: true,
|
|
56
|
+
autoDeletePrevious: true,
|
|
57
|
+
resetWhenUpdate: true, // wipe OTA bundles on native app update
|
|
58
|
+
// defaultChannel: "Development", // TEST/QA BUILDS ONLY — omit in production
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default config;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### autoUpdate modes
|
|
67
|
+
|
|
68
|
+
| Value | Behavior |
|
|
69
|
+
| ---------------------------- | ------------------------------------------------------------ |
|
|
70
|
+
| `false` / `"off"` | No automatic updates — drive everything from JS |
|
|
71
|
+
| `"atBackground"` *(default)* | Download silently, apply when app moves to background |
|
|
72
|
+
| `"atInstall"` | Apply right after a fresh install / native update |
|
|
73
|
+
| `"onLaunch"` | Apply on next launch, then background |
|
|
74
|
+
| `"always"` | Apply immediately whenever a check runs |
|
|
75
|
+
| `"onlyDownload"` | Download only; emit `updateAvailable` for you to act |
|
|
76
|
+
|
|
77
|
+
`periodCheckDelay` sets poll frequency (seconds, **minimum 600** / 10 min).
|
|
78
|
+
|
|
79
|
+
## Channels
|
|
80
|
+
|
|
81
|
+
A channel points to one JS bundle. Swap which bundle a channel points to for instant rollout/rollback without rebuilding.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx @capgo/cli@latest bundle upload --channel=production # build → upload → assign
|
|
85
|
+
npx @capgo/cli@latest channel set production -s default # make it the cloud default
|
|
86
|
+
npx @capgo/cli@latest channel set beta --self-assign # allow in-app setChannel()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Precedence** (highest first): forced device mapping → cloud per-device override → config `defaultChannel` → cloud Default Channel.
|
|
90
|
+
|
|
91
|
+
Ship production binaries **without** `defaultChannel` so users follow the cloud default. Recommended ladder:
|
|
92
|
+
|
|
93
|
+
| Channel | Bundle version | Audience |
|
|
94
|
+
| ------------- | -------------- | ------------------------- |
|
|
95
|
+
| `development` | `1.2.3-dev.1` | dev / emulator builds |
|
|
96
|
+
| `qa` | `1.2.3-qa.1` | QA verification |
|
|
97
|
+
| `staging` | `1.2.3-rc.1` | production-like sign-off |
|
|
98
|
+
| `production` | `1.2.3` | end users (store version) |
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// Runtime switch — needs "Allow device self-assignment" on the channel (v8+).
|
|
102
|
+
await CapacitorUpdater.setChannel({ channel: "beta", triggerAutoUpdate: true });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## End-to-end encryption (v2)
|
|
106
|
+
|
|
107
|
+
Hybrid RSA-2048 + AES-256: a random AES key encrypts each bundle; your private RSA key signs the key + checksum; the app decrypts with the embedded public key.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx @capgo/cli@latest key create
|
|
111
|
+
npx @capgo/cli@latest bundle upload --key-v2 --channel=production
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- `.capgo_key_v2` (private) — **never commit**; store as a CI secret.
|
|
115
|
+
- `.capgo_key_v2.pub` (public) — safe to commit.
|
|
116
|
+
- With encryption configured, the plugin **strictly enforces** a valid signature; tampered/unsigned bundles are rejected.
|
|
117
|
+
|
|
118
|
+
## Staged rollouts & monitoring
|
|
119
|
+
|
|
120
|
+
- Start at ~10% of users, widen over 24h; schedule heavy rollouts 1–4 AM local, Wi-Fi-preferred.
|
|
121
|
+
- Targets: ~95% adoption within 24h, ~82% global success rate.
|
|
122
|
+
- Roll back instantly from the channel dashboard (crown icon on the previous build).
|
|
123
|
+
- Watch the `downloadFailed` / `appReady` ratio in Capgo stats; a spike means roll back.
|
|
124
|
+
|
|
125
|
+
## Store compliance
|
|
126
|
+
|
|
127
|
+
OTA of JS/HTML/CSS is permitted — Apple §3.3.2 (since iOS 4.3) and Google Play (webview/JS VM code is exempt). Shipping native code OTA is not.
|
|
128
|
+
|
|
129
|
+
## Anti-Patterns
|
|
130
|
+
|
|
131
|
+
| Anti-pattern | Why it's wrong |
|
|
132
|
+
| ----------------------------------------- | ------------------------------------------------------------------------ |
|
|
133
|
+
| **Not calling `notifyAppReady()`** | Every bundle auto-rolls back after `appReadyTimeout`. The #1 OTA failure. |
|
|
134
|
+
| **OTA-ing a native change** | Bundle needs a native API the shell lacks → crash. Bump the binary first. |
|
|
135
|
+
| **`defaultChannel` in production builds** | Pins users off the cloud default; breaks instant rollback. |
|
|
136
|
+
| **Committing `.capgo_key_v2`** | Leaks your signing key. Commit only the `.pub`. |
|
|
137
|
+
| **`periodCheckDelay` < 600** | Silently clamped; never polls faster than 10 min. |
|
|
138
|
+
| **100% rollout with no canary** | A bad bundle hits everyone at once. Stage it. |
|
|
139
|
+
| **Unpinned `@capgo/cli@latest` in CI** | A CLI minor can change upload behavior mid-pipeline. Pin it. |
|
|
140
|
+
|
|
141
|
+
## See Also
|
|
142
|
+
|
|
143
|
+
- [Capacitor & OTA Best Practices](../best-practices/capacitor/README.md) — full reference set
|
|
144
|
+
- [ct-vite-vitest-patterns](vite-vitest-patterns.md) — the build that produces `webDir`
|
|
145
|
+
- [ct-playwright-patterns](playwright-patterns.md) — E2E-verify a bundle before upload
|
package/package.json
CHANGED
package/src/detect.ts
CHANGED
|
@@ -126,6 +126,18 @@ const DETECTORS: StackDetector[] = [
|
|
|
126
126
|
return null;
|
|
127
127
|
},
|
|
128
128
|
},
|
|
129
|
+
{
|
|
130
|
+
name: "capacitor",
|
|
131
|
+
detect: (dir, pkg) => {
|
|
132
|
+
if (hasDep(pkg, "@capgo/capacitor-updater"))
|
|
133
|
+
return { name: "capacitor", reason: "found @capgo/capacitor-updater in dependencies" };
|
|
134
|
+
if (hasDep(pkg, "@capacitor/core"))
|
|
135
|
+
return { name: "capacitor", reason: "found @capacitor/core in dependencies" };
|
|
136
|
+
const config = rootConfigExists(dir, "capacitor.config");
|
|
137
|
+
if (config) return { name: "capacitor", reason: `found ${config}` };
|
|
138
|
+
return null;
|
|
139
|
+
},
|
|
140
|
+
},
|
|
129
141
|
];
|
|
130
142
|
|
|
131
143
|
/** Scan a project directory and detect which stacks are present */
|
package/src/types.ts
CHANGED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ct-capacitor-ota
|
|
3
|
+
description: Capacitor 8 native runtime and Capgo OTA live updates — channels, encryption, staged rollouts, and store-safe over-the-air delivery
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Capacitor OTA (Capgo) Patterns
|
|
7
|
+
|
|
8
|
+
Ship JavaScript, HTML, and CSS changes to installed apps in minutes instead of waiting days for app store review. Capgo's `@capgo/capacitor-updater` swaps the web bundle the native shell loads; native code still ships through the stores.
|
|
9
|
+
|
|
10
|
+
**Golden rule:** OTA updates only the **web layer**. Anything that changes native code (new plugin, native dependency, Capacitor major bump) requires a new store binary. Never OTA a bundle whose native contract differs from the installed shell.
|
|
11
|
+
|
|
12
|
+
## Versions (as of June 2026)
|
|
13
|
+
|
|
14
|
+
| Package | Version | Notes |
|
|
15
|
+
| -------------------------- | -------- | ----------------------------------------------------------- |
|
|
16
|
+
| `@capacitor/core` & CLI | ^8.3.x | 8.3.1 (2026-04-16); Node 22+, SPM default for new iOS apps |
|
|
17
|
+
| `@capgo/capacitor-updater` | ^8.x | Major version tracks Capacitor major; v8 stores channels locally |
|
|
18
|
+
| `@capgo/cli` | latest | `npx @capgo/cli@latest …` — pin in CI |
|
|
19
|
+
|
|
20
|
+
Capacitor 8 platform floor: Node 22, iOS deployment target 15.0 (Xcode 26+), Android `minSdk 24` / `compileSdk 36` / `targetSdk 36`, Gradle plugin 8.13.0, wrapper 8.14.3, Kotlin 2.2.20.
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm i @capgo/capacitor-updater && npx cap sync
|
|
26
|
+
npx @capgo/cli@latest init <API_KEY> # adds app, injects notifyAppReady, builds, uploads, tests
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Critical: notifyAppReady()
|
|
30
|
+
|
|
31
|
+
Call it **as early as possible** after the web bundle boots. If the plugin doesn't hear it within `appReadyTimeout` (default 10s), it assumes the bundle is broken and **auto-rolls back** to the previous one.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { CapacitorUpdater } from "@capgo/capacitor-updater";
|
|
35
|
+
|
|
36
|
+
// Run once, at the top of app bootstrap — before heavy async work.
|
|
37
|
+
CapacitorUpdater.notifyAppReady();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This is the safety net that makes OTA reversible. A bundle that white-screens never sticks.
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// capacitor.config.ts
|
|
46
|
+
import type { CapacitorConfig } from "@capacitor/cli";
|
|
47
|
+
|
|
48
|
+
const config: CapacitorConfig = {
|
|
49
|
+
appId: "com.example.app",
|
|
50
|
+
appName: "Example",
|
|
51
|
+
webDir: "dist",
|
|
52
|
+
plugins: {
|
|
53
|
+
CapacitorUpdater: {
|
|
54
|
+
autoUpdate: "atBackground", // default — apply when app backgrounds
|
|
55
|
+
appReadyTimeout: 10000,
|
|
56
|
+
responseTimeout: 20,
|
|
57
|
+
autoDeleteFailed: true,
|
|
58
|
+
autoDeletePrevious: true,
|
|
59
|
+
resetWhenUpdate: true, // wipe OTA bundles on native app update
|
|
60
|
+
// defaultChannel: "Development", // TEST/QA BUILDS ONLY — omit in production
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default config;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### autoUpdate modes
|
|
69
|
+
|
|
70
|
+
| Value | Behavior |
|
|
71
|
+
| --------------------------- | -------------------------------------------------------------------- |
|
|
72
|
+
| `false` / `"off"` | No automatic updates — drive everything from JS |
|
|
73
|
+
| `"atBackground"` *(default)* | Download silently, apply when app moves to background |
|
|
74
|
+
| `"atInstall"` | Apply immediately after a fresh install / native update, then background |
|
|
75
|
+
| `"onLaunch"` | Apply immediately on next launch, then background |
|
|
76
|
+
| `"always"` | Apply immediately whenever an update check runs |
|
|
77
|
+
| `"onlyDownload"` | Download but never auto-apply; emit `updateAvailable` for you to act |
|
|
78
|
+
|
|
79
|
+
`periodCheckDelay` controls how often the plugin polls (seconds, **minimum 600** / 10 min).
|
|
80
|
+
|
|
81
|
+
## Channels
|
|
82
|
+
|
|
83
|
+
A channel points to one JS bundle. Devices check their assigned channel; you swap which bundle a channel points to for instant rollout/rollback — no rebuild.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx @capgo/cli@latest bundle upload --channel=production # build → upload → assign
|
|
87
|
+
npx @capgo/cli@latest channel set production -s default # make it the cloud default
|
|
88
|
+
npx @capgo/cli@latest channel set beta --self-assign # allow in-app setChannel()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Channel precedence** (highest first): forced device mapping → cloud per-device override → config `defaultChannel` → cloud Default Channel (the ~99% path).
|
|
92
|
+
|
|
93
|
+
**Production rule:** ship production binaries **without** `defaultChannel` so users follow the cloud default. Only set `defaultChannel` in builds explicitly handed to testers.
|
|
94
|
+
|
|
95
|
+
Recommended ladder with semver pre-release tags:
|
|
96
|
+
|
|
97
|
+
| Channel | Bundle version | Audience |
|
|
98
|
+
| ------------- | --------------- | ------------------------- |
|
|
99
|
+
| `development` | `1.2.3-dev.1` | dev / emulator builds |
|
|
100
|
+
| `qa` | `1.2.3-qa.1` | QA verification |
|
|
101
|
+
| `staging` | `1.2.3-rc.1` | production-like sign-off |
|
|
102
|
+
| `production` | `1.2.3` | end users (store version) |
|
|
103
|
+
|
|
104
|
+
### Runtime channel switching
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// Requires "Allow device self-assignment" enabled on the channel (plugin v8+).
|
|
108
|
+
await CapacitorUpdater.setChannel({ channel: "beta", triggerAutoUpdate: true });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
In v8 `setChannel` stores the choice **locally** and takes effect on the next check (no replication lag). The backend still validates self-assignment permission.
|
|
112
|
+
|
|
113
|
+
## Manual / background update flow
|
|
114
|
+
|
|
115
|
+
For full control, set `autoUpdate: false` and drive the lifecycle yourself:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { App } from "@capacitor/app";
|
|
119
|
+
import { SplashScreen } from "@capacitor/splash-screen";
|
|
120
|
+
|
|
121
|
+
CapacitorUpdater.addListener("download", ({ percent }) => console.log(`↓ ${percent}%`));
|
|
122
|
+
|
|
123
|
+
App.addListener("appStateChange", async ({ isActive }) => {
|
|
124
|
+
if (isActive) {
|
|
125
|
+
const latest = await CapacitorUpdater.getLatest();
|
|
126
|
+
if (latest.url) {
|
|
127
|
+
const bundle = await CapacitorUpdater.download({ version: latest.version, url: latest.url });
|
|
128
|
+
await CapacitorUpdater.next({ id: bundle.id }); // apply on next background
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Key methods: `getLatest()`, `download()`, `next()` (queue), `set()` (apply now — destroys JS context), `current()`, `reload()`, `reset()`. Events: `download`, `downloadComplete`, `updateAvailable`, `downloadFailed`, `appReady`, `majorAvailable`.
|
|
135
|
+
|
|
136
|
+
## End-to-end encryption (v2)
|
|
137
|
+
|
|
138
|
+
Hybrid RSA-2048 + AES-256: a random AES key encrypts each bundle; your **private** RSA key signs the AES key + checksum; the app decrypts with the embedded **public** key. Only your users can read the bundle — not even Capgo.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npx @capgo/cli@latest key create # generates the key pair
|
|
142
|
+
npx @capgo/cli@latest bundle upload --key-v2 --channel=production
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- `.capgo_key_v2` (private) — **never commit**; store as a CI secret.
|
|
146
|
+
- `.capgo_key_v2.pub` (public) — safe to commit; injected into the app.
|
|
147
|
+
- Once encryption is configured, the plugin **strictly enforces** a valid signature — an unsigned/tampered bundle is rejected.
|
|
148
|
+
|
|
149
|
+
## Delta updates
|
|
150
|
+
|
|
151
|
+
Direct/instant updates auto-enable delta updates — only changed files transfer, not the whole bundle. Keep deltas small: pin build tooling and strip build timestamps so unchanged files hash identically. Use `--delta-only` to require delta-capable clients.
|
|
152
|
+
|
|
153
|
+
## Staged rollouts & monitoring
|
|
154
|
+
|
|
155
|
+
- Start at ~10% of users, widen over 24h; schedule heavy rollouts for 1–4 AM local time, Wi-Fi-preferred.
|
|
156
|
+
- Healthy targets: ~95% adoption within 24h, ~82% global success rate.
|
|
157
|
+
- Roll back instantly from the channel dashboard (crown icon on the previous build) — devices pick it up on next check-in.
|
|
158
|
+
- Watch the `downloadFailed` / `appReady` ratio and Capgo stats; a spike means rollback.
|
|
159
|
+
|
|
160
|
+
## Store compliance
|
|
161
|
+
|
|
162
|
+
OTA of JS/HTML/CSS is allowed: **Apple** developer agreement §3.3.2 (since iOS 4.3) and **Google Play** (code in a webview/JS VM is exempt from the non-Play update restriction). Shipping *native* code OTA is not — that's what gets apps pulled.
|
|
163
|
+
|
|
164
|
+
## Anti-Patterns
|
|
165
|
+
|
|
166
|
+
1. **Not calling `notifyAppReady()`** — every bundle auto-rolls back after `appReadyTimeout`. This is the #1 "OTA doesn't work" cause.
|
|
167
|
+
2. **OTA-ing a native change** — bundle that needs a plugin/native API the installed shell lacks crashes. Match the bundle to the shipped native version; bump the store binary first.
|
|
168
|
+
3. **`defaultChannel` in production builds** — pins users off the cloud default and breaks instant rollback. Test builds only.
|
|
169
|
+
4. **Committing `.capgo_key_v2`** — leaks your signing key. Commit only the `.pub`.
|
|
170
|
+
5. **`periodCheckDelay` < 600** — silently clamped; the plugin won't poll faster than 10 minutes.
|
|
171
|
+
6. **100% rollout with no canary** — a bad bundle hits everyone at once. Stage it.
|
|
172
|
+
7. **Unpinned `@capgo/cli@latest` in CI** — a CLI minor can change upload behavior mid-pipeline. Pin the version.
|
|
173
|
+
|
|
174
|
+
## See Also
|
|
175
|
+
|
|
176
|
+
- `ct-vite-vitest-patterns` — the build that produces `webDir`
|
|
177
|
+
- `ct-playwright-patterns` — E2E-verify a bundle before you upload it
|
|
178
|
+
- `ct-typescript-conventions` — typing `capacitor.config.ts` and the updater API
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ct-capacitor-ota",
|
|
3
|
+
"description": "Capacitor 8 native runtime and Capgo OTA live updates: channels, encryption, staged rollouts",
|
|
4
|
+
"defaultMappings": {
|
|
5
|
+
"capacitor.config.ts": "ct-capacitor-ota",
|
|
6
|
+
"capacitor.config.json": "ct-capacitor-ota",
|
|
7
|
+
"ios": "ct-capacitor-ota",
|
|
8
|
+
"android": "ct-capacitor-ota"
|
|
9
|
+
},
|
|
10
|
+
"fileExtensions": [],
|
|
11
|
+
"skillRules": {
|
|
12
|
+
"ct-capacitor-ota": {
|
|
13
|
+
"description": "Capacitor 8 live updates with Capgo: notifyAppReady, channels, encryption, CLI, staged rollouts",
|
|
14
|
+
"priority": 7,
|
|
15
|
+
"triggers": {
|
|
16
|
+
"keywords": [
|
|
17
|
+
"capacitor",
|
|
18
|
+
"capgo",
|
|
19
|
+
"ota",
|
|
20
|
+
"live update",
|
|
21
|
+
"updater",
|
|
22
|
+
"notifyAppReady",
|
|
23
|
+
"channel",
|
|
24
|
+
"bundle",
|
|
25
|
+
"hot update",
|
|
26
|
+
"app store",
|
|
27
|
+
"setChannel"
|
|
28
|
+
],
|
|
29
|
+
"keywordPatterns": [
|
|
30
|
+
"\\bcapacitor\\b",
|
|
31
|
+
"\\bcapgo\\b",
|
|
32
|
+
"\\bota\\b",
|
|
33
|
+
"\\blive\\s+updates?\\b",
|
|
34
|
+
"\\bnotifyAppReady\\b"
|
|
35
|
+
],
|
|
36
|
+
"pathPatterns": [
|
|
37
|
+
"**/capacitor.config.ts",
|
|
38
|
+
"**/capacitor.config.json",
|
|
39
|
+
"**/capacitor.config.js",
|
|
40
|
+
"**/android/**",
|
|
41
|
+
"**/ios/**"
|
|
42
|
+
],
|
|
43
|
+
"intentPatterns": [
|
|
44
|
+
"(?:ship|push|deploy|roll\\s*out).*(?:update|bundle|version)",
|
|
45
|
+
"(?:setup|configure|add).*(?:capgo|capacitor|live\\s+update|ota)",
|
|
46
|
+
"(?:rollback|revert).*(?:bundle|channel|update)"
|
|
47
|
+
],
|
|
48
|
+
"contentPatterns": [
|
|
49
|
+
"@capgo/capacitor-updater",
|
|
50
|
+
"CapacitorUpdater",
|
|
51
|
+
"notifyAppReady",
|
|
52
|
+
"setChannel",
|
|
53
|
+
"@capacitor/core",
|
|
54
|
+
"autoUpdate"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"relatedSkills": [
|
|
58
|
+
"ct-vite-vitest-patterns",
|
|
59
|
+
"ct-playwright-patterns",
|
|
60
|
+
"ct-typescript-conventions"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|