otakit
llms.txt48 starsChanged 7 days agoMITView on GitHub
Canonical documentation: https://otakit.app/docs Generated from the public docs pages in packages/site/app/docs. Route: /docs How OtaKit works and where to start. OtaKit ships over-the-air updates for Capacitor apps. You build your web app, upload its bundle, and the plugin delivers that bundle to devices without store submissions or reviews. Start with the quick setup Need help with setup, billing, or rollout issues? Email support@otakit.app . Route: /docs/setup Set up the default hosted OtaKit flow in a Capacitor project. This is the…
- Reads credentials
- Installs packages
- Sends data out
In this repositoryOne other file it gives its agents.
- otakitSkill
The file
Show its rules and text
We split this llms.txt into the rules it gives an agent
56 rules. About 0 are common, 29 are distinctive, and 27 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈29
Distinctive · estimate
This author's own choices. Worth a look.
27
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 20
Update Strategies: /docs/update-strategies
- Distinctive: line 37
Start with the quick setup
- Very specific: line 40
Create an app in the OtaKit dashboard and copy its appId into your capacitor.config.ts file.
- Very specific: line 41
Call notifyAppReady() when your app finishes loading, so newly activated updates can be confirmed healthy.
- Distinctive: line 56
MCP & Agent Skills: Ask your coding agent to ship an update.
- Very specific: line 57
Self-hosting: Run OtaKit on your own infrastructure when you need full control over delivery and trust.
- Distinctive: line 67
Plugin API: Default automatic flow, manual flow, and configuration.
- Very specific: line 94
Add the OtaKit plugin to capacitor.config.ts and paste in the appId from the dashboard.
- Very specific: line 120
Call notifyAppReady() once your app has loaded.
- Distinctive: line 165
Read Channels & Runtimes for more advanced release flows.
- Distinctive: line 166
Use the Plugin API and CLI reference for exact configuration and command details.
- Distinctive: line 174
Use the CLI to upload bundles, release them, inspect bundle and release history, and manage apps.
- Very specific: line 206
If your account has multiple organizations, login asks you to choose a default by name for commands that are not tied to an app.
- Distinctive: line 303
Create a private link and QR code that open a bundle in the installed app on one phone, without releasing it.
- Distinctive: line 320
Check the local native plugin set against a channel's current release without uploading.
- Very specific: line 392
Create a new app and print the plugin snippet to paste into capacitor.config.ts.
- Distinctive: line 450
Remove stored token for a server.
- Distinctive: line 546
Start the local MCP server, bound to one project and organization for its lifetime.
- Very specific: line 602
Import from @otakit/capacitor-updater.
- Very specific: line 657
If you ship a new store build and do not want it to keep consuming older OTA bundles, bump runtimeVersion in the plugin config before …
- Distinctive: line 696
If you want no automatic checks at all, turn every automatic policy off and drive the update lifecycle yourself.
- Very specific: line 721
After a successful apply() or an update() that installs a new bundle, the app reloads immediately.
- Distinctive: line 739
If you want a split flow, download first and apply later:
- Distinctive: line 770
Use these only when you run a custom server or need custom verification behavior.
- Very specific: line 863
All endpoints are under /api/v1.
- Distinctive: line 880
Create a new app. The slug should match your Capacitor app identifier.
- Distinctive: line 903
Start a bundle upload session.
- Distinctive: line 971
Delete a bundle. Bundles that are part of a release history cannot be deleted.
- Very specific: line 1132
Create a preview link that opens the bundle in the installed app, without releasing it.
- Very specific: line 1251
Create an app in the OtaKit dashboard and copy its appId.
- Distinctive: line 1275
Create a client component that confirms the app loaded successfully:
- Distinctive: line 1295
Add it to your root layout:
- Distinctive: line 1335
Read Channels & Runtimes for release tracks and native compatibility boundaries.
- Very specific: line 1416
Call notifyAppReady() once your app has rendered on a native device:
- Distinctive: line 1499
Use channels for rollout tracks and runtimeVersion for native compatibility boundaries.
- Very specific: line 1509
If you omit channel from the plugin config, the app uses the unnamed base channel.
- Distinctive: line 1527
Add a channel when you want a separate rollout track — for example, internal QA, beta, or a staged production rollout.
- Distinctive: line 1616
No device identifiers are collected.
- Very specific: line 1788
After an update, getUnseenReleaseNotes() returns the running release's notes until you mark them seen, so each release shows once per phone:
- Distinctive: line 2005
Make sure your bundle ID is registered under Identifiers with the Push Notifications capability, in the same team as the key.
- Very specific: line 2069
Every method resolves with a status instead of throwing, so push setup never breaks your app.
- Very specific: line 2126
Use GitHub Actions to build, upload, and optionally release OtaKit bundles.
- Very specific: line 2203
If you run your own OtaKit server, add this environment variable to the workflow as well:
- Very specific: line 2211
Use the base channel for the simplest production setup.
- Distinctive: line 2212
Use branch protection so OTA uploads only run from trusted branches.
- Distinctive: line 2215
Use the CLI reference for exact upload, login, and token commands.
- Very specific: line 2263
Run /mcp and you should see otakit connected to whichever project you have open.
- Very specific: line 2276
Create .vscode/mcp.json, then run MCP: List Servers to trust and start it:
- Distinctive: line 2329
Add it alongside the local server, not instead of it — running both means your agent sees two copies of the shared tools.
- Very specific: line 2360
Use your own console origin everywhere — otakit connect picks it up from plugins.OtaKit.serverUrl automatically.
- Distinctive: line 2383
Every manifest is signed with ES256 (ECDSA P-256) when a release is published.
- Distinctive: line 2389
Every bundle download is verified against the SHA-256 hash in the signed manifest.
- Very specific: line 2403
After a new bundle is activated, the app must call notifyAppReady() within a configurable timeout (default 10 seconds).
- Very specific: line 2435
If you discover a security issue, please email security@otakit.app .
- Very specific: line 2441
Run the full OtaKit platform on your own infrastructure, step by step.
- Very specific: line 2616
Put the private key in the console env (MANIFEST_SIGNING_KID, MANIFEST_SIGNING_KEY) and the public key in the plugin config's manifestKeys.
≈ “Common” is an estimate: at least 1 in 5 of the 3,592 llms.txt files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
# OtaKit Docs
Canonical documentation: https://otakit.app/docs
Generated from the public docs pages in `packages/site/app/docs`.
## Pages
- Overview: /docs
- Setup: /docs/setup
- CLI Reference: /docs/cli
- Plugin API: /docs/plugin
- REST API: /docs/api
- Next.js Guide: /docs/guide
- React Guide: /docs/react
- Channels: /docs/channels
- Rollouts: /docs/rollouts
- Previews: /docs/previews
- Release Notes: /docs/release-notes
- Update Strategies: /docs/update-strategies
- Events & Listeners: /docs/events
- Push Notifications: /docs/push
- CI Automation: /docs/ci
- MCP & Agent Skills: /docs/agents
- Security: /docs/security
- Self-hosting: /docs/self-host
- Changelog: /changelog
## Overview
Route: /docs
How OtaKit works and where to start.
OtaKit ships over-the-air updates for Capacitor apps. You build your web app, upload its bundle, and the plugin delivers that bundle to devices without store submissions or reviews.
Start with the quick setup
### How it works
1. Create an app in the OtaKit dashboard and copy its `appId` into your `capacitor.config.ts` file.
2. Call `notifyAppReady()` when your app finishes loading, so newly activated updates can be confirmed healthy.
3. Build your web app and run `otakit upload --release` to upload and publish a new bundle.
4. On device, the plugin checks for updates, verifies the downloaded bundle, and by default activates it on the next cold launch.
### Features
- **One-command shipping**: Build your web app, then release with otakit upload --release.
- **Channels & runtime lanes**: Channels for rollout tracks, runtimeVersion for native compatibility lanes — switchable at runtime with setChannel().
- **Delta updates**: Opt-in per-file updates: devices download only the files that changed between releases.
- **Automatic update delivery**: The normal flow checks and stages automatically, then activates according to the configured launch, resume, and runtime policies.
- **Force-immediate releases**: Emergency releases apply and reload on the device's next check, whatever its configured policies.
- **Update lifecycle events**: updateAvailable, updateStaged, updateApplied, downloadFailed, rollback — build your own update UX.
- **Manual update control**: Turn automatic policies off when your app wants to show its own update prompt or control install timing.
- **Safe activation & rollback**: A newly activated bundle must call notifyAppReady() or OtaKit rolls back automatically.
- **Signed & verified**: ES256-signed manifests and SHA-256 checks on every download, plus optional end-to-end encryption.
- **Native compatibility guardrail**: The CLI warns at upload time when a bundle depends on native code the installed app doesn't have.
- **MCP & Agent Skills**: Ask your coding agent to ship an update. It checks compatibility, uploads, and stops for your approval — the same release process, same audit trail.
- **Self-hosting**: Run OtaKit on your own infrastructure when you need full control over delivery and trust.
### Getting started
- **Quick setup**: Connect the default hosted OtaKit flow to your Capacitor app.
- **Next.js Guide**: Go from Next.js + Capacitor to your first OTA update.
- **Channels & Runtimes**: Rollout tracks vs runtime compatibility lanes, and when to use each.
- **Update Strategies**: How launchPolicy, resumePolicy, and runtimePolicy combine to control when updates apply.
- **MCP & Agent Skills**: One command connects Claude Code, Codex, or VS Code to your project.
- **CI Automation**: Build and ship bundles from GitHub Actions.
- **CLI Reference**: Commands, options, and release workflows.
- **Plugin API**: Default automatic flow, manual flow, and configuration.
Need help with setup, billing, or rollout issues? Email support@otakit.app .
## Setup
Route: /docs/setup
Set up the default hosted OtaKit flow in a Capacitor project.
This is the most simple, default hosted setup path.
### 1. Connect your app in the dashboard
Sign in to the OtaKit dashboard and choose “Connect your app”. Give it a label that is unique in your workspace, such as `my-app`; it does not need to match your native app ID.
OtaKit generates a separate app ID. Copy that generated value from the dashboard into `plugins.OtaKit.appId`, not the label you entered. The floating setup panel guides you through connecting your project.
### 2. Install the Capacitor plugin
```txt
npm install @otakit/capacitor-updater
npx cap sync
```
### 3. Configure the plugin
Add the OtaKit plugin to `capacitor.config.ts` and paste in the `appId` from the dashboard.
```txt
// capacitor.config.ts
import type { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
appId: "com.example.myapp",
appName: "My App",
webDir: "out",
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID",
}
}
};
export default config;
```
Build and install the native app with the OtaKit plugin configured before testing OTA. A local test build is enough to get started. To reach existing users, publish an App Store or Google Play build containing the plugin; those users can receive OtaKit updates after installing that build.
Optional: if you want something other than the hosted defaults, you can also set `launchPolicy`, `resumePolicy`, `runtimePolicy`, and `checkInterval`. See the Plugin API page for the full configuration surface.
### 4. Add notifyAppReady()
Call `notifyAppReady()` once your app has loaded. If the new bundle is activated and your app never confirms that it started successfully, OtaKit rolls back automatically.
```txt
import { OtaKit } from "@otakit/capacitor-updater";
await OtaKit.notifyAppReady();
```
For React-style apps, wrap it in a client-side effect:
```txt
"use client";
import { useEffect } from "react";
import { Capacitor } from "@capacitor/core";
import { OtaKit } from "@otakit/capacitor-updater";
export function AppReadyProvider() {
useEffect(() => {
if (Capacitor.isNativePlatform()) {
OtaKit.notifyAppReady();
}
}, []);
return null;
}
```
### 5. Install the CLI and sign in
```txt
npm install -g @otakit/cli
otakit login
```
### 6. Build and release
```txt
npm run build
otakit upload --release
```
That publishes the bundle to the base channel. By default, OtaKit downloads it in the background and activates it on the next cold app launch.
### Next
- Continue with the Next.js guide or React guide
- Read Channels & Runtimes for more advanced release flows.
- Use the Plugin API and CLI reference for exact configuration and command details.
## CLI Reference
Route: /docs/cli
Upload bundles, release them, inspect history, and manage apps with the OtaKit CLI.
Use the CLI to upload bundles, release them, inspect bundle and release history, and manage apps.
### Project config
Project commands read from `capacitor.config.*`.
```txt
// capacitor.config.ts
import type { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
appId: "com.example.myapp",
appName: "My App",
webDir: "out",
plugins: {
OtaKit: {
appId: "app_xxxxxxxx",
// Optional named channel:
// channel: "staging"
// Optional compatibility lane:
// runtimeVersion: "2026.04"
}
}
};
export default config;
```
### Authentication
For local development, sign in once and the CLI stores a token locally. For CI or non-interactive environments, use an organization secret key instead.
If your account has multiple organizations, login asks you to choose a default by name for commands that are not tied to an app. Change it with `otakit organization select` . Configured apps still use their owning organization, and organization keys are already bound to one.
```txt
# Local development
otakit login
# CI / non-interactive
export OTAKIT_TOKEN=otakit_sk_...
export OTAKIT_APP_ID=app_xxxxxxxx
```
### Release flow
- Upload only: `otakit upload`
- Upload and release to the base channel: `otakit upload --release`
- Upload and release to a named channel: `otakit upload --release beta`
- Promote an existing bundle later: `otakit release <bundleId> --channel production`
### Resolution order
The CLI resolves values in a deterministic order.
- App ID: `--app-id` -> `OTAKIT_APP_ID` -> `capacitor.config.*`
- Server URL: `--server` -> `OTAKIT_SERVER_URL` -> `plugins.OtaKit.serverUrl` -> hosted default
- Auth token: `OTAKIT_TOKEN` -> stored login token
- App-less organization: `OTAKIT_ORGANIZATION_ID` -> stored login default
- Upload path: CLI path argument -> `OTAKIT_BUILD_DIR` -> `capacitor.config.* webDir`
- Release channel: `--release` -> base channel, `--release <channel>` -> named channel
- Runtime version: `plugins.OtaKit.runtimeVersion` -> bundle metadata during upload
- Upload version: `--version` -> `OTAKIT_VERSION` -> auto-generated version
### Command reference
### otakit upload [path]
Upload a bundle. Optionally release it immediately. Before uploading, the CLI warns about content that should not ship (.env files, .git, node_modules, native installers) and notes source maps and hidden files. After uploading, it prints what changed against the bundle the targeted channel runs, such as “+3 ~12 -1 · 4.1 MB → 4.3 MB · devices on 1.4.1 download up to 38.0 KB”.
Options
- `[path]`: Bundle directory. If omitted, the CLI uses OTAKIT_BUILD_DIR or capacitor.config.* webDir.
- `--app-id <id>`: App ID override.
- `--server <url>`: Server URL override.
- `--version <version>`: Version string. Otherwise OTAKIT_VERSION, then auto-generated.
- `--strict-version`: Require explicit or env-provided version.
- `--strict-artifacts`: Fail before uploading when the bundle is very large or contains native installers, .env files, .git or node_modules. Source maps and hidden files are only noted.
- `--release [channel]`: Release after upload. Omit channel to release to the base channel.
- `--strategy <strategy>`: Upload strategy: "zip" (single archive, default) or "deltas" (per-file objects; devices download only what changed).
- `--force-immediate`: With --release: devices apply and reload this release on their next check (emergency fixes).
- `--rollout <percent>`: With --release: release to this share of devices first (1-100, default 100). See Percentage rollouts.
- `--replace-rollout`: With --release: cancel the channel's active rollout and release this bundle in its place.
- `--notes <text>`: With --release: release notes app users may see (plain text, up to 2,000 characters). See Release notes.
- `--notes-file <path>`: With --release: read the release notes from a file.
- `--preview`: Also create a preview link and QR code for the uploaded bundle. See Preview links.
- `--encrypt`: Encrypt the bundle with OTAKIT_ENCRYPTION_KEY before upload (auto-enabled when the env var is set).
- `--fail-on-incompatible`: Exit non-zero when the native compatibility check finds changes that need a store build.
- `--ignore-compat`: Skip the native compatibility check.
Example
```txt
otakit upload --release
```
### otakit release [bundleId]
Release a bundle to the base channel or a named channel. The bundle already carries its runtimeVersion, so release only chooses the rollout channel.
Options
- `--channel <channel>`: Target named channel. Omit it to use the base channel.
- `--force-immediate`: Devices apply and reload this release on their next check (emergency fixes).
- `--rollout <percent>`: Release to this share of devices first (1-100, default 100). The channel needs a previous release.
- `--replace-rollout`: Cancel the channel's active rollout and release this bundle in its place.
- `--notes <text>`: Release notes app users may see (plain text, up to 2,000 characters). Edit them later in the dashboard.
- `--notes-file <path>`: Read the release notes from a file.
Example
```txt
otakit release --channel production --rollout 10
```
### otakit rollout [releaseId]
Show active rollouts, or raise, lower, complete, or cancel one. Without a release ID it acts on the rollout of the selected channel.
Options
- `--channel <channel>`: Channel of the rollout.
- `--base`: The rollout on the base channel.
- `--percent <percent>`: Set the share of devices (1-100; 100 completes the rollout).
- `--complete`: Release to every device.
- `--cancel`: Revert the rolling release; every device returns to the previous release.
Example
```txt
otakit rollout --channel production --percent 50
```
### otakit preview [bundleId]
Create a private link and QR code that open a bundle in the installed app on one phone, without releasing it. Defaults to the latest upload. The app needs previewLinks: true and a custom URL scheme.
Options
- `--expires <duration>`: How long the link works: 1h, 24h, 7d (default) or 30d.
- `--scheme <scheme>`: The app's custom URL scheme, such as myapp. Needed once; the app remembers it.
- `--list`: List active preview links.
- `--revoke <previewId>`: Revoke a preview link.
- `--json`: Print JSON output.
Example
```txt
otakit preview --scheme myapp
```
### otakit compatibility
Check the local native plugin set against a channel's current release without uploading.
Options
- `--channel <channel>`: Channel to compare against. Omit for the base channel.
- `--package-json <path>`: package.json used for native dependency detection.
- `--node-modules <path>`: node_modules used for native dependency detection.
Example
```txt
otakit compatibility --channel production
```
### otakit list
List uploaded bundles.
Options
- `--limit <n>`: Max results. Defaults to 20.
Example
```txt
otakit list --limit 20
```
### otakit diff <bundleId>
Compare a bundle with another: files added, changed and removed, the unpacked size before and after, what devices on the base download (the whole archive for zip updates; at most the content they do not have yet for delta updates), and warnings about content that should not ship. The warnings help catch build mistakes; they are not a security scan. Encrypted bundles cannot be inspected.
Options
- `--against <bundleId>`: Compare with this bundle. Without --against, --channel or --base, the CLI compares with the previous upload of the same runtime version.
- `--channel <channel>`: Compare with the bundle this channel runs.
- `--base`: Compare with the bundle the base channel runs.
- `--json`: Print the comparison as JSON, with up to 5,000 changed files (unlistedChanges counts the rest).
Example
```txt
otakit diff 2ffbb928-96b1-4297-a5b7-c1633c104868 --channel production
```
### otakit releases
Show release history across all streams or a specific target.
Options
- `--channel <channel>`: Show only a named channel.
- `--base`: Show only the base channel.
- `--limit <n>`: Max results. Defaults to 10.
Example
```txt
otakit releases --base
```
### otakit delete <bundleId>
Delete a bundle.
Options
- `--force`: Skip confirmation prompt.
Example
```txt
otakit delete abc123 --force
```
### otakit register
Create a new app and print the plugin snippet to paste into capacitor.config.ts.
Options
- `--slug <slug>`: App slug (for example com.example.app).
- `--server <url>`: Server URL override.
- `--token <token>`: Access token or organization API key.
- `--secret-key <key>`: Alias for --token.
Example
```txt
otakit register --slug com.example.myapp
```
### otakit login
Sign in with email OTP and store a token locally.
Options
- `--email <email>`: Email address. If omitted, prompts interactively.
- `--server <url>`: Server URL override.
- `--token-only`: Print token to stdout only.
Example
```txt
otakit login --email you@example.com
```
### otakit whoami
Show current authenticated user and organization context.
Options
- `--server <url>`: Server URL override.
- `--json`: Print machine-readable account and organization details.
Example
```txt
otakit whoami
```
### otakit organization select
Choose the default organization for commands not tied to an app. Configured apps always use their owning organization.
Options
- `--server <url>`: Server URL override.
Example
```txt
otakit organization select
```
### otakit logout
Remove stored token for a server.
Options
- `--server <url>`: Server URL override.
Example
```txt
otakit logout
```
### otakit config resolve
Show effective CLI values and where they came from.
Options
- `--app-id <id>`: App ID override.
- `--server <url>`: Server URL override.
- `--output-dir <path>`: Output directory override.
- `--channel <channel>`: Channel override.
- `--json`: Print machine-readable JSON output.
Example
```txt
otakit config resolve --json
```
### otakit connect
Connect this project to your coding agent. Signs in if needed, then writes the client's MCP configuration after showing exactly what it resolved and what it will write.
Options
- `--client <client>`: claude, codex, or vscode. Defaults to detected.
- `--project-root <path>`: Project to connect. Defaults to the current directory.
- `--server <url>`: OtaKit console URL override.
- `--dry-run`: Show the plan and exit without writing.
- `--yes`: Skip the confirmation prompt.
Example
```txt
npx -y @otakit/cli@latest connect
```
### otakit push send
Send a push notification to your app users. Needs the Push notifications add-on (Settings → Add-ons) and an APNs key and/or Firebase service account for the app. Shows the audience size and asks before sending.
Options
- `--title <title>`: Notification title (required).
- `--body <body>`: Notification text (required).
- `--url <url>`: Path or https link the app opens, sent as data.url.
- `--data <key=value>`: Extra data for the app (repeatable).
- `--platform <platform>`: ios or android (repeatable; default both).
- `--channel <channel>`: Only devices on this OTA channel (repeatable).
- `--topic <topic>`: Only devices subscribed to this topic (repeatable).
- `--user <id>`: Only devices of this user ID (repeatable).
- `--yes`: Send without the confirmation prompt (required in CI).
- `--json`: Print the campaign as JSON.
Example
```txt
otakit push send --title "New drop" --body "Open the app to see it" --url /shop --topic news
```
### otakit push campaigns
List recent push campaigns with delivery counts.
Options
- `--limit <n>`: Number of campaigns (default 10, max 100).
- `--json`: Print machine-readable JSON output.
Example
```txt
otakit push campaigns
```
### otakit push campaign <campaignId>
Show one campaign: status, devices targeted and accepted by Apple/Google, failures and removed tokens.
Options
- `--json`: Print machine-readable JSON output.
Example
```txt
otakit push campaign 11dead02-8f6f-4349-8ea3-7667c2a4382c
```
### otakit mcp
Start the local MCP server, bound to one project and organization for its lifetime.
Options
- `--project-root <path>`: Project root available to local MCP tools.
- `--organization-id <id>`: Advanced organization override for app-less automation.
Example
```txt
npx -y @otakit/cli@latest mcp --project-root .
```
### otakit config validate
Validate the OtaKit-related values in capacitor.config.*.
Options
- `--json`: Print machine-readable JSON output.
Example
```txt
otakit config validate
```
### otakit generate-signing-key
Generate an ES256 key pair for manifest signing.
Example
```txt
otakit generate-signing-key
```
### otakit generate-encryption-key
Generate an AES-256 bundle encryption key. Keep it in CI as OTAKIT_ENCRYPTION_KEY and ship it in the app's bundleKeys config.
Example
```txt
otakit generate-encryption-key
```
### Troubleshooting
- Missing app ID: add `plugins.OtaKit.appId` to `capacitor.config.ts`, or pass `--app-id`.
- Missing `index.html`: build your web app and verify `webDir` or the explicit upload path.
- Need to create an app from automation: use `otakit register --slug <slug>`.
## Plugin API
Route: /docs/plugin
Capacitor plugin setup, policy-based automatic updates, and manual advanced flows.
Import from `@otakit/capacitor-updater`. The default flow usually only needs `notifyAppReady()`. The other public methods exist for advanced manual update flows where your app decides when to check, download, and apply an update.
```txt
import { OtaKit } from "@otakit/capacitor-updater";
```
### Configuration
Hosted OtaKit keeps the config small. The default automatic behavior is runtime catch-up on cold start, staged activation on later cold starts, and background checks on resume.
```txt
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID",
// Optional:
// channel: "production",
// runtimeVersion: "2026.04",
// launchPolicy: "apply-staged",
// resumePolicy: "shadow",
// runtimePolicy: "immediate",
// checkInterval: 600000,
}
}
```
- `appId` (string): OtaKit app ID for manifest fetches and event ingest.
- `channel` (string): Named release track to check. Omit it to use the base channel.
- `runtimeVersion` (string): Optional native compatibility lane. Set it when a new store build must stop receiving older OTA bundles.
- `launchPolicy` ("off" | "shadow" | "apply-staged" | "immediate"): Cold-start policy after the current runtime lane is already resolved. Default: apply-staged.
- `resumePolicy` ("off" | "shadow" | "apply-staged" | "immediate"): Foreground resume policy. Default: shadow.
- `runtimePolicy` ("off" | "shadow" | "apply-staged" | "immediate"): Cold-start policy used when runtimeVersion changes or resolves for the first time — in practice, the app's first native install, or a native update where you bumped runtimeVersion in the plugin config. Default: immediate.
- `checkInterval` (number): Milliseconds between automatic background resume checks. Applies to `resumePolicy: "shadow"` and `resumePolicy: "apply-staged"` when no staged bundle is already waiting. Default: 600000. Set to 0 or a negative value to disable resume throttling.
- `appReadyTimeout` (number): Milliseconds to wait for notifyAppReady(). Default: 10000.
- `cdnUrl` (string): Optional CDN base URL for static manifest and bundle delivery. Leave unset for the hosted default.
- `ingestUrl` (string): Optional event ingest base URL. Leave unset for the hosted default.
- `serverUrl` (string): Optional control-plane API base URL used by self-host tooling such as the CLI. The native runtime uses cdnUrl and ingestUrl instead.
- `manifestKeys` (array): Optional public verification keys for custom or self-hosted manifest signing.
- `previewLinks` (boolean): Open preview links (myapp://otakit-preview?…) in this build (plugin 3.2+). Default: false. See Preview Links.
- `allowInsecureUrls` (boolean): Allow HTTP only for localhost development. Default: false.
Hosted OtaKit points at the managed CDN and ingest service automatically. Do not set `cdnUrl`, `ingestUrl`, or `manifestKeys` unless you intentionally want custom hosting or verification behavior.
### Policies
Policies are the only automatic behaviors. The same policy logic runs no matter which event triggered it. `launchPolicy`, `resumePolicy`, and `runtimePolicy` only decide when to invoke that policy.
- `off` (policy): Do nothing automatically.
- `shadow` (policy): Check for the latest update and stage it locally, but never apply it in that flow.
- `apply-staged` (policy): Apply an already staged bundle if one exists. If nothing is staged, fall back to shadow.
- `immediate` (policy): Check, stage, and immediately apply the newest update when one is available.
The hosted defaults are usually the right production baseline: `runtimePolicy: "immediate"`, `launchPolicy: "apply-staged"`, and `resumePolicy: "shadow"`.
### Compatibility lanes
`channel` is for rollout audience. `runtimeVersion` is for native compatibility.
If you ship a new store build and do not want it to keep consuming older OTA bundles, bump `runtimeVersion` in the plugin config before uploading the next OTA bundle.
The CLI reads that same value automatically during upload, so releases stay inside the correct runtime lane without extra flags.
When `runtimeVersion` changes, or when the app resolves a runtime lane for the first time, OtaKit runs `runtimePolicy` on cold start. In practice that means the app's first native install, or a native update where you bumped `runtimeVersion` in the plugin config. By default that policy is `immediate`, so fresh installs and new native shells catch up before that lane is marked resolved.
Want the full breakdown of how `launchPolicy`, `resumePolicy`, and `runtimePolicy` combine? See the Update Strategies guide.
### Automatic Flow (Default)
The hosted default is equivalent to this configuration:
```txt
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID",
runtimePolicy: "immediate",
launchPolicy: "apply-staged",
resumePolicy: "shadow",
appReadyTimeout: 10000,
}
}
```
In this mode, your app code usually only needs to call `notifyAppReady()`.
Resume throttling is intentionally narrow: `checkInterval` only affects resume background checks. Cold-start runtime handling, cold-start launch handling, and all manual APIs always act immediately.
Set `checkInterval` to `0` or a negative value if you want resume checks to run every time instead of being throttled.
- `notifyAppReady()` -> `void`: Confirm the current bundle is working. Call this once when your app has fully loaded. If it is not called within appReadyTimeout, the plugin rolls back.
```txt
import { OtaKit } from "@otakit/capacitor-updater";
await OtaKit.notifyAppReady();
```
### Manual Flow (Advanced)
If you want no automatic checks at all, turn every automatic policy off and drive the update lifecycle yourself.
```txt
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID",
launchPolicy: "off",
resumePolicy: "off",
runtimePolicy: "off",
}
}
```
- `getState()` -> `OtaKitState`: Inspect the current updater state: current bundle, fallback bundle, staged bundle, builtin version, the active percentage rollout seen by the last check (plugin 3.1+), and the active preview link (plugin 3.2+).
- `check()` -> `CheckResult`: Check the configured channel for a newer bundle without downloading it. Returns no_update, already_staged, or update_available.
- `download()` -> `DownloadResult`: Ensure the latest bundle is staged for later activation. Returns no_update or staged.
- `update()` -> `void`: Convenience helper for manual flows. It runs the same native immediate-flow operation used by automatic immediate policies. Terminal operation: if an update is applied, it does not resolve back into the old JS context.
- `apply()` -> `void`: Activate the currently staged bundle and reload the WebView. Terminal operation: on success it does not resolve back into the old JS context.
- `notifyAppReady()` -> `void`: Still required after the updated bundle launches. Call this once when your app has fully loaded so the plugin can mark the new bundle healthy.
- `getLastFailure()` -> `BundleInfo | null`: Returns information about the most recent failed update (rollback). Useful for diagnostics and crash reporting. Returns null if no failure has occurred.
- `stopPreview()` -> `void`: End the active preview link and return to the normal release (plugin 3.2+). Terminal operation while a preview bundle is running: the app reloads into the release.
- `getUnseenReleaseNotes()` -> `ReleaseNotes | null`: The running release's notes, until markReleaseNotesSeen() is called (plugin 3.3+). Show them once in a What's new screen, rendered as text. See Release Notes.
- `markReleaseNotesSeen()` -> `void`: Mark the running release's notes as shown, so getUnseenReleaseNotes() returns null until the next release with notes.
The plugin also emits lifecycle events — `updateAvailable`, `updateStaged`, `updateApplied`, `downloadFailed`, and `rollback` — via `OtaKit.addListener(...)`. See Events & Listeners for the full reference and the startup reconciliation pattern.
After a successful `apply()` or an `update()` that installs a new bundle, the app reloads immediately. Call `notifyAppReady()` from the reloaded app startup, not in the same JS flow that triggered the activation.
The simplest manual pattern is: check for updates, show your own prompt, then call `update()` if the user accepts.
```txt
const latest = await OtaKit.check();
if (latest.kind === "no_update") {
return;
}
const accepted = window.confirm("Update available. Install now?");
if (accepted) {
await OtaKit.update();
}
```
If you want a split flow, download first and apply later:
```txt
const state = await OtaKit.getState();
if (state.staged) {
await OtaKit.apply();
return;
}
const latest = await OtaKit.check();
if (latest.kind === "no_update") {
return;
}
const accepted = window.confirm("Update available. Download now?");
if (!accepted) {
return;
}
const result = await OtaKit.download();
if (result.kind === "no_update") {
return;
}
// Later, after another user action:
await OtaKit.apply();
```
### Advanced Overrides
Use these only when you run a custom server or need custom verification behavior.
```txt
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID",
// Optional advanced overrides
// cdnUrl: "https://cdn.your-domain.com",
// ingestUrl: "https://ingest.your-domain.com/v1",
// serverUrl: "https://your-domain.com/api/v1",
// allowInsecureUrls: false,
// manifestKeys: [
// { kid: "key-2026-01", key: "MFkwEwYH..." }
// ]
}
}
```
- `cdnUrl` (string): Custom CDN base URL for static manifest and bundle delivery.
- `ingestUrl` (string): Custom event ingest base URL used for device telemetry.
- `serverUrl` (string): Control-plane API URL used by self-host tooling such as the CLI. The native runtime does not read it.
- `allowInsecureUrls` (boolean): Allow HTTP for localhost development only. Default: false.
- `manifestKeys` (array): Public verification keys for manifest signature verification on custom/self-hosted setups.
### Types
```txt
type OtaKitPolicy = "off" | "shadow" | "apply-staged" | "immediate";
interface BundleInfo {
id: string;
version: string;
runtimeVersion?: string;
status: "builtin" | "pending" | "trial" | "success" | "error";
downloadedAt?: string;
sha256?: string;
channel?: string;
releaseId?: string;
notes?: string; // release notes, plugin 3.3+
}
interface ReleaseNotes {
text: string; // plain text: render it as text
version: string;
releaseId: string;
}
interface OtaKitState {
current: BundleInfo;
fallback: BundleInfo;
staged: BundleInfo | null;
builtinVersion: string;
rollout: RolloutState | null; // plugin 3.1+
preview: PreviewInfo | null; // plugin 3.2+
}
interface PreviewInfo {
startedAt: string; // ISO timestamp
version?: string; // previewed bundle, once it is running
}
interface RolloutState {
releaseId: string;
version: string;
percent: number; // share of devices (1-99)
bucket: number; // this device's number (1-100) for this rollout
included: boolean; // bucket <= percent
}
interface LatestVersion {
version: string;
runtimeVersion?: string;
url: string;
sha256: string;
size: number;
releaseId: string;
}
type CheckResult =
| { kind: "no_update" }
| { kind: "already_staged"; latest: LatestVersion }
| { kind: "update_available"; latest: LatestVersion };
type DownloadResult =
| { kind: "no_update" }
| { kind: "staged"; bundle: BundleInfo };
```
## REST API
Route: /docs/api
Public OtaKit REST API endpoints for app, bundle, and release automation.
All endpoints are under `/api/v1`. This page covers the public Bearer-token API for automation and server-side tooling. Requests and responses use JSON.
### Authentication
Public REST requests use a Bearer token:
Bearer token CLI / server operations — upload, release, list. Use your organization secret key ( `otakit_sk_...`) or a user access token from `otakit login`. The app ID is part of the URL path.
```txt
# CLI / server operations
Authorization: Bearer otakit_sk_...
```
### Apps
### POST /api/v1/apps
Create a new app. The slug should match your Capacitor app identifier. Returns an appId for plugin and CLI usage.
- Auth: Bearer
Request body
```txt
{ "slug": "com.example.myapp" }
```
Response
```txt
{
"id": "uuid",
"slug": "com.example.myapp",
"createdAt": "ISO timestamp"
}
```
### Bundles
### POST /api/v1/apps/:appId/bundles/initiate
Start a bundle upload session. Returns a presigned PUT URL. Upload your zip file to this URL with Content-Type: application/zip.
- Auth: Bearer
Request body
```txt
{
"version": "1.0.1", // semver string
"size": 1048576, // bundle size in bytes
"sha256": "64-char hex checksum of the zip file",
"runtimeVersion": "2026.04" // optional compatibility lane
}
```
Response
```txt
{
"uploadId": "uuid",
"presignedUrl": "https://...",
"storageKey": "...",
"expiresAt": "ISO timestamp"
}
```
### POST /api/v1/apps/:appId/bundles/finalize
Finalize a bundle upload session. The server checks that the uploaded object exists and that its size matches the initiated session, then creates the bundle record from the stored session data.
- Auth: Bearer
Request body
```txt
{
"uploadId": "uuid"
}
```
Response
```txt
{
"id": "uuid",
"version": "1.0.1",
"sha256": "...",
"size": 1048576,
"runtimeVersion": "2026.04",
"createdAt": "ISO timestamp"
}
```
### GET /api/v1/apps/:appId/bundles
List bundles sorted by creation date (newest first).
- Auth: Bearer
- Query: ?limit=20&offset=0
Response
```txt
{
"bundles": [{ id, version, sha256, size, createdAt }],
"total": 42
}
```
### DELETE /api/v1/apps/:appId/bundles/:bundleId
Delete a bundle. Bundles that are part of a release history cannot be deleted.
- Auth: Bearer
Response
```txt
{ "deleted": true, "id": "uuid" }
```
### GET /api/v1/apps/:appId/bundles/:bundleId/diff
Compare a bundle with another one. Without against or channel, the base is the previous upload of the same runtime version. Zip bundles are read from the archive's central directory and delta bundles from their file list; encrypted bundles cannot be inspected. channel compares with the bundle that channel runs for the target's runtime version (404 when the channel has no release for it); when the target is that bundle, it is compared with the release it replaced there (baseSource channel_previous). against and channel cannot be combined. downloadBytes is what a device on the base downloads: the whole archive for zip updates, at most the content it does not have yet for delta updates (it may already have more), or null when that depends on the device. changes lists up to 5,000 files; unlistedChanges counts the rest. Warning codes: env_file, git_directory and node_modules (warnings); source_map, dotfile, native_installer, large_new_file and size_increase (notes). Warnings help catch build mistakes; they are not a security scan.
- Auth: Bearer
- Query: ?against=<bundleId> or ?channel=<name> (empty for the base channel)
Response
```txt
{
"target": { "id": "…", "version": "1.4.2", "strategy": "deltas", "size": 4508876 },
"base": { "id": "…", "version": "1.4.1", … },
"baseSource": "channel", // "against" | "channel" | "channel_previous" | "previous_upload"
"baseChannel": "production", // with ?channel= (null: base)
"status": "ok", // or "target_unavailable" | "base_unavailable"
"unavailableReason": null, // "encrypted" | "unreadable"
"comparable": true, // false: zip vs deltas, compared by size only
"summary": { "added": 3, "changed": 12, "removed": 1, "unchanged": 210,
"totalBefore": 4299161, "totalAfter": 4508876,
"downloadBytes": 38912 },
"changes": [{ "path": "assets/hero.png", "status": "added",
"sizeBefore": null, "sizeAfter": 3145728 }],
"unlistedChanges": 0,
"warnings": [{ "code": "env_file", "severity": "warning",
"message": "…", "paths": [".env.production"], "count": 1 }]
}
```
### Releases
### POST /api/v1/apps/:appId/releases
Release a bundle to the base channel or a named channel. The runtimeVersion comes from the bundle itself, so current resolution is per (channel, runtimeVersion). A rolloutPercent below 100 releases to that share of devices; it needs a previous release on the lane. While a rollout is active, another release returns 409 ROLLOUT_IN_PROGRESS unless replaceRollout is true.
- Auth: Bearer
Request body
```txt
{
"bundleId": "uuid",
"channel": "staging", // optional; omit or null for base channel
"rolloutPercent": 10, // optional; 1-100, default 100
"replaceRollout": true, // optional; cancel the active rollout first
"notes": "Faster checkout." // optional; plain text, up to 2,000 characters
}
```
Response
```txt
{
"release": {
"id": "uuid",
"channel": null,
"runtimeVersion": "2026.04",
"bundleId": "uuid",
"bundleVersion": "1.0.1",
"rolloutPercent": 10,
"promotedAt": "ISO timestamp"
},
"previousRelease": { ... } | null
}
```
### PATCH /api/v1/apps/:appId/releases/:releaseId/notes
Change a release's notes. The release's channel is republished, so phones that update from now on get the new text; phones that already updated keep what they received.
- Auth: Bearer
Request body
```txt
{ "notes": "Faster checkout.\nPhoto upload fixed." } // or null to remove
```
Response
```txt
{ "release": { ..., "notes": "Faster checkout.\nPhoto upload fixed." } }
```
### PATCH /api/v1/apps/:appId/releases/:releaseId/rollout
Change the percentage of an active rollout. Only the current release of a lane can change while it is below 100%, and a completed rollout stays complete. To cancel a rollout, revert the release: POST /api/v1/apps/:appId/releases/:releaseId/revert, optionally with expectedRolloutPercent so the revert is refused if the rollout completed or changed meanwhile.
- Auth: Bearer
Request body
```txt
{
"percent": 50, // 1-100; 100 completes the rollout
"expectedPercent": 10 // optional; rejects the change if the rollout moved
}
```
Response
```txt
{
"release": { ..., "rolloutPercent": 50 },
"previousPercent": 10,
"publicationStatus": "published"
}
```
### GET /api/v1/apps/:appId/releases
List release history sorted newest first. Omit channel to list every stream, or pass an empty channel value to query only the base channel.
- Auth: Bearer
- Query: ?channel=staging&limit=20&offset=0
Response
```txt
{
"releases": [{
id, channel, runtimeVersion, bundleId, bundleVersion, promotedAt
}],
"total": 12
}
```
### GET /api/v1/apps/:appId/releases/:releaseId/timeseries
A release's client-reported events per UTC hour (24h, 7d) or day (30d), zero-filled, with totals, the rollback share (rollbacks ÷ (applies + rollbacks)) and markers for release, rollout steps and reverts. The range defaults to 24h for releases younger than two days and 7d otherwise. lane=1 adds applies per bucket for the releases of the same channel and runtime that were live during the range, up to 8 around this one. Counts are events, not unique devices; when analytics are unavailable, analyticsAvailable is false.
- Auth: Bearer
- Query: ?range=24h|7d|30d&platform=ios|android&lane=1
Response
```txt
{
"range": "24h",
"granularity": "hour", // "day" for 30d
"asOf": "2026-10-01T12:40:00.000Z", // the last bucket is still filling
"unit": "events",
"analyticsAvailable": true,
"totals": { "downloads": 53, "applied": 48, "downloadErrors": 0,
"rollbacks": 1, "rollbackSharePercent": 2.04 },
"buckets": [{ "start": "2026-10-01T12:00:00.000Z", "downloads": 6,
"applied": 5, "downloadErrors": 0, "rollbacks": 0,
"appliedTotal": 5 }],
"markers": [{ "at": "…", "type": "rollout", "label": "10% → 50%" }],
"lane": { "channel": "production", "releases": [{ "id": "…",
"bundleVersion": "1.4.1", "applied": [3, 2] }] }
}
```
### Preview links
### POST /api/v1/apps/:appId/bundles/:bundleId/previews
Create a preview link that opens the bundle in the installed app, without releasing it. Share url or qrUrl; deepLink is null until the app's URL scheme is known. An app can have 20 active links (409 PREVIEW_LIMIT_REACHED).
- Auth: Bearer
Request body
```txt
{
"expiresIn": "24h", // optional; 1h, 24h, 7d (default) or 30d
"urlScheme": "myapp" // optional; remembered for the app
}
```
Response
```txt
{
"preview": {
"id": "uuid",
"bundleId": "uuid",
"bundleVersion": "1.0.1",
"runtimeVersion": "2026.04",
"expiresAt": "ISO timestamp",
"url": "https://console.otakit.app/p/<token>",
"qrUrl": "https://console.otakit.app/p/<token>/qr.png",
"deepLink": "myapp://otakit-preview?token=<token>" | null
}
}
```
### GET /api/v1/apps/:appId/previews
List active preview links, newest first, optionally for one bundle.
- Auth: Bearer
- Query: ?bundleId=uuid
Response
```txt
{
"previews": [{ id, bundleId, bundleVersion, expiresAt, url, qrUrl, deepLink, ... }],
"urlScheme": "myapp" | null
}
```
### DELETE /api/v1/apps/:appId/previews/:previewId
Revoke a preview link. Phones on it return to their release on their next update check.
- Auth: Bearer
Response
```txt
{ "status": "revoked" | "already_ended", "previewId": "uuid" }
```
## Next.js Guide
Route: /docs/guide
Step-by-step guide: from Next.js + Capacitor to your first OTA update.
This walkthrough takes a Next.js app from zero to its first OTA update with the default hosted OtaKit flow.
### 1. Create the Next.js app
```txt
npx create-next-app@latest my-app
cd my-app
```
### 2. Configure Next.js for static export
```txt
// next.config.ts
const nextConfig = {
output: "export",
};
export default nextConfig;
```
### 3. Add Capacitor
```txt
npm install @capacitor/core @capacitor/cli
npx cap init my-app com.example.myapp
```
Set `webDir` to `out` in `capacitor.config.ts`:
```txt
import type { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
appId: "com.example.myapp",
appName: "my-app",
webDir: "out",
};
export default config;
```
### 4. Add native platforms
```txt
npm install @capacitor/ios @capacitor/android
npx cap add ios
npx cap add android
```
### 5. Install the OtaKit plugin
```txt
npm install @otakit/capacitor-updater
npx cap sync
```
### 6. Create an OtaKit app and log in
Create an app in the OtaKit dashboard and copy its `appId`. Then install the CLI and log in:
```txt
npm install -g @otakit/cli
otakit login
```
### 7. Configure the plugin
```txt
const config: CapacitorConfig = {
appId: "com.example.myapp",
appName: "my-app",
webDir: "out",
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID",
}
}
};
```
### 8. Add notifyAppReady()
Create a client component that confirms the app loaded successfully:
```txt
// app/components/AppReady.tsx
"use client";
import { useEffect } from "react";
import { Capacitor } from "@capacitor/core";
import { OtaKit } from "@otakit/capacitor-updater";
export function AppReady() {
useEffect(() => {
if (Capacitor.isNativePlatform()) {
OtaKit.notifyAppReady();
}
}, []);
return null;
}
```
Add it to your root layout:
```txt
// app/layout.tsx
import { AppReady } from "./components/AppReady";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<AppReady />
{children}
</body>
</html>
);
}
```
### 9. Build and run on a device
```txt
npm run build
npx cap sync
npx cap run ios # or: npx cap run android
```
Note: The app must be published to the App Store (and/or Play Store) at least once with the OtaKit plugin configured before end users’ devices can receive live updates.
### 10. Ship your first OTA update
Make a visible change to your app, rebuild, and release:
```txt
npm run build
otakit upload --release
```
Relaunch the app on your device. By default, OtaKit downloads the update in the background and activates it on the next cold launch.
### Next
- Read Channels & Runtimes for release tracks and native compatibility boundaries.
- Use the Plugin API and CLI reference for exact configuration and command details.
## React Guide
Route: /docs/react
Step-by-step guide: from Vite + React + Capacitor to your first OTA update.
This walkthrough uses Vite + React with Capacitor and the default hosted OtaKit flow.
### 1. Create the React app
```txt
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
```
### 2. Add Capacitor
```txt
npm install @capacitor/core @capacitor/cli
npx cap init my-app com.example.myapp
```
Set `webDir` to `dist` in `capacitor.config.ts`:
```txt
import type { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
appId: "com.example.myapp",
appName: "my-app",
webDir: "dist",
};
export default config;
```
### 3. Add native platforms
```txt
npm install @capacitor/ios @capacitor/android
npx cap add ios
npx cap add android
```
### 4. Install the OtaKit plugin
```txt
npm install @otakit/capacitor-updater
npx cap sync
```
### 5. Create an OtaKit app and log in
Create an app in the OtaKit dashboard and copy its `appId`. Then install the CLI and log in:
```txt
npm install -g @otakit/cli
otakit login
```
### 6. Configure the plugin
```txt
const config: CapacitorConfig = {
appId: "com.example.myapp",
appName: "my-app",
webDir: "dist",
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID",
}
}
};
```
### 7. Add notifyAppReady()
Call `notifyAppReady()` once your app has rendered on a native device:
```txt
// src/AppReady.tsx
import { useEffect } from "react";
import { Capacitor } from "@capacitor/core";
import { OtaKit } from "@otakit/capacitor-updater";
export function AppReady() {
useEffect(() => {
if (Capacitor.isNativePlatform()) {
OtaKit.notifyAppReady();
}
}, []);
return null;
}
```
Render it near the top of your app:
```txt
// src/App.tsx
import { AppReady } from "./AppReady";
export default function App() {
return (
<>
<AppReady />
<main>...</main>
</>
);
}
```
### 8. Build and run on a device
```txt
npm run build
npx cap sync
npx cap run ios # or: npx cap run android
```
Note: The app must be published to the App Store (and/or Play Store) at least once with the OtaKit plugin configured before end users’ devices can receive live updates.
### 9. Ship your first OTA update
Make a visible change, rebuild, and release:
```txt
npm run build
otakit upload --release
```
Relaunch the app on your device. By default, OtaKit downloads the update in the background and activates it on the next cold launch.
### Keep delta updates small
With `updateStrategy: 'deltas'`, OtaKit only downloads files whose contents changed since the device’s last bundle — images, fonts, and unchanged chunks are reused from the on-device cache. Anything that rewrites files on every build defeats that.
The common culprit is a non-deterministic build ID. Next.js (static export) stamps a fresh random ID into every HTML file, RSC payload, and `_next/static/<id>/` path on each build, so a one-line change re-downloads ~20 files instead of one. Pin it so unchanged output stays byte-identical:
```txt
// next.config.ts
const nextConfig = {
output: 'export',
// Stable build ID → minimal OTA deltas. Per-chunk content hashes still
// handle cache-busting, so nothing goes stale.
generateBuildId: async () => 'my-app',
};
export default nextConfig;
```
Vite already emits stable content-hashed filenames, so no change is needed there. To verify determinism for any bundler, run your production build twice without editing source — the output should be byte-for-byte identical (e.g. compare file hashes). If it is, a no-op release downloads nothing and a one-file change downloads one file.
### Next
- Read Channels & Runtimes for release tracks and native compatibility boundaries.
- Use the Plugin API and CLI reference for exact configuration and command details.
## Channels
Route: /docs/channels
Use channels for rollout tracks and runtimeVersion for native compatibility boundaries.
Channels control which audience receives a release — for example, a `staging` channel for testers and the default channel for everyone else. Runtime version creates a compatibility boundary between native builds and OTA bundles — each side only sees releases meant for its version.
Both are optional and build-time settings. Start with neither and add them when needed.
### Channels
#### Base channel
If you omit `channel` from the plugin config, the app uses the unnamed base channel. This is the default and the simplest setup.
```txt
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID"
}
}
```
Release to the base channel:
```txt
otakit upload --release
```
#### Named channels
Add a channel when you want a separate rollout track — for example, internal QA, beta, or a staged production rollout.
```txt
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID",
channel: "staging"
}
}
```
Release to that channel:
```txt
otakit upload --release staging
```
#### Switching channels at runtime
The configured channel is only the default — apps can override it at runtime with `setChannel()`. The override persists across launches and takes effect on the next check; pass `null` to go back to the configured channel.
```txt
// "Join the beta" toggle in a settings screen
await OtaKit.setChannel({ channel: 'beta' }); // next check pulls from beta
await OtaKit.setChannel({ channel: null }); // back to the configured channel
const { channel, source } = await OtaKit.getChannel(); // source: 'override' | 'config'
```
Channel names are public CDN paths — runtime switching is a rollout convenience, not access control.
#### Promoting across channels
You can upload once, test on one channel, then promote the same bundle to another.
```txt
# Upload and release to staging
otakit upload --release staging
# Promote that bundle to production later
otakit release <bundle-id> --channel production
```
### Runtime Version
#### When to use it
`runtimeVersion` is optional. Use it when a new store submission changes what the native shell expects from the web bundle. Devices on the old native build won't receive bundles meant for the new one, and devices on the new build won't receive old bundles.
Without `runtimeVersion`, all OTA releases share one lane per channel. With it, each runtime version gets its own lane.
#### How to use it
Set `runtimeVersion` in the plugin config before building.
```txt
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID",
runtimeVersion: "2026.04"
}
}
```
With that config:
- The plugin only requests releases matching that runtime version.
- CLI uploads inherit the same runtime version from the config.
- Old cached OTA bundles from a different runtime are ignored on startup.
### Common setups
- Single stream — no channel, no runtime version. Everything goes to the base channel.
- Beta + production — use channels like `beta` and `production` to split audiences.
- Gradual production release — release to `production` with `--rollout 10`, then raise the share. See Percentage Rollouts .
- New store baseline: — bump `runtimeVersion` so the new native build starts a fresh OTA lane.
## Rollouts
Route: /docs/rollouts
Release a bundle to a share of devices first, watch its health, then raise the share or complete it.
Release a bundle to a share of devices first — for example 10% of production — check its health, then raise the share or complete the rollout. If it misbehaves, cancel it and every device returns to the previous release.
Rollouts need `@otakit/capacitor-updater` 3.1 or later on the device. Channels choose who gets a release (testers, beta, production); rollouts choose how many of them get it.
### How devices are selected
- Each installation keeps a random secret that never leaves the device. For every rollout it derives a number from 1 to 100 and takes the rolling release when that number is within the percentage.
- The number stays the same for the whole rollout. Raising 10% to 25% adds devices; lowering it moves the extra devices back to the previous release after their next check. A device that already downloaded the rolling bundle may run it once more before switching back, which also applies when a rollout is cancelled.
- Each rollout draws new numbers, so the same devices are not always first.
- No device identifiers are collected. The percentage and the rolling release are part of the signed manifest.
- Devices on plugin versions before 3.1 ignore rollouts and stay on the previous release until the rollout completes.
- The first release on a channel goes to every device, since there is nothing to fall back to.
### Start a rollout
Pass `--rollout` when you release. In the dashboard, choose the share under Roll out to in the release dialog.
```txt
# Upload and release to 10% of production
otakit upload --release production --rollout 10
# Or roll out a bundle you already uploaded
otakit release <bundle-id> --channel production --rollout 10
```
### Raise, complete, or cancel
```txt
otakit rollout # show active rollouts
otakit rollout --channel production --percent 50 # raise or lower the share
otakit rollout --channel production --complete # every device gets it
otakit rollout --channel production --cancel # everyone returns to the previous release
```
In the dashboard, open the channel badge of the rolling bundle for Change percentage, Complete rollout, and Cancel rollout. A completed rollout is final; to undo it, revert the release.
### Releasing during a rollout
A channel has one rollout at a time. Releasing another bundle while one is rolling out is refused, so a rollout is never replaced by accident. Complete or cancel it first, or pass `--replace-rollout` to cancel it and release the new bundle in its place:
```txt
otakit upload --release production --replace-rollout --rollout 10
```
### Health and auto-revert
Devices on the rolling release report events with its own release ID, so its health counts only those devices. Auto-revert works on rolling releases and returns every device to the previous release. At low percentages it takes longer to reach the minimum sample, so small apps should start at 5% or more.
To watch a rollout over time, click the release's channel badge in the bundle row, or choose Activity in its ⋯ menu for the bundle on every channel. The activity chart above the app's device events then shows downloads, applies, errors and rollbacks over time, with each release, rollout step and revert marked, and the rollback share above it. Agents read a release's series with `get_release_timeseries`.
### On the device
`getState().rollout` shows what the last check saw, which helps when testing a rollout on a phone:
```txt
const { rollout } = await OtaKit.getState();
// { releaseId, version, percent: 10, bucket: 57, included: false }, or null
```
### API and agents
The REST API takes `rolloutPercent` and `replaceRollout` when releasing and has an endpoint to change the percentage; see the REST API . Agents use `publish_release` with `rolloutPercent` and `set_rollout_percent`; see MCP & Agent Skills .
## Previews
Route: /docs/previews
Open an uploaded bundle in the installed app on a phone with a link or QR code, without releasing it.
A preview link opens an uploaded bundle in the real app on one phone, without releasing it. Scan the QR code, tap Open in the app, and the app downloads that bundle and reloads into it. Use it to check a change on a device, get a client's sign-off, or let an agent share “here is my change”.
Previews need `@otakit/capacitor-updater` 3.2 or later. Only web code can be previewed; native changes still need a store build.
### Set up the app once
Turn preview links on in the builds that should accept them, for example internal or staging builds:
```txt
// capacitor.config.ts
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID",
previewLinks: true
}
}
```
The app also needs a custom URL scheme, such as `myapp`. Many apps already have one. On iOS add it to `Info.plist`:
```txt
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array><string>myapp</string></array>
</dict>
</array>
```
On Android add an intent filter to the main activity in `AndroidManifest.xml`:
```txt
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
```
The plugin handles `myapp://otakit-preview?…` links itself; no app code is needed, and other links keep reaching your app. On iOS it receives links through Capacitor's `ApplicationDelegateProxy`, which the default `AppDelegate` already forwards to.
### Create a preview link
From the dashboard: Preview link in a bundle row's ⋯ menu. The first link asks for the app's URL scheme and remembers it. From the CLI:
```txt
# Upload and create a link in one step
otakit upload --preview
# Or preview a bundle you already uploaded
otakit preview <bundle-id> --expires 24h --scheme myapp
otakit preview --list
otakit preview --revoke <preview-id>
```
The CLI prints the link and a QR code. Links last 7 days by default (1h, 24h, 7d or 30d), and an app can have 20 active links.
### What the tester sees
- The link opens a page with the bundle version, expiry, a QR code on desktop, and the buttons Open in the app and Exit preview.
- Opening it downloads the bundle like any update, with signature checks, and reloads the app. The app still calls `notifyAppReady()`; a bundle that fails to start rolls back and ends the preview.
- The app returns to its normal release when the tester taps Exit preview, when your code calls `OtaKit.stopPreview()`, or when the link expires or is revoked. The app notices that on its next update check (launch, resume, or `OtaKit.update()`). If the channel has nothing to download, it returns to the bundle it ran before the preview, or to the one built into the app.
- Nobody else is affected: a preview is not a release, does not appear as a channel, and does not count towards release health or auto-revert. Its downloads count as downloads.
```txt
const { preview } = await OtaKit.getState(); // { startedAt, version } or null
if (preview) showBanner(`Preview ${preview.version}`, () => OtaKit.stopPreview());
OtaKit.addListener('previewFailed', ({ reason }) => {
// 'unavailable': expired, revoked, or built for another runtime version
// 'busy': another update did not finish in time; open the link again
});
```
### Security
Each link carries a random 130-bit token and works only for the bundle it was created for. Anyone with the link and a build that accepts previews can open it, so keep `previewLinks` off in builds that should never show unreleased work (such a build also ends a preview an earlier build started), and revoke links you no longer need.
### API and agents
See the REST API for the preview endpoints. Agents use `create_preview` and `revoke_preview`; see MCP & Agent Skills .
## Release Notes
Route: /docs/release-notes
Attach short notes to a release and show them once in the app as a "What's new" screen.
Attach a few lines to a release, such as “Faster checkout. Photo upload fixed on Android.” Your team sees them next to the release in the dashboard, and the app can show them once after the update as a “What's new” screen.
Notes are plain text with line breaks, up to 2,000 characters. Showing them in the app needs `@otakit/capacitor-updater` 3.3 or later; everything else works with any plugin version.
### Write notes when you release
In the dashboard, the release dialog has a “Release notes” field. From the CLI:
```txt
otakit release --channel production --notes "Faster checkout.
Photo upload fixed on Android."
# Or from a file, in the same command that uploads
otakit upload --release production --notes-file NOTES.txt
```
Agents pass `notes` to `prepare_release` and `publish_release`, and show you the text before publishing. Over the API, send `notes` with the release; see the REST API .
Notes belong to the release, so the same bundle can carry different notes on beta and production. Write them for the people using your app: what changed for them, in a few short lines.
### Show “What's new” in the app
After an update, `getUnseenReleaseNotes()` returns the running release's notes until you mark them seen, so each release shows once per phone:
```txt
import { OtaKit } from '@otakit/capacitor-updater';
await OtaKit.notifyAppReady(); // first, so a bundle that rolls back never marks its notes seen
try {
const notes = await OtaKit.getUnseenReleaseNotes(); // { text, version, releaseId } or null
if (notes) {
showWhatsNew(notes.text);
await OtaKit.markReleaseNotesSeen();
}
} catch {
// A native shell on plugin 3.2 or older doesn't have these methods.
}
function showWhatsNew(text: string) {
const sheet = document.createElement('div');
sheet.className = 'whats-new';
sheet.textContent = text; // plain text: never insert it as HTML
document.body.append(sheet);
}
```
Render notes as text, with `white-space: pre-line` for line breaks. Never insert them as HTML. The bundle the app is running also exposes its notes as `getState().current.notes` and in the `updateApplied` event.
### Edit notes later
Fix a typo or polish the copy from the release's menu in the dashboard (“Edit release notes”) or with `PATCH /releases/:releaseId/notes`. Phones that update from then on get the new text; phones that already updated keep the notes they received.
### Security
Notes travel in their own signed block of the release manifest. The app only shows notes signed for that exact release, so they cannot be changed in transit. Older plugin versions ignore the block and update as before.
## Update Strategies
Route: /docs/update-strategies
How launchPolicy, resumePolicy, and runtimePolicy combine to control when updates apply.
OtaKit has one set of four policy values and three settings that decide when each one runs. The values — `off`, `shadow`, `apply-staged`, and `immediate` — describe what happens. `launchPolicy`, `resumePolicy`, and `runtimePolicy` describe when it happens. This guide walks through picking a combination; for the full field list see the Plugin API reference.
### The four policy values
- off — do nothing automatically. Nothing is checked, staged, or applied.
- shadow — check for the latest update and stage it locally, but never apply it. The current session keeps running on the bundle it already has.
- apply-staged — apply a bundle if one is already staged. If nothing is staged, it falls back to shadow behavior for that check.
- immediate — check, stage, and apply the newest bundle right away when one is available. The reload can land mid-session — and since any release can be escalated with `--force-immediate`, you rarely need this as the launch or resume default.
### When each setting runs
- `launchPolicy` — every normal cold start, once the app's runtime lane is already resolved. This is the one most apps tune, since it covers the vast majority of app opens. Default: `apply-staged`.
- `resumePolicy` — when the app returns to the foreground from the background, throttled by `checkInterval`. Default: `shadow`.
- `runtimePolicy` — cold start specifically when `runtimeVersion` changes or resolves for the first time. In practice that's the app's first native install, or a native update where you bumped `runtimeVersion` in the plugin config before that release. Default: `immediate`, so a fresh runtime lane catches up fast instead of waiting on a bundle staged under the old lane.
### Recommended combinations
#### Hosted default — right for most apps
`runtimePolicy: "immediate"`, `launchPolicy: "apply-staged"`, `resumePolicy: "shadow"`. Fast on first install, seamless between launches, and new bundles are ready to go as soon as a session restarts.
#### Update quietly, reload only with consent
Set `launchPolicy` and `resumePolicy` to `shadow`. Bundles stage in the background and nothing ever activates on its own — show a "restart to update" prompt from the `updateStaged` event and call `apply()` on accept (see Events & Listeners ).
#### Full manual control
Turn all three policies `off` and drive the lifecycle yourself with `check()`, `download()`, and `apply()`. Use this when your app owns the entire update experience — see the Manual Flow section of the Plugin API reference.
#### Ship critical fixes instantly
This one isn't a config — it's per release. Mark it force immediate in the dashboard's release dialog or with `otakit release --force-immediate`: the flag is baked into the signed manifest, and devices on `shadow` or `apply-staged` escalate that one release to immediate — download, apply, and reload on their next check. No rebuild, no config change.
It's "immediate on the next check", not push, and the trial/rollback safety net still applies. Policies set to `off` never fetch a manifest, so they never see the flag. Because the reload can land mid-session, reserve it for broken releases — not routine rollouts.
Also see Channels & Runtime Version for how rollout audience and native compatibility lanes interact with these policies.
## Events & Listeners
Route: /docs/events
Subscribe to update lifecycle events — update available, staged, applied, failed, rolled back.
The plugin emits lifecycle events through the standard Capacitor listener API. Events complement — they don't replace — the pull APIs `check()` and `getState()`: listeners tell you the moment something happens while your app is running, and the pull APIs reconcile anything that happened while it wasn't.
```txt
import { OtaKit } from '@otakit/capacitor-updater';
OtaKit.addListener('updateStaged', ({ bundle }) => {
// show "Update ready — restart now?" and call OtaKit.apply() on accept
});
```
### Event reference
- `updateAvailable` (LatestVersion): A check found a newer bundle, before any download starts. Fires on manual check() and at the start of automatic flows.
- `updateStaged` ({ bundle: BundleInfo }): A bundle was downloaded, verified, and staged, ready to apply. Download and stage are atomic in OtaKit, so this is the single 'downloaded' event.
- `updateApplied` ({ bundle: BundleInfo }): A newly activated bundle was confirmed healthy — fires when notifyAppReady() confirms the trial bundle, in the reloaded JS context.
- `downloadFailed` ({ version, runtimeVersion?, releaseId?, channel?, reason }): A download, verification, or extraction failed. Non-terminal: the app keeps running its current bundle and retries on a later cycle.
- `rollback` ({ version, runtimeVersion?, releaseId?, channel?, reason }): An applied bundle failed its health check (notifyAppReady timeout) and was reverted to the previous bundle.
- `previewFailed` ({ reason: 'unavailable' | 'download_failed' | 'busy' }): A preview link could not be opened (plugin 3.2+). unavailable: the link expired, was revoked, or was built for another runtime version. busy: another update did not finish in time. The app keeps running its current bundle.
### Caveats
- Events fire only while the app process is alive. Nothing is delivered for work that happened while the app was killed or before your listener attached — there is no buffering or replay.
- Reconcile with `getState()` and `getLastFailure()` on startup. A bundle staged in a previous session shows up in `getState().staged`, and a startup rollback (the app restarted before `notifyAppReady()`) happens before any JS runs, so it can only be observed via `getLastFailure()`.
- `apply()` destroys the old JS context. A listener registered before the restart will not see `updateApplied` — that event fires in the reloaded bundle. Attach `updateApplied` and `rollback` listeners early in app startup, not inside the restart click handler.
- One `updateStaged`, no separate "downloaded" event. Download, hash verification, and staging are one atomic operation.
The apply timeline, end to end:
```txt
updateStaged (old context)
-> user taps "Restart"
-> OtaKit.apply() (old JS context ends here; WebView reloads)
-> new bundle boots
-> app calls notifyAppReady()
-> updateApplied (NEW context - attach this listener at startup)
```
### Use cases
From the smallest building block to the most hands-off setup — each pattern moves one more step of the flow from your code into the plugin.
#### 1. Check for an update, download if the user says yes
The simplest pattern, no listeners at all — a "Check for updates" button in a settings screen, driven entirely by the pull API:
```txt
async function checkForUpdates() {
const result = await OtaKit.check();
if (result.kind === 'no_update') {
toast("You're up to date");
return;
}
// update_available or already_staged
if (await confirmDownload(result.latest.version)) {
const download = await OtaKit.download(); // no-op if already staged
if (download.kind === 'staged' && (await confirmRestart())) {
await OtaKit.apply(); // terminal: reloads into the new bundle
}
}
}
```
#### 2. Ask before every download
Same consent flow, but event-driven instead of a button — pair manual mode with `updateAvailable` so the user approves the download (useful on metered connections):
```txt
// capacitor.config.ts: { launchPolicy: 'off', resumePolicy: 'off' }
OtaKit.addListener('updateAvailable', async (latest) => {
if (await confirmDownload(latest.version)) {
await OtaKit.download(); // emits updateStaged when done
}
});
await OtaKit.check();
```
#### 3. Auto-download in the background, prompt to apply
The most hands-off custom flow: `shadow` mode does the checking, downloading, throttling, and deduplication, and your code only owns the restart prompt:
```txt
// capacitor.config.ts
plugins: {
OtaKit: { launchPolicy: 'shadow', resumePolicy: 'shadow' }
}
```
```txt
// app startup
const state = await OtaKit.getState();
if (state.staged) showRestartPrompt(state.staged);
OtaKit.addListener('updateStaged', ({ bundle }) => showRestartPrompt(bundle));
// in the prompt's accept handler
await OtaKit.apply();
```
#### 4. Confirm and report after the fact
Works alongside any of the above (or the fully automatic default) — a toast when an update landed, and error reporting when one failed:
```txt
// attach at startup, in the reloaded bundle
OtaKit.addListener('updateApplied', ({ bundle }) => {
toast('Updated to ' + bundle.version);
});
OtaKit.addListener('downloadFailed', (e) => reportError('ota_download', e));
OtaKit.addListener('rollback', (e) => reportError('ota_rollback', e));
// startup rollbacks can't reach a listener - reconcile:
const failure = await OtaKit.getLastFailure();
if (failure) reportError('ota_rollback_startup', failure);
```
### Cleanup
`addListener` resolves to a `PluginListenerHandle`; call `.remove()` on it when the owning UI unmounts, or `OtaKit.removeAllListeners()` to drop everything at once.
Also see Update Strategies for choosing the policies these events observe, and the Plugin API reference for the full method list.
## Push Notifications
Route: /docs/push
Free push notifications for Capacitor apps: upload your APNs key and Firebase service account, register devices with @otakit/push, and send from the dashboard, CLI, API or an AI agent.
OtaKit sends push notifications to your Capacitor app on iOS and Android. It uses the official `@capacitor/push-notifications` plugin on the device and talks to Apple (APNs) and Google (Firebase Cloud Messaging) directly with your own keys. There is no extra native SDK, and it works with or without OtaKit live updates.
Push is an add-on. Turn it on per workspace in the dashboard under Settings → Add-ons → Push notifications; a Push page then appears in the menu.
### 1. Upload your keys
Open Push → Setup and choose the app. Only workspace owners and admins can upload keys. Keys are encrypted at rest and never shown again after upload.
#### iOS: APNs key
1. In the Apple Developer account, open Certificates, IDs & Profiles → Keys, create a key with Apple Push Notifications service (APNs) enabled, and download the `.p8` file (Apple lets you download it once).
2. Note the Key ID (10 characters) and your Team ID (top right of the developer account).
3. Make sure your bundle ID is registered under Identifiers with the Push Notifications capability, in the same team as the key.
4. Upload the `.p8` file with the Key ID, Team ID and bundle ID, then click Test. One key works for development and production builds.
#### Android: Firebase service account
1. In the Firebase console, add an Android app with your application ID and download `google-services.json` (you need it in step 2).
2. Open Project settings → Service accounts and click Generate new private key.
3. Upload the JSON file and click Test. A key created a minute ago can be rejected briefly while Google activates it.
### 2. Install
```txt
npm install @capacitor/push-notifications @otakit/push
npx cap sync
```
#### iOS project
In Xcode, add the Push Notifications capability to the app target, then add the two `AppDelegate` methods from the Capacitor push notifications guide :
```txt
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
}
```
#### Android project
Copy `google-services.json` to `android/app/`. The Capacitor Android template applies the Google Services plugin when the file is present. Android 13 and newer show notifications only after the user grants the notification permission.
### 3. Register devices
```txt
import { PushNotifications } from '@capacitor/push-notifications';
import { OtaKitPush } from '@otakit/push';
OtaKitPush.init({ appId: 'YOUR_OTAKIT_APP_ID' });
PushNotifications.addListener('registration', ({ value }) => {
OtaKitPush.syncToken(value, { userId: currentUser?.id ?? null, topics: ['news'] });
});
const permission = await PushNotifications.requestPermissions();
if (permission.receive === 'granted') {
await PushNotifications.register();
}
// Open a screen when the user taps a notification.
PushNotifications.addListener('pushNotificationActionPerformed', ({ notification }) => {
const url = notification.data?.url;
if (url) router.push(url);
});
```
The app ID is the same one you use for live updates (`plugins.OtaKit.appId`). The device appears under Push → Devices within seconds.
#### OtaKitPush API
Method
What it does
API_ROWS.map(([method, description]) => ( <tr key={method} className="border-b border-border align-top"> <td className="py-2 pr-4"> <Code>{method}</Code> </td> <td className="py-2">{description}</td> </tr> ))
Every method resolves with a status instead of throwing, so push setup never breaks your app. When OtaKit live updates are installed, the device's OTA channel and bundle version are attached automatically, so you can target by channel.
#### Development builds
Builds run from Xcode get sandbox tokens. On the first send, OtaKit notices when Apple rejects such a token on production, delivers it through the sandbox instead, and remembers that for the device. You can also pass `environment: 'sandbox'` to `init` in debug builds.
### 4. Send
Dashboard: Push → Send. Write the title and text, choose the audience, and check the device count before you confirm.
CLI (`@otakit/cli` 1.7.0 or newer):
```txt
otakit push send --title "Your order shipped" --body "Track it in the app" --url /orders --user user_42
otakit push campaigns
```
REST API with an organization key. Preview first to see the audience, then send with an idempotency key so a retry never sends twice:
```txt
curl -X POST https://console.otakit.app/api/v1/apps/$APP_ID/push/campaigns \
-H "Authorization: Bearer $OTAKIT_TOKEN" \
-H "Idempotency-Key: order-42-shipped" \
-H "Content-Type: application/json" \
-d '{
"payload": { "title": "Your order shipped", "body": "Track it in the app", "url": "/orders" },
"audience": { "userIds": ["user_42"] }
}'
```
`POST …/push/audience` takes the same body and returns the device count and warnings without sending. `GET …/push/campaigns/:id` returns delivery counts.
#### Message
`title` (up to 100 characters) and `body` (up to 1,000) are required. Optional: `url` (a path like `/inbox` or an https link, delivered as `data.url`), `data` (up to 20 string values), `sound`, `badge` and `ttlSeconds` (default one day). The whole message must fit Apple's 4 KB limit.
#### Audience
Filter by `platforms`, `topics`, `userIds`, `channels` (OTA channel), `runtimeVersions` or `deviceIds`. Different filters must all match; values inside one filter match any. An empty audience sends to every device of the app.
### Limits
Plan
Devices
Notifications per month
LIMIT_ROWS.map(([plan, devices, sends]) => ( <tr key={plan} className="border-b border-border"> <td className="py-2 pr-4">{plan}</td> <td className="py-2 pr-4">{devices}</td> <td className="py-2">{sends}</td> </tr> ))
Over the device limit, new devices are not stored and the app is told why; it keeps working. Invalid tokens reported by Apple or Google are removed automatically.
### Troubleshooting
You see
Fix
TROUBLESHOOTING.map(([problem, fix]) => ( <tr key={problem} className="border-b border-border align-top"> <td className="py-2 pr-4"> <Code>{problem}</Code> </td> <td className="py-2">{fix}</td> </tr> ))
## CI Automation
Route: /docs/ci
Use GitHub Actions to build, upload, and optionally release OtaKit bundles.
This workflow builds your app, uploads a bundle, and can either leave it uploaded, release it to the base channel, or release it to a named channel.
### 1. Add repository secrets and variables
GitHub repository secrets:
```txt
OTAKIT_TOKEN=otakit_sk_...
OTAKIT_APP_ID=app_...
```
Optional GitHub repository variables:
```txt
OTAKIT_RELEASE_CHANNEL=base
# Leave empty or unset for upload-only.
# Use "base" for the base channel.
# Use a channel name like "staging" for a named release track.
```
### 2. Copy this workflow
Create `.github/workflows/otakit.yml`:
```txt
name: OTA upload
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
upload:
runs-on: ubuntu-latest
env:
OTAKIT_TOKEN: ${{ secrets.OTAKIT_TOKEN }}
OTAKIT_APP_ID: ${{ secrets.OTAKIT_APP_ID }}
OTAKIT_RELEASE_CHANNEL: ${{ vars.OTAKIT_RELEASE_CHANNEL }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
- name: Upload bundle
run: |
if [ -z "$OTAKIT_RELEASE_CHANNEL" ]; then
npx --yes @otakit/cli@latest upload
elif [ "$OTAKIT_RELEASE_CHANNEL" = "base" ]; then
npx --yes @otakit/cli@latest upload --release
else
npx --yes @otakit/cli@latest upload --release "$OTAKIT_RELEASE_CHANNEL"
fi
```
This workflow uses the default bundle path from `capacitor.config.ts`.
### Release channel options
- Empty or unset `OTAKIT_RELEASE_CHANNEL`: upload only
- `OTAKIT_RELEASE_CHANNEL=base`: upload and release to the base channel
- `OTAKIT_RELEASE_CHANNEL=staging`: upload and release to the `staging` channel
### Self-hosted addition
If you run your own OtaKit server, add this environment variable to the workflow as well:
```txt
OTAKIT_SERVER_URL: ${{ secrets.OTAKIT_SERVER_URL }}
```
### Best practices
- Pin the CLI version after your first successful run instead of using `@latest` forever.
- Use the base channel for the simplest production setup. Add a named channel only when you need a separate rollout track such as `staging`.
- Use branch protection so OTA uploads only run from trusted branches.
### Next
- Use the CLI reference for exact upload, login, and token commands.
## MCP & Agent Skills
Route: /docs/agents
Let Claude Code, Codex, and VS Code ship your Capacitor OTA updates — with the same review and approval your team already uses.
Ask your coding agent to ship an update and it will read your Capacitor project, check whether the change is safe to send over the air, upload the build, and then stop and show you exactly what it is about to publish. Nothing reaches a device until you approve it.
### Set it up
One command, from your project directory:
```txt
npx -y @otakit/cli@latest connect
```
It detects your client, signs you in if you are not already, and prints the console, organization, project, and app it resolved — plus the exact file it will write — before writing anything:
```txt
Connecting Claude Code (detected).
console https://console.otakit.app
organization Acme Inc
signed in as dev@acme.example
project /Users/dev/shop
app com.acme.shop (from config)
Will add server "otakit" in .mcp.json:
...
Write it? [y/N]
```
`--dry-run` prints the same plan and writes nothing. `--client` overrides detection. `--yes` skips the prompt in scripts.
#### Claude Code
The official plugin ships the server and the OtaKit Skill together, so this is the whole setup:
```txt
npx -y @otakit/cli@latest login
claude plugin marketplace add OtaKit/otakit
claude plugin install otakit@otakit
```
Run `/mcp` and you should see `otakit` connected to whichever project you have open.
#### Codex
```txt
npx skills add https://github.com/OtaKit/otakit --skill otakit
npx -y @otakit/cli@latest login
codex mcp add otakit -- npx -y @otakit/cli@latest mcp
```
#### VS Code and GitHub Copilot
Create `.vscode/mcp.json`, then run MCP: List Servers to trust and start it:
```txt
{
"servers": {
"otakit": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@otakit/cli@latest", "mcp", "--project-root", "${workspaceFolder}"]
}
}
}
```
### What to ask for
Clients that support MCP prompts expose these as slash commands. Plain language works just as well.
Command
What it does
Or just say what you want:
“Upload version 2.4.1 to staging but don’t publish it — tell me the bundle ID.”
“Prepare this for production with auto-revert on, then wait for me.”
“Why are people on 1.4.2 seeing rollbacks?”
“Roll production back to the previous release.”
### Before anything ships
Publishing and reverting always stop for approval, and always show the same block — so you read it at a glance instead of parsing whatever the agent decided to write:
```txt
Publish com.acme.shop
lane base · runtime 2026.04
from 1.4.0 -> 1.5.0
native compatible (12 packages unchanged)
immediate no auto-revert on · 10% · min 100
Approve? This goes live for every device on that lane.
```
Uploading is separate from publishing, so an agent can prepare a build for you to look at with no risk of it reaching a device. A publish carries the release state the agent reviewed, so if a teammate releases in between, yours is rejected instead of silently overwriting theirs. Every write is attributed in the audit log.
Native compatibility is checked against what the current release actually shipped. If your app gained a native dependency, that needs a store build rather than an OTA update, and the agent stops and says so. If it cannot read your dependencies at all, it says that too instead of guessing.
### Working without a checkout
The setup above runs OtaKit in your repository, which is what lets it inspect the project and upload builds. There is also a remote endpoint over HTTPS at `console.otakit.app/mcp` for clients that cannot run a local process, and for CI. It does everything except the parts that need your files:
- **Inspect the Capacitor project**: Local MCP — Yes; Remote MCP — No
- **Check native compatibility**: Local MCP — Yes; Remote MCP — No
- **Package and upload web assets**: Local MCP — Yes; Remote MCP — No
- **Read apps, bundles, releases, and events**: Local MCP — Yes; Remote MCP — Yes
- **Prepare, publish, and revert releases**: Local MCP — Yes; Remote MCP — Yes
- **Signs in with**: Local MCP — CLI login; Remote MCP — OAuth or key
Add it alongside the local server, not instead of it — running both means your agent sees two copies of the shared tools.
```txt
# Claude Code
claude mcp add-json --scope project otakit-remote \
'{"type":"http","url":"https://console.otakit.app/mcp","oauth":{"scopes":"otakit:read otakit:app:write otakit:bundle:write otakit:release:write"}}'
claude mcp login otakit-remote
# Codex
codex mcp add otakit-remote --url https://console.otakit.app/mcp
codex mcp login --oauth-client-registration cimd \
--scopes otakit:read,otakit:app:write,otakit:bundle:write,otakit:release:write,offline_access \
otakit-remote
```
Clients that add a server by its URL connect the same way:
- Claude (claude.ai and Claude Desktop): Settings → Connectors → Add custom connector, with the URL `https://console.otakit.app/mcp`.
- ChatGPT: Settings → Apps → Advanced → Developer mode → Create app, with the URL `https://console.otakit.app/mcp` and OAuth authentication.
- Cursor: Add to Cursor
- VS Code: Add to VS Code
The browser flow shows the client, the organization, and what it is asking for before anything is granted. Clients that connect with just the URL start read-only: the write tools are listed, and the first write sends you back to the browser to approve the extra access. Leave the write scopes out of the commands above to start read-only. Revoke a connection from Settings → Agents and its tokens stop working immediately. For CI, use an organization key in `OTAKIT_TOKEN` and keep it in your secret store, never in a project file.
### Which organization it uses
A connection is bound to one organization for its lifetime, and it tells the agent which one as soon as it connects. A project with a configured `appId` uses that app’s organization. Without one, it uses the default you chose at login — change it with `otakit organization select` and restart the server. Automation can set `OTAKIT_ORGANIZATION_ID` instead.
This is deliberately separate from the dashboard: switching workspaces in the browser does not move a running agent connection.
### Self-hosting
Use your own console origin everywhere — `otakit connect` picks it up from `plugins.OtaKit.serverUrl` automatically. Remote MCP lives at `<console-origin>/mcp`.
The agent features roll out in stages and ship disabled. Back up and test the database, apply the additive migrations through your normal reviewed process, then deploy the code with every new flag off. Enable release reliability, remote MCP, and OAuth one at a time. OtaKit never migrates a live database for you.
See the self-hosting guide, and the Agent Skill source for the flags and rollout checklist.
### Reference
- OtaKit CLI reference — every command the agent falls back to
- Codex MCP
- Claude Code MCP
- VS Code MCP
- Agent Skills specification
## Security
Route: /docs/security
How OtaKit secures your OTA update delivery pipeline.
OtaKit is designed so that a compromised CDN or network cannot push malicious code to your users. Every layer — from upload to delivery to activation — has a verification step.
### Manifest signing
Every manifest is signed with ES256 (ECDSA P-256) when a release is published. The plugin verifies the signature before trusting the manifest. A tampered manifest — whether modified in transit, at the CDN edge, or in storage — is rejected.
The signing key stays on your server. The plugin only holds the public verification key. Self-hosters generate their own key pair with `otakit generate-signing-key`.
### Bundle verification
Every bundle download is verified against the SHA-256 hash in the signed manifest. If the hash does not match — due to corruption, tampering, or a partial download — the bundle is discarded and the update is not applied.
### End-to-end bundle encryption (optional)
For code-sensitive or regulated apps, bundles can be encrypted with AES-256-GCM before upload. Generate a key with `otakit generate-encryption-key`, keep it in CI as `OTAKIT_ENCRYPTION_KEY`, and ship the same key in the app's `bundleKeys` config. Object storage and the CDN then only ever hold ciphertext — a leaked URL or bucket exposes nothing readable, and even OtaKit cannot decrypt your code.
The encryption parameters are covered by the manifest signature, decryption happens on device only after the download passes its hash check, and a decryption failure behaves like a failed download: the running bundle is untouched. This is confidentiality, not DRM — the key ships inside your binary, like every client-side scheme.
### Native compatibility guardrail
The most common way OTA updates break an app is shipping a web bundle that calls native code the installed shell doesn't have. At upload time the CLI fingerprints your native plugin dependencies and compares them against the channel's current release — new or changed native code produces a warning (or a CI failure with `--fail-on-incompatible`) before the release ever ships.
### Automatic rollback
After a new bundle is activated, the app must call `notifyAppReady()` within a configurable timeout (default 10 seconds). If the call does not arrive — because the new bundle crashes, hangs, or breaks — the plugin automatically rolls back to the last known-good bundle.
This means a bad OTA release self-heals on the device without user intervention.
### Infrastructure
On the managed OtaKit service, manifests and bundles are served from Cloudflare's global CDN with 300+ edge locations. Download availability inherits Cloudflare's infrastructure SLA. Bundles are stored in Cloudflare R2 with encryption at rest.
The dashboard and control plane run on isolated infrastructure. Device traffic (manifest fetches, bundle downloads) never touches the dashboard — it goes directly to the CDN.
### Data collection
OtaKit collects only device events (download, applied, rollback, error) for analytics and billing. Events include an app ID, platform, bundle version, and a random event ID. No persistent device identifiers, IP addresses, or personally identifiable information is stored.
Self-hosters control the full data pipeline. The ingest service and analytics are optional and can be omitted entirely.
### Open source
The entire OtaKit codebase — plugin, CLI, dashboard, and ingest service — is open source under the MIT license. The signing and verification logic can be audited directly on GitHub .
Self-hosting gives you full control over infrastructure, keys, and data. No vendor lock-in.
### HTTPS enforcement
The plugin enforces HTTPS for all manifest and bundle requests. HTTP is only allowed for `localhost` during development when explicitly opted in via `allowInsecureUrls`.
### API authentication
All dashboard and CLI operations require authentication via organization service keys or session tokens. Service keys currently carry the organization's full operational API authority; they are not fine-grained scopes. Keys are hashed before storage — the raw key is shown once at creation and never stored. Current organization membership permits normal app, upload, and release operations, while owner/admin checks protect team, key, billing, organization settings, and audit administration. Remote OAuth connections additionally intersect their granted scopes with current membership on every call.
### Reporting a vulnerability
If you discover a security issue, please email security@otakit.app . We aim to respond within 7 business days.
## Self-hosting
Route: /docs/self-host
Run the full OtaKit platform on your own infrastructure, step by step.
OtaKit is MIT-licensed and fully open source — everything the hosted service runs is in the public repo. The fastest way to ship OTA updates is the managed service at otakit.app (free tier, zero infrastructure). But if you need full control over delivery, keys, and data, this guide walks you through running your own instance end to end.
There is no all-in-one Docker image yet. The control plane is a standard Next.js app, so it runs anywhere Node.js runs — a VPS behind nginx, your own container, or Vercel — plus a Postgres database and an S3-compatible bucket. That's the whole required stack.
### How it works
Three things to understand before you deploy anything:
The console publishes to storage.
When you run
otakit upload --release
, the console API writes the bundle zip and a manifest JSON to your object storage bucket.
Devices update from the CDN, not from your server.
The Capacitor plugin fetches the manifest and bundle straight from the CDN domain in front of the bucket. Your console can be completely down and updates keep working.
Everything else is optional.
Analytics, email, billing, rate limiting, and CDN purge all degrade gracefully when their env vars are unset. You can start with the minimal stack and add pieces later.
### What you need
#### Required
Console
(
packages/console
) — the Next.js control plane: dashboard, auth, API, and manifest publishing. This is the only server you must run.
PostgreSQL 14+
— apps, bundles, releases, users.
S3-compatible object storage
— Cloudflare R2 or AWS S3, for bundle zips and manifests.
A public CDN domain
in front of the bucket — devices download manifests and bundles from here.
One sign-in method
— Google, Apple, or GitHub OAuth credentials, or email OTP (needs Resend in production).
#### Optional
Ingest Worker + Tinybird
(
packages/ingest
,
tinybird/
) — device event analytics. Without it, updates work fine; the dashboard just shows empty analytics.
Manifest signing
— ES256 signatures on manifests. Strongly recommended for production; see below.
Resend
— transactional email (OTP codes, invites). Without it, emails are logged to the server console.
Cloudflare cache purge
— instant CDN invalidation after a release. Without it, manifests may be stale until the CDN TTL (minutes) expires.
Upstash Redis
— API rate limiting. Without it, rate limiting is disabled.
Polar
— billing. Leave it unset when self-hosting: all organizations get unlimited usage and the billing UI is hidden.
Public site
(
packages/site
) — the marketing site and docs you're reading now. You don't need it.
The CLI (`packages/cli`) and Capacitor plugin ( `packages/capacitor-plugin`) run on your machine and inside your app — you point them at your instance in steps 6 and 7.
### Step 1 — Clone and install
You need Node.js 20.9+ and pnpm 9+.
```txt
git clone https://github.com/OtaKit/otakit
cd otakit
pnpm install
```
### Step 2 — Create the database
Any PostgreSQL 14+ works — a managed database (Neon, RDS, Supabase) or your own server. Note the connection string; you'll set it as `DATABASE_URL` in step 4.
### Step 3 — Create the storage bucket and CDN
Create a bucket on Cloudflare R2 or AWS S3 and put a public CDN domain in front of it (e.g. R2 custom domain, or CloudFront for S3). Generate S3 API credentials with read/write access to the bucket. The public domain becomes `CDN_BASE_URL` — devices will fetch manifests and bundles from it, so it must be publicly readable.
### Step 4 — Configure the console
Copy `packages/console/.env.example` to `.env`. It is annotated with the same required/optional split as this page. The required variables:
```txt
# Database
DATABASE_URL=postgresql://user:pass@host:5432/otakit
# API write secret for admin/CLI endpoints
SECRET_KEY=change-me # openssl rand -hex 32
# Auth + public URLs
BETTER_AUTH_SECRET=change-me # openssl rand -hex 32
BETTER_AUTH_URL=https://console.your-domain.com
NEXT_PUBLIC_APP_URL=https://console.your-domain.com
NEXT_PUBLIC_SITE_URL=https://console.your-domain.com
# At least one sign-in provider
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
# or APPLE_CLIENT_ID / APPLE_CLIENT_SECRET
# or GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET
# Object storage (R2 shown; any S3-compatible API works)
R2_BUCKET=otakit-bundles
R2_ACCESS_KEY=...
R2_SECRET_KEY=...
R2_ENDPOINT=https://<account>.r2.cloudflarestorage.com
# Public CDN domain in front of the bucket
CDN_BASE_URL=https://cdn.your-domain.com
```
Everything else in `.env.example` (Tinybird, Resend, Polar, Cloudflare purge, Upstash, signing) is optional and covered at the end of this page.
### Step 5 — Migrate, build, start
```txt
cd packages/console
pnpm db:migrate
pnpm build
pnpm start
```
The console listens on port 3000. Put a reverse proxy (nginx, Caddy) in front with HTTPS — or deploy it like any other Next.js app, to Vercel or your own container platform.
### Step 6 — Sign in and create your app
Open your console URL, sign in, and create an app. Copy its `appId` and create an API token (`otakit_sk_...`) for the CLI.
### Step 7 — Point the CLI at your instance
```txt
export OTAKIT_SERVER_URL=https://console.your-domain.com
export OTAKIT_TOKEN=otakit_sk_...
```
Alternatively set `serverUrl` in the plugin config (next step) — the CLI reads it from `capacitor.config.*` automatically — and authenticate with `otakit login` instead of an env token.
### Step 8 — Configure the plugin
In your app's `capacitor.config.ts`:
```txt
plugins: {
OtaKit: {
appId: "YOUR_APP_ID",
cdnUrl: "https://cdn.your-domain.com", // required: your CDN domain
serverUrl: "https://console.your-domain.com", // optional: lets the CLI find your console
ingestUrl: "https://ingest.your-domain.com/v1", // optional: only with analytics (below)
manifestKeys: [ // optional: only with signing (below)
{ kid: "key-2026-01", key: "MFkwEwYH..." }
]
}
}
```
### Step 9 — Ship a release and verify
```txt
# build your web assets, then:
otakit upload --release
```
Confirm the release appears in your dashboard, and that the manifest is publicly readable at `{CDN_BASE_URL}/manifests/{appId}/__base__/__default__/manifest.json`. Then launch your app: on start or resume the plugin fetches that manifest, downloads the new bundle, and applies it. Your self-hosted pipeline is live.
### Optional: manifest signing (recommended)
Signing lets devices verify manifests were produced by your server, not just served from your CDN. Generate a key pair:
```txt
otakit generate-signing-key
```
Put the private key in the console env (`MANIFEST_SIGNING_KID`, `MANIFEST_SIGNING_KEY`) and the public key in the plugin config's `manifestKeys`. To run without signing, set `MANIFEST_SIGNING_DISABLED=true`.
### Optional: analytics (Ingest Worker + Tinybird)
Device events (downloaded, applied, rolled back) flow from the plugin to a Cloudflare Worker, which batches them into Tinybird. The dashboard reads its charts and download counts from Tinybird pipes.
```txt
# 1. Deploy the Tinybird project (datasources + pipes)
cd tinybird
tb login
tb deploy
# 2. Deploy the Worker
cd packages/ingest
npx wrangler secret put TINYBIRD_EVENTS_TOKEN # Tinybird append token
npx wrangler deploy
```
See `packages/ingest/README.md` for the Worker's queue and rate-limit bindings in `wrangler.jsonc`. Then set `TINYBIRD_API_HOST` and `TINYBIRD_READ_TOKEN` in the console env, and `ingestUrl` in the plugin config.
### Optional: everything else
Email
— set
RESEND_API_KEY
and
EMAIL_FROM
to send OTP codes and invites via Resend.
Auto-revert
— releases published with the auto-revert flag are reverted automatically when too many devices roll back within 24 hours. Requires analytics (above) plus any scheduler calling
POST /api/cron/auto-revert
every ~10 minutes. If
CRON_SECRET
is set, pass it as a Bearer header or
?secret=
query param; if unset, the endpoint is open (the sweep is idempotent and only reverts releases whose own thresholds trip).
CDN purge
— set
CF_ZONE_ID
and
CF_API_TOKEN
(Cloudflare) so releases invalidate cached manifests instantly.
Rate limiting
— set
UPSTASH_REDIS_REST_URL
and
UPSTASH_REDIS_REST_TOKEN
to rate-limit the API.
Billing
— Polar integration powers the hosted service's plans. Leave the
POLAR_*
vars unset for unlimited usage with no billing UI.
Stuck on something? Email us or open an issue on GitHub — self-hosting reports help us make this guide better.
## Changelog
Route: /changelog
New features, improvements and fixes, newest first. Plugin features need the plugin version shown.
Latest versions: @otakit/capacitor-updater 3.3.0 (2026-10-12), @otakit/cli 1.12.0 (2026-11-05), @otakit/push 0.1.1 (2026-09-27).
### Live demo of the dashboard (2026-11-06)
Try the real OtaKit dashboard on 72,000 simulated devices, with no sign-up. Bundles, releases, channels, events and the activity chart all respond as they would for your app.
- Release a bundle to a share of devices, complete the rollout and watch the new version land in the chart.
- Ship a broken bundle with auto-revert on and see it reverted once rollbacks cross the threshold.
- Open bundle changes, preview links with QR codes and the event list, all running in your browser.
Docs: Open the live demo (https://console.otakit.app/demo)
### Release health and activity chart (2026-11-05, CLI 1.12.0)
Watch every release spread and stay healthy: device activity over time on the app page, above the event list and under the same filters, with each release, rollout step and revert marked on it.
- Split the bars by action, bundle, channel, platform or runtime. The range sets the bucket size: per minute, per 30 minutes, per 3 hours or per day.
- The totals show downloads, applies, errors, rollbacks and the rollback share auto-revert watches. Click a bar to list its events, or a legend entry to filter to it.
- Activity in a bundle's ⋯ menu filters to that bundle, and its channel badge to that release. Read a release's numbers over the REST API or with the `get_release_timeseries` agent tool.
Docs: Rollouts guide (/docs/rollouts)
### Bundle changes (2026-10-22, CLI 1.11.0)
See what a bundle changes before it ships: files added, changed and removed, the size before and after, and what devices download.
- Open Changes on a bundle in the dashboard, or run `otakit diff`. Compare with the previous upload, another bundle, or what a channel runs.
- Warnings point out content that should not ship, such as `.env` files, `.git` directories, `node_modules` and source maps.
- `otakit upload` prints what changed against the bundle the channel runs, and agents compare bundles with `diff_bundles`.
```
otakit diff <bundleId> --channel production
```
Docs: CLI reference (/docs/cli)
### Release notes (2026-10-12, Plugin 3.3.0, CLI 1.10.0)
Attach short plain-text notes to a release, such as "Faster checkout. Photo upload fixed on Android." Your team sees them next to the release, and the app can show them once after the update as a "What's new" screen.
- Write notes in the release dialog, with `otakit release --notes` or `--notes-file`, over the REST API, or through the `prepare_release` and `publish_release` agent tools.
- In the app, `getUnseenReleaseNotes()` returns the running release's notes until `markReleaseNotesSeen()` is called, so each release shows once per phone.
- Notes travel in their own signed block, so the app only shows text signed for that exact release. Fix a typo later from the release's menu in the dashboard.
```
otakit release --channel production --notes "Faster checkout."
```
Requires: Plugin 3.3+ to show notes in the app. Older plugins ignore them and update as before.
Docs: Release notes guide (/docs/release-notes)
### Preview links (2026-09-30, Plugin 3.2.0, CLI 1.9.0)
Open any uploaded bundle in the installed app on one phone, without releasing it. Share a link or QR code, tap Open in the app, and the app loads that bundle while every other device stays on its release.
- Create links in the dashboard, with `otakit preview` or `otakit upload --preview`, over the REST API, or with the `create_preview` agent tool.
- Links last from 1 hour to 30 days and can be revoked at any time. Each carries a random 130-bit token.
- The app returns to its release when the tester exits, the link ends, or the preview fails to start.
```
otakit upload --preview
```
Requires: Plugin 3.2+, `previewLinks: true` and a custom URL scheme in a native build.
Docs: Preview links guide (/docs/previews)
### Percentage rollouts (2026-09-30, Plugin 3.1.0, CLI 1.8.0)
Release a bundle to a share of devices first, watch its health, then raise the share or complete it. Cancel, and every device returns to the previous release.
- Start with `otakit release --rollout 10`, then change it with `otakit rollout --percent 50`, `--complete` or `--cancel`.
- Each device draws its own stable number for every rollout. No device identifier is stored or sent.
- Available on every plan, in the dashboard, and to agents through `set_rollout_percent`.
```
otakit release --channel production --rollout 10
```
Requires: Plugin 3.1+. Older plugins stay on the previous release until the rollout completes.
Docs: Percentage rollouts guide (/docs/rollouts)
### Push notifications (2026-09-27, Push SDK 0.1.0, CLI 1.7.0)
Send push notifications to your Capacitor app on iOS and Android with your own APNs key and Firebase project. Send from the dashboard, the CLI, your backend or an AI agent.
- Turn it on per workspace under Settings → Add-ons. No extra native SDK: devices use the official `@capacitor/push-notifications` plugin.
- Target your own user IDs, topics or OTA channels, and see delivery counts for every campaign.
- Send with `otakit push send`, or call the REST API with an idempotency key so a retry never sends twice.
- Free plan: 10,000 devices and 100,000 notifications a month.
```
npm install @otakit/push
```
Docs: Push setup guide (/docs/push), Push overview (/push)
### Plugin 3.0: reliability release (2026-09-20, Plugin 3.0.0, CLI 1.6.0)
A hardening release for the native updater. The last healthy bundle is always kept, update state survives crashes, downloads retry, and every failure reports a specific reason.
- Readiness is bound to the WebView document, and `appReadyTimeout` counts foreground time only.
- Downloads retry transient errors and verify integrity before a bundle is installed.
- Device events are queued on the device and retried, with more context for failed update checks.
- `otakit upload` warns about installers and very large files in the web build. `--strict-artifacts` fails CI instead.
Upgrade notes:
- Ship it in a native build: an OTA update cannot replace the native plugin.
- Android needs a System WebView that supports `DOCUMENT_START_SCRIPT` to apply updates.
- Encrypted bundles must fit within 128 MiB and the device's available memory.
- `manifestKeys` must be an array when it is set.
- Self-hosted: deploy the updated ingest Worker and Tinybird endpoints before releasing the native SDK.
Docs: Compatibility notes (https://github.com/OtaKit/otakit/tree/main/packages/capacitor-plugin#reliability-update-compatibility)
### MCP server and Agent Skill (2026-09-01, CLI 1.5.0)
Release updates from your AI coding agent. OtaKit ships an MCP server, hosted and inside the CLI, plus an Agent Skill, so agents in Claude Code, Codex, VS Code and Copilot can upload, release, check health and revert.
- Releases take two steps: the agent prepares, you approve, then it publishes.
- Release health, recent device events and reverts are available as tools.
- `otakit connect` sets a project up for your agent, and the dashboard shows how to connect.
```
npx @otakit/cli connect
```
Docs: MCP & Agent Skills guide (/docs/agents)
### Auto-revert (2026-07-02, CLI 1.4.0)
A release can revert itself. Publish with `--auto-revert`, and OtaKit reverts the release when too many devices roll back within 24 hours.
- By default it reverts when rollbacks reach 20% of at least 50 applied or rolled-back updates.
- Tune it with `--auto-revert-rate` and `--auto-revert-min-sample`.
- It also watches percentage rollouts, and returns every device to the previous release.
```
otakit upload --release production --auto-revert
```
Docs: Health and auto-revert (/docs/rollouts)
### Delta updates (2026-07-02, Plugin 2.3.0, CLI 1.3.0)
Devices download only the files that changed. Upload with `--strategy deltas`, and OtaKit stores each file once by its content hash, so devices reuse unchanged images, fonts and chunks.
- Opt in per upload. A single zip stays the default.
- Every file is verified by its hash before the bundle is assembled.
- Keep build output deterministic so unchanged files stay unchanged.
```
otakit upload --strategy deltas --release
```
Docs: CLI reference (/docs/cli)
### End-to-end bundle encryption (2026-07-02, Plugin 2.2.0, CLI 1.2.0)
Encrypt bundles before they leave your machine. OtaKit and the CDN only ever store ciphertext, and the app decrypts with a key that ships inside your native build.
- Run `otakit generate-encryption-key`, then upload with `OTAKIT_ENCRYPTION_KEY` set. Encryption turns on automatically.
- AES-256-GCM with key IDs, so old and new keys can ship side by side during a rotation.
```
otakit generate-encryption-key
```
Requires: Plugin 2.2+ with `bundleKeys` in the plugin config.
Docs: Security model (/docs/security)
### Update listeners and in-app channel switching (2026-07-02, Plugin 2.2.0)
React to updates from JavaScript, and move a device between channels at runtime, for example to let internal users opt into a beta channel.
- Listen for `updateAvailable`, `updateStaged`, `updateApplied`, `downloadFailed` and `rollback`.
- `setChannel()` and `getChannel()` switch channels without a new native build.
```
OtaKit.addListener('updateApplied', ({ bundle }) => {});
```
Docs: Events & listeners (/docs/events), Channels (/docs/channels)
### Native compatibility check and emergency releases (2026-07-02, CLI 1.2.0, Plugin 2.2.0)
Uploads compare your native plugins with the ones the channel's current release was built with, and warn when a change needs a store build. For urgent fixes, `--force-immediate` makes devices apply a release on their next check.
- `otakit compatibility` runs the same check without uploading.
- `--fail-on-incompatible` blocks the upload in CI.
- The force flag is part of the signed manifest, so it cannot be added in transit.
```
otakit upload --release production --force-immediate
```
Docs: CLI reference (/docs/cli)
### Update policies (2026-04-23, Plugin 2.1.1, CLI 1.1.1)
Choose exactly when updates apply with `launchPolicy`, `resumePolicy` and `runtimePolicy`: off, shadow, apply-staged or immediate.
- By default, staged bundles apply on the next cold start and resumes download in the background.
- After a native update, the first launch can apply the latest bundle right away with `runtimePolicy`.
```
launchPolicy: 'apply-staged', resumePolicy: 'shadow', runtimePolicy: 'immediate'
```
Docs: Update strategies (/docs/update-strategies)
### Device events and release analytics (2026-04-13, Plugin 2.0.1)
Devices report downloads, applies and rollbacks to an edge ingest service, and the dashboard shows an events timeline with counts for every release.
- Events are validated and rate-limited at the edge before they reach analytics.
Docs: Events & listeners (/docs/events)
### Self-hosting (2026-04-13)
Run OtaKit on your own infrastructure. The dashboard needs only Postgres, S3-compatible storage and authentication; billing, email and analytics switch off when they are not configured.
Docs: Self-hosting guide (/docs/self-host)
### Signed manifests on the CDN (2026-04-12, Plugin 2.0.0)
Update checks no longer reach an app server. Every release is a signed, static manifest on Cloudflare's CDN, and bundles download from public CDN URLs, so checks stay fast at any scale.
Upgrade notes:
- Plugin 2.0 uses the new manifest protocol: update the plugin in a native build.
Docs: Security model (/docs/security)
### Runtime versions (2026-04-11, Plugin 1.2.0, CLI 1.1.0)
Set `runtimeVersion` in the plugin config to keep web bundles matched to native builds. Each native build only receives bundles uploaded for its runtime version.
```
runtimeVersion: '2026.04'
```
Docs: Channels & runtime versions (/docs/channels)
### OtaKit 1.0 (2026-04-10, Plugin 1.0.0, CLI 1.0.0)
Over-the-air updates for Capacitor apps. Upload a web build with the CLI, release it to a channel, and devices download it in the background.
- A new bundle runs on trial: if the app does not call `notifyAppReady()` in time, the device rolls back automatically.
- Open source under the MIT license.
```
npm install @otakit/capacitor-updater
```
Docs: Quick setup (/docs/setup)
## Related
Building the native iOS shell of a Capacitor app needs a Mac with Xcode. NoMac (https://nomac.app), from friends of OtaKit, gives developers and AI agents a cloud Mac for those builds, billed by the second.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

