# OtaKit 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 - CI Automation: /docs/ci - Security: /docs/security - Self-hosting: /docs/self-host ## 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 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 by default. ### Features - **One-command shipping**: Build your web app, then release with otakit upload --release. - **Channels & runtime lanes**: Use channels for rollout tracks and runtimeVersion for native compatibility boundaries. - **Automatic update delivery**: The normal flow checks and downloads automatically, then activates based on updateMode. - **Manual update control**: Switch to manual mode 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. - **SHA-256 verification**: Downloaded bundles are verified before activation so corrupted or tampered files are rejected. - **Organization access & API keys**: Manage apps, members, and scoped keys inside an organization. - **Self-hosting**: Run OtaKit on your own infrastructure when you need full control over delivery and trust. ### Getting started - **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. - **CI Automation**: Build and ship bundles from GitHub Actions. - **CLI Reference**: Commands, options, and release workflows. - **Plugin API**: Default automatic flow, manual flow, events, and configuration. Need help with setup, billing, or rollout issues? Email SUPPORT_EMAIL or use the contact page . ## 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. Create your app in the dashboard Sign in to the OtaKit dashboard , create an app, and copy its OtaKit `appId`. ### 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; ``` Note: Your app must be published to the app store at least once with the OtaKit plugin configured before it can receive updates! ### 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 the React guide if you want a full walkthrough. Use the Plugin API and CLI reference for advanced flows and exact 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. ```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 --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 - Upload path: CLI path argument -> `OTAKIT_BUILD_DIR` -> `capacitor.config.* webDir` - Release channel: `--release` -> base channel, `--release ` -> 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. Options - `[path]`: Bundle directory. If omitted, the CLI uses OTAKIT_BUILD_DIR or capacitor.config.* webDir. - `--app-id `: App ID override. - `--server `: Server URL override. - `--version `: Version string. Otherwise OTAKIT_VERSION, then auto-generated. - `--strict-version`: Require explicit or env-provided version. - `--release [channel]`: Release after upload. Omit channel to release to the base channel. 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 `: Target named channel. Omit it to use the base channel. Example ```txt otakit release --channel production ``` ### otakit list List uploaded bundles. Options - `--limit `: Max results. Defaults to 20. Example ```txt otakit list --limit 20 ``` ### otakit releases Show release history across all streams or a specific target. Options - `--channel `: Show only a named channel. - `--base`: Show only the base channel. - `--limit `: Max results. Defaults to 10. Example ```txt otakit releases --base ``` ### otakit delete 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 `: App slug (for example com.example.app). - `--server `: Server URL override. - `--token `: Access token or organization API key. - `--secret-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 address. If omitted, prompts interactively. - `--server `: 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 `: Server URL override. Example ```txt otakit whoami ``` ### otakit logout Remove stored token for a server. Options - `--server `: Server URL override. Example ```txt otakit logout ``` ### otakit config resolve Show effective CLI values and where they came from. Options - `--app-id `: App ID override. - `--server `: Server URL override. - `--output-dir `: Output directory override. - `--channel `: Channel override. - `--json`: Print machine-readable JSON output. Example ```txt otakit config resolve --json ``` ### 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 ``` ### 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 `. ## Plugin API Route: /docs/plugin Capacitor plugin setup, default automatic updates, and manual advanced flows. Import from `@otakit/capacitor-updater`. The normal 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 For hosted OtaKit, keep the plugin config small: ```txt plugins: { OtaKit: { appId: "YOUR_OTAKIT_APP_ID", appReadyTimeout: 10000, // Optional: // channel: "production", // runtimeVersion: "2026.04", // updateMode: "next-resume", } } ``` - `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. - `updateMode` ('manual' | 'next-launch' | 'next-resume' | 'immediate'): Overall update behavior. Optional, defaults to next-launch. - `checkInterval` (number): Milliseconds between automatic checks in next-launch and next-resume. Manual APIs and immediate mode ignore it. Optional, defaults to 600000 (10 min). - `appReadyTimeout` (number): Milliseconds to wait for notifyAppReady(). Optional, defaults to 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. 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. ### 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 simple: release the bundle and it naturally stays inside its own runtime lane. ### Update Modes `next-launch` and `next-resume` check on cold start and app resume, throttled by `checkInterval`. `immediate` ignores the interval, and manual APIs always perform a live check. - `next-launch` (default): Check and download in the background. Activate the staged bundle only on the next cold start. Zero disruption during a session. - `next-resume` (recommended): Check and download in the background. Activate the staged bundle on the next resume or cold start. Brief reload when returning to the app. - `immediate` (development): Check, download, and activate as soon as possible on both cold start and resume. Primarily for development and testing. - `manual` (optional): No automatic checks. Your app drives everything via check(), download(), apply(), and update(). ### Manifest Verification Hosted OtaKit verifies manifests automatically. The native plugin ships with built-in trusted public keys for the managed service and uses them by default when you stay on the hosted CDN. You only need `manifestKeys` when you intentionally override trust for a custom or self-hosted setup. In that case, set `cdnUrl` to your manifest CDN and `ingestUrl` to your own event ingest base URL. ### Automatic Flow (Default) This is the normal OtaKit flow. Leave `updateMode` unset or set it to `next-launch`. The plugin checks automatically on startup, downloads in the background, and activates the new bundle according to the selected update mode. In this mode, your app code usually only needs to call `notifyAppReady()`. - `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) Use this only when your app wants to control the update UX itself, for example by showing an “Update available” prompt or delaying install until the user confirms. Set `updateMode` to `"manual"` first. ```txt plugins: { OtaKit: { appId: "YOUR_OTAKIT_APP_ID", updateMode: "manual", appReadyTimeout: 10000, } } ``` - `getState()` -> `OtaKitState`: Inspect the current updater state: current bundle, fallback bundle, staged bundle, and builtin version. - `check()` -> `LatestVersion | null`: Check the configured channel for a newer bundle without downloading it. When downloaded=true, that exact update is already staged locally. - `download()` -> `BundleInfo | null`: Ensure the latest bundle is staged for later activation. If it is already staged, the staged bundle is returned without re-downloading it. - `update()` -> `void`: Recommended one-shot manual helper. Bring the app to the newest available update now. If the newest update is already staged, apply it. Otherwise download it and apply it. Terminal operation. - `apply()` -> `void`: Activate the currently staged bundle and reload the WebView. Terminal operation. - `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. The simplest manual pattern is: check for updates, show your own prompt, then call `update()` if the user accepts. If `check()` returns `downloaded: true`, the latest update is already staged locally and you can call `apply()` directly. ```txt const latest = await OtaKit.check(); if (!latest) { 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) { return; } const accepted = window.confirm("Update available. Download now?"); if (!accepted) { return; } await OtaKit.download(); // 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 plugin event writes. - `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. ### Events Listen to update lifecycle events with `OtaKit.addListener(event, callback)`. Returns a handle that can be removed with `.remove()`. - `downloadStarted` ({ version }): A download has begun - `downloadComplete` (BundleInfo): Download finished and bundle staged - `downloadFailed` ({ version, error }): Download failed - `updateAvailable` (LatestVersion): A newer bundle is available. downloaded=true means it is already staged locally. - `noUpdateAvailable`: App is up to date - `appReady` (BundleInfo): A newly activated OTA bundle was confirmed healthy by notifyAppReady(). - `rollback` ({ from, to, reason }): The running bundle rolled back to fallback or builtin ```txt OtaKit.addListener("downloadComplete", (bundle) => { console.log(`Update staged: ${bundle.version}`); }); await OtaKit.removeAllListeners(); ``` ### Types ```txt interface BundleInfo { id: string; version: string; runtimeVersion?: string; status: "builtin" | "pending" | "trial" | "success" | "error"; downloadedAt?: string; sha256?: string; channel?: string; releaseId?: string; } interface OtaKitState { current: BundleInfo; fallback: BundleInfo; staged: BundleInfo | null; builtinVersion: string; } interface LatestVersion { version: string; url: string; sha256: string; size: number; runtimeVersion?: string; downloaded?: boolean; releaseId?: string; } ``` ## 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" } ``` ### 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). - Auth: Bearer Request body ```txt { "bundleId": "uuid", "channel": "staging" // optional; omit or null for base channel } ``` Response ```txt { "release": { "id": "uuid", "channel": null, "runtimeVersion": "2026.04", "bundleId": "uuid", "bundleVersion": "1.0.1", "promotedAt": "ISO timestamp" }, "previousRelease": { ... } | null } ``` ### 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 } ``` ## 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", appReadyTimeout: 10000, } } }; ``` ### 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 ( {children} ); } ``` ### 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 - Add CI automation . - Keep the base channel first, then add channels only when you need them. - Use the Plugin API and CLI reference for advanced flows. ## 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", appReadyTimeout: 10000, } } }; ``` ### 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 ( <>
...
); } ``` ### 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. ### Next - Add CI automation . - Keep the base channel first, then add channels only when needed. - Use the Plugin API and CLI reference for advanced flows. ## 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 ``` #### 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 --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. - New store baseline: — bump `runtimeVersion` so the new native build starts a fresh OTA lane. ## 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. Related docs: CLI reference and channels . ## 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. ### 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 scoped API keys or session tokens. API keys are hashed before storage — the raw key is shown once at creation and never stored. Organization-level role-based access controls restrict who can upload bundles, create releases, or manage team members. ### 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 OtaKit on your own infrastructure. OtaKit is fully open source and can run on your own infrastructure. The managed service at otakit.app runs everything for you — self-hosting is the advanced path. ### What you deploy - Public site (`packages/site`) — landing page, docs, contact, and legal pages. - Console (`packages/console`) — Next.js control plane: auth, dashboard UI, API routes, billing, and Prisma migrations. - Ingest Worker (`packages/ingest`) — Cloudflare Worker that receives device events and writes them to Tinybird. Required if you want to use dashboard analytics. - CDN bucket — public R2 or S3 bucket with a CDN domain. Serves manifest files and bundle zips directly to devices. The CLI (`packages/cli`) and Capacitor plugin (`packages/capacitor-plugin`) are client-side tools — they can be configured to point at your self-hosted services. ### Required services - PostgreSQL 14+ - S3-compatible object storage (Cloudflare R2, AWS S3) - A public CDN domain in front of the storage bucket - At least one provider (Google, Apple, Github, or Email OTP via Resend) for sign-in ### Environment variables #### Dashboard — required ```txt DATABASE_URL=postgresql://user:pass@localhost:5432/otakit BETTER_AUTH_SECRET=your-random-secret # openssl rand -hex 32 BETTER_AUTH_URL=https://your-domain.com # At least one provider for sign-in GOOGLE_CLIENT_ID=... GOOGLE_CLIENT_SECRET=.... RESEND_API_KEY=... EMAIL_FROM=... R2_BUCKET=... R2_ACCESS_KEY=... R2_SECRET_KEY=... R2_ENDPOINT=https://....r2.cloudflarestorage.com CDN_BASE_URL=https://cdn.your-domain.com # Cloudflare CDN purge — instant cache invalidation after releases. # Without this, stale manifests may be served until CDN TTL expires. CF_ZONE_ID=... CF_API_TOKEN=... # Tinybird — device event analytics and dashboard counts. # Without this, the dashboard shows empty analytics and download counts return 0. TINYBIRD_API_HOST=https://api.tinybird.co TINYBIRD_READ_TOKEN=... # Manifest signing — ES256 signatures on manifest JSON. # Generate with: otakit generate-signing-key MANIFEST_SIGNING_KID=key-2026-01 MANIFEST_SIGNING_KEY=-----BEGIN EC PRIVATE KEY-----... # Set MANIFEST_SIGNING_DISABLED=true to skip signing entirely. ``` #### Ingest Worker Only needed if you want to use analytics. See `packages/ingest/wrangler.jsonc` and `packages/ingest/.env.example` for the full config. The Worker needs a Tinybird append token and a Cloudflare Queue. ### Deploy #### Public site ```txt git clone https://github.com/OtaKit/otakit cd otakit pnpm install cd packages/site pnpm build pnpm start ``` #### Console ```txt git clone https://github.com/OtaKit/otakit cd otakit pnpm install cd packages/console npx prisma migrate deploy pnpm build pnpm start ``` Runs on port 3000. Put a reverse proxy (nginx, Caddy) in front with HTTPS. #### Ingest Worker ```txt cd packages/ingest npx wrangler deploy ``` #### Tinybird project ```txt cd tinybird tb login tb deploy ``` ### Manifest signing ```txt otakit generate-signing-key ``` Add the private key to the dashboard env (`MANIFEST_SIGNING_KID`, `MANIFEST_SIGNING_KEY`). Add the public key to the Capacitor plugin config ( `manifestKeys`). ### Configure the plugin ```txt plugins: { OtaKit: { appId: "YOUR_OTAKIT_APP_ID", cdnUrl: "https://cdn.your-domain.com", ingestUrl: "https://ingest.your-domain.com/v1", // omit if not using Tinybird manifestKeys: [ { kid: "key-2026-01", key: "MFkwEwYH..." } ] } } ``` ### Configure the CLI ```txt export OTAKIT_SERVER_URL=https://your-domain.com/api/v1 export OTAKIT_TOKEN=otakit_sk_... ``` Or set `serverUrl` in the Capacitor plugin config so the CLI can read it automatically.