How to migrate from Capgo or Capawesome to OtaKit
A production-grade migration guide: exact config and API translations from Capgo and Capawesome to OtaKit, what maps cleanly, and a safe cutover plan for your install base.
This guide assumes your Capacitor app already runs Capgo or Capawesome Live Update in production, and walks through the move to OtaKit: what maps one-to-one, what maps to a different (usually smaller) concept, and how to cut over without breaking the install base you already have in the field.
The one constraint you can't skip
An OTA migration is a plugin migration. Devices running your current store build have the Capgo or Capawesome plugin compiled into the binary, and they will keep checking that vendor's endpoints until their user installs a store build that contains OtaKit instead. There is no server-side switch that reroutes an existing binary.
Plan for a dual-system window: old binaries keep receiving updates from the old vendor, new binaries receive them from OtaKit. It typically lasts as long as your store-update adoption curve — weeks, not days.
Step 0: inventory your routing model
Before touching code, write down how your current setup decides which device gets which bundle: every channel, every runtime override, every min/max/semver native-version rule, every percentage rollout. Migrations go wrong when this list lives in three heads and a dashboard.
Nearly everything on that list collapses into four OtaKit translations:
- Audience →
channel(withsetChannel()for user-facing opt-ins). - Native compatibility →
runtimeVersion. - Release pacing → channel promotion.
- Per-user behavior → feature flags in your app, outside the OTA transport.
Migrating from Capgo
Config translation
A typical Capgo production config:
// capacitor.config.ts
plugins: {
CapacitorUpdater: {
appId: "com.example.app",
autoUpdate: true,
defaultChannel: "production",
directUpdate: false,
periodCheckDelay: 600, // seconds
appReadyTimeout: 10000,
publicKey: "YOUR_PUBLIC_KEY"
}
}The OtaKit equivalent:
// capacitor.config.ts
plugins: {
OtaKit: {
appId: "app_xxxxxxxx",
channel: "production",
// defaults shown explicitly:
launchPolicy: "apply-staged", // activate staged bundles on cold start
resumePolicy: "shadow", // download in background on resume
checkInterval: 600000, // milliseconds, not seconds
appReadyTimeout: 10000
}
}defaultChannel→channel, directly.periodCheckDelayis seconds; OtaKit'scheckIntervalis milliseconds. This is the classic migration typo — 600 becomes 600000.directUpdatetranslates by activation timing:false(apply on next launch) is OtaKit's defaultlaunchPolicy: "apply-staged";"always"maps tolaunchPolicy: "immediate";"atInstall"maps toruntimePolicy: "immediate"— which is also OtaKit's default, so fresh installs catch up fast out of the box.- Capgo's
publicKeyencryption maps to OtaKit's end-to-end encryption: runotakit generate-encryption-key, ship the key inbundleKeys, upload with--encrypt. See Security for the rotation story.
Runtime API translation
| Capgo | OtaKit | Migration note |
|---|---|---|
| notifyAppReady() | notifyAppReady() | Identical name, identical job. Wire it into your real app-ready path. |
| getLatest() | check() | Ask whether a newer bundle exists for the current lane, without downloading. |
| download() | download() | Same manual staging step; OtaKit verifies the hash before staging. |
| next() / set() + reload() | apply() or update() | apply() activates the staged bundle; update() is the one-call "get me current" helper. |
| setChannel() / getChannel() | setChannel() / getChannel() | Direct mapping. OtaKit persists the override across launches; pass null to clear it. |
| unsetChannel() | setChannel({ channel: null }) | Clearing the override returns the device to the configured channel. |
| listChannels() | Dashboard / CLI | Channel discovery is an operator concern in OtaKit, not a device API. |
| setCustomId() | App-level feature flags | Keep per-user targeting in your product logic; OTA decides versions, not cohorts. |
| reset() | Automatic rollback + release rollback | Recovery is health-driven on device and one click per channel in the dashboard. |
Method names may drift between plugin major versions — treat the table as the conceptual map and your IDE as the source of truth.
Version targeting
Where Capgo routes by native version metadata on the bundle, OtaKit inverts it: the app declares its lane. Each store build sets a runtimeVersion in config, and releases into that lane can only reach shells that declared it.
# Capgo npx @capgo/cli bundle upload --channel production --native-version "2.0.0" # OtaKit: the store build declares runtimeVersion: "2.0.0" in config, # and you release into that lane otakit upload --release production
If your Capgo setup uses semver ranges or version filters, name each native baseline explicitly and release per lane. It's more verbose to say and much easier to operate — the compatibility contract is visible in one config line instead of spread across upload flags.
Migrating from Capawesome
Config translation
A typical Capawesome Live Update production config:
// capacitor.config.ts
plugins: {
LiveUpdate: {
appId: "6e351b4f-69a7-415e-a057-4567df7ffe94",
defaultChannel: "production",
autoUpdateStrategy: "background",
readyTimeout: 10000,
publicKey: "YOUR_PUBLIC_KEY",
autoDeleteBundles: true,
autoBlockRolledBackBundles: true
}
}The OtaKit equivalent:
// capacitor.config.ts
plugins: {
OtaKit: {
appId: "app_xxxxxxxx",
channel: "production",
launchPolicy: "apply-staged",
resumePolicy: "shadow",
appReadyTimeout: 10000
}
}defaultChannel→channel;readyTimeout→appReadyTimeout.autoUpdateStrategy: "background"is OtaKit's default behavior:shadowon resume,apply-stagedon launch. Bundles download silently and activate on the next cold start.autoDeleteBundlesandautoBlockRolledBackBundleshave no OtaKit equivalents because the behaviors are built in: the plugin keeps exactly current + fallback + staged, and a rolled-back bundle is never re-activated.- Capawesome's public-key bundle signing maps to OtaKit's always-on signed manifests (ES256) plus SHA-256 verification; if you also need content secrecy, add
--encrypt. - If you used Capawesome's delta updates, upload with
--strategy deltas— same benefit, per-file content-addressed delivery.
Runtime API translation
| Capawesome | OtaKit | Migration note |
|---|---|---|
| ready() | notifyAppReady() | Same readiness handshake, different name. |
| sync() | update() — or check() + download() + apply() | update() covers the common case in one call; the split API gives full control. |
| fetchLatestBundle() | check() | Same job: query the lane for a newer version. |
| downloadBundle() | download() | Same manual staging step. |
| setNextBundle() | download() + apply() | OtaKit always moves to the newest compatible bundle instead of pinning by ID. |
| setChannel() / getChannel() | setChannel() / getChannel() | Direct mapping, persisted across launches. |
| addListener(...) | addListener(...) | Map to updateAvailable, updateStaged, updateApplied, downloadFailed, rollback. |
| reset() | Automatic rollback + release rollback | Health-confirmed activation replaces the manual reset button. |
Version ranges → runtime lanes
Capawesome's per-platform min/max/eq version codes become explicit lanes: each native baseline declares one runtimeVersion, and you release into the lanes you still support. Ranges disappear; what remains is a list you can read aloud.
# Capawesome npx @capawesome/cli apps:bundles:create \ --channel production --android-max 12 --ios-max 12 # OtaKit: one lane per supported native baseline otakit upload --release production # runtimeVersion "12" build otakit upload --release production # run again from the "11" branch if you still ship fixes there
Feature-by-feature map
| You use today | OtaKit equivalent | Notes |
|---|---|---|
| Channels / audiences | channel + setChannel() | Named channels with promotion; runtime overrides for opt-in flows like beta toggles. |
| Native version targeting | runtimeVersion | One explicit compatibility lane per native baseline instead of min/max/semver rules. |
| Delta updates | --strategy deltas | Per-file, content-addressed objects; devices download only changed files. |
| End-to-end encryption | --encrypt + bundleKeys | AES-256-GCM with your key; generate it with otakit generate-encryption-key. |
| Forced/critical updates | --force-immediate | Devices apply and reload on their next check — for emergency fixes. |
| Staged rollout percentages | Channels + promotion | Promote qa → beta → production explicitly instead of percentage math in the updater. |
| Update lifecycle events | addListener() | updateAvailable, updateStaged, updateApplied, downloadFailed, rollback. |
| Per-device targeting | Feature flags in your app | Deliberately not an OTA concern in OtaKit — ship compatible code, gate features in product logic. |
The one deliberate gap: OtaKit has no per-device targeting inside the OTA transport. Every production use we've seen for it — support builds, VIP cohorts, gradual exposure — is better served by feature flags in the app, where product logic already lives. OTA decides which version a lane runs; your app decides which features a user sees.
The cutover plan
- Freeze routing changes on the old system except for active incidents.
- Document every channel, override, rollout rule, and version rule (step 0 above).
- Decide your OtaKit lane model up front: which named channels, and whether
runtimeVersionstarts now (recommended) or at your next native break. - Swap the plugin: remove
@capgo/capacitor-updateror@capawesome/capacitor-live-update, thennpm install @otakit/capacitor-updater, updatecapacitor.config.ts, and sync native projects. - Wire
notifyAppReady()into your real app-ready path — after your app shell renders, not before. This is the rollback safety net; don't ship without it. - Verify the full cycle on a test channel: release, download, activation, a deliberate broken build to watch automatic rollback, and a
runtimeVersionmismatch to confirm lane isolation. - Ship the new store build containing OtaKit.
- Keep the old vendor account alive while old binaries age out — they still need it for any emergency fix you push during the window.
- Watch adoption in the OtaKit dashboard; when the old binaries are down to a tail you can live with, retire the old channels and the old subscription.
Day-to-day releases after the switch
npm run build otakit upload --release # base channel otakit upload --release staging # named channel otakit release <bundle-id> --channel production # promote the same bundle otakit upload --release --force-immediate # emergency fix otakit upload --release --strategy deltas # delta delivery
Questions mid-migration? The setup guide and update strategies cover the details, and we're happy to help with gnarly routing models at support@otakit.app — we've probably seen yours.