# OtaKit Docs Generated from the public docs pages in `packages/web/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 - 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 a bundle, and the plugin delivers that bundle to devices without waiting for a store review. 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. ### Features - **One-command shipping**: Build your web app, then release with otakit upload --release. - **Channels & staged rollouts**: Ship on the base channel by default, then add named channels when you need separate rollout tracks. - **Automatic update delivery**: The normal flow downloads updates in the background and activates them on the next cold launch. - **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**: Stay on the base channel first, then add named channels only when needed. - **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", appReadyTimeout: 10000, } } }; export default config; ``` ### 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, devices download it in the background and activate 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. ### Quick start 1. Create an app in the OtaKit dashboard and copy its `appId`. 2. Add that `appId` to `plugins.OtaKit` in `capacitor.config.ts`. 3. Log in: `otakit login` 4. Build your web app. 5. Ship it: `otakit upload --release` ### 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" } } }; 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_SECRET_KEY=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` -> `OTAKIT_ACCESS_TOKEN` -> stored login token -> `OTAKIT_SECRET_KEY` - Upload path: CLI path argument -> `OTAKIT_BUILD_DIR` -> `capacitor.config.* webDir` - Release channel: `--release` -> base channel, `--release ` -> named channel - 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. - `--min-native-build `: Minimum native build required for this update. - `--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. If bundleId is omitted, releases the latest bundle. 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, manual advanced flows, and debug methods. Import from `@otakit/capacitor-updater`. For normal app code, the normal hosted 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: // updateMode: "next-resume", } } ``` - `appId` (string): OtaKit app ID for manifest and stats access. - `channel` (string): Named release track to check. Omit it to use the base channel. - `updateMode` ('manual' | 'next-launch' | 'next-resume' | 'immediate'): Overall update behavior. Optional, defaults to next-launch. - `checkInterval` (number): Milliseconds between automatic update checks. Optional, defaults to 600000 (10 min). - `appReadyTimeout` (number): Milliseconds to wait for notifyAppReady(). Optional, defaults to 10000. Hosted OtaKit points at the managed server automatically. Do not set `serverUrl` or `manifestKeys` unless you intentionally want custom server or verification behavior. ### Update Modes All automatic modes check for updates on cold start (always) and app resume (throttled by checkInterval, default 10 minutes). - `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` (dev/debug): 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 server. You only need `manifestKeys` when you intentionally override trust for a custom or self-hosted server. In that case, also set `serverUrl` to your own API 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 on the next cold app launch. 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 app-facing updater state: current 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. 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(); ``` ### Debug API Manual inspection and control methods live under `OtaKit.debug`. These are for diagnostics, support, and test flows, not normal app code. - `debug.check(options?)` -> `LatestVersion | null`: Check the server for a newer bundle without downloading it. You can optionally pass { channel } for a one-off debug override. - `debug.download(options?)` -> `BundleInfo | null`: Debug version of download() that ensures the latest bundle is staged for a one-off { channel } override. - `debug.reset()` -> `void`: Clear active updater state, return to the builtin bundle, clear fallback and last failure state, and reload the WebView. Terminal operation. - `debug.listBundles()` -> `{ bundles: BundleInfo[] }`: List downloaded OTA bundles stored on the device. - `debug.deleteBundle({ bundleId })` -> `void`: Delete a downloaded bundle that is not current, fallback, or staged. - `debug.getLastFailure()` -> `BundleInfo | null`: Return the last failed update metadata for diagnostics. The failed bundle files themselves are cleaned up automatically after rollback. ### 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 // serverUrl: "https://your-server.com/api/v1", // allowInsecureUrls: false, // manifestKeys: [ // { kid: "key-2026-01", key: "MFkwEwYH..." } // ] } } ``` - `serverUrl` (string): Custom OtaKit server URL. Leave unset for the hosted service default. - `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; status: "builtin" | "pending" | "trial" | "success" | "error"; downloadedAt?: string; sha256?: string; channel?: string; releaseId?: string; } interface OtaKitState { current: BundleInfo; staged: BundleInfo | null; builtinVersion: string; } interface LatestVersion { version: string; url: string; sha256: string; size: number; downloaded?: boolean; releaseId?: string; minNativeBuild?: number; } ``` ## 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_... # or OTAKIT_ACCESS_TOKEN ``` ### 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", "minNativeBuild": 100 // optional } ``` 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, "minNativeBuild": null, "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. One bundle = one release event. - Auth: Bearer Request body ```txt { "bundleId": "uuid", "channel": "staging" // optional; omit or null for base channel } ``` Response ```txt { "release": { "id": "uuid", "channel": null, "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, 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 ``` ### 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 ``` ### 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 the base channel by default, then add named channels only when you need separate rollout tracks. Most apps should start with the base channel only. Channels are optional named release tracks such as `staging` or `production`. ### Base channel first If you omit `channel` from the plugin config, the app uses the base channel. ```txt plugins: { OtaKit: { appId: "YOUR_OTAKIT_APP_ID" } } ``` Release to the base channel with: ```txt otakit upload --release ``` ### Named channels Add a channel only when you want a separate rollout track for a specific build, such as internal QA or staged production rollout. ```txt plugins: { OtaKit: { appId: "YOUR_OTAKIT_APP_ID", channel: "staging" } } ``` Release to that channel with: ```txt otakit upload --release staging ``` ### Promote an existing bundle You can upload once, test it, then promote the same bundle to another channel later. ```txt # Upload and release to staging otakit upload --release staging # Promote that bundle to production later otakit release --channel production ``` ### Common setups - One stream only: no channel in the app config, release everything to the base channel. - Staging and production: internal builds the base channel or the `channel: "staging"`, production builds use `channel: "production"`. ### Rules - Channel is a build-time setting in `capacitor.config.ts`. - Omit `channel` to use the base channel. - Add named channels only when you really need separate release tracks. ## 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_SECRET_KEY=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_SECRET_KEY: ${{ secrets.OTAKIT_SECRET_KEY }} 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 . ## Self-hosting Route: /docs/self-host Run OtaKit on your own infrastructure. OtaKit is fully open source and can run on your own infrastructure. You'll need PostgreSQL and any S3-compatible object storage (AWS S3, Cloudflare R2, MinIO). If you want the managed OtaKit service, use the standard hosted setup guide instead. ### Requirements - Node.js 23+ - PostgreSQL 14+ - S3-compatible storage (R2, MinIO, AWS S3) - Upstash Redis (recommended for manifest cache) ### Environment variables ```txt # Database DATABASE_URL=postgresql://user:pass@localhost:5432/otakit # Auth BETTER_AUTH_SECRET=your-random-secret # openssl rand -hex 32 BETTER_AUTH_URL=https://your-domain.com # S3-compatible storage R2_BUCKET=otakit-bundles R2_ACCESS_KEY=... R2_SECRET_KEY=... R2_ENDPOINT=https://....r2.cloudflarestorage.com # Optional but recommended: Upstash Redis for manifest cache UPSTASH_REDIS_REST_URL=https://... UPSTASH_REDIS_REST_TOKEN=... # Optional: upload size limit (bytes, default 200MB) MAX_BUNDLE_SIZE=209715200 # Manifest signing is enabled by default # Generate with: otakit generate-signing-key MANIFEST_SIGNING_KID=key-2026-01 MANIFEST_SIGNING_KEY=-----BEGIN EC PRIVATE KEY-----... # Only set this if you intentionally want unsigned manifests # MANIFEST_SIGNING_DISABLED=true # Optional: global admin key for organization management ADMIN_SECRET_KEY=your-admin-key ``` ### Deploy ```txt git clone https://github.com/nicepkg/otakit cd otakit # Install dependencies pnpm install # Run database migrations cd packages/web npx prisma migrate deploy # Build and start pnpm build pnpm start ``` The server runs on port 3000 by default. Point your reverse proxy (nginx, Caddy) to it and ensure HTTPS is configured. ### Redis cache (recommended) Self-hosted OtaKit can use Upstash Redis to cache the hot manifest lookup path. This reduces repeated Postgres reads when devices check for updates. The cache is optional, but recommended for production. OtaKit still works without it and falls back to direct database reads. ```txt UPSTASH_REDIS_REST_URL=https://... UPSTASH_REDIS_REST_TOKEN=... ``` ### Docker ```txt docker run -d \ -p 3000:3000 \ -e DATABASE_URL=postgresql://... \ -e BETTER_AUTH_SECRET=... \ -e BETTER_AUTH_URL=https://your-domain.com \ -e R2_BUCKET=otakit-bundles \ -e R2_ACCESS_KEY=... \ -e R2_SECRET_KEY=... \ -e R2_ENDPOINT=https://... \ ghcr.io/nicepkg/otakit:latest ``` ### Initial setup 1. Open your domain in a browser and sign in — the first user account is created automatically via email OTP. 2. Create an organization from the Account tab. 3. Generate an API key from the Organization tab — you'll need this for the CLI. 4. Point the CLI to your server: ```txt export OTAKIT_SERVER_URL=https://your-domain.com/api/v1 export OTAKIT_SECRET_KEY=otakit_sk_... ``` ### Manifest signing Manifest signing is enabled by default. For a normal self-hosted setup, generate an ES256 key pair and set these on the server: ```txt otakit generate-signing-key ``` This outputs the server environment variables (`MANIFEST_SIGNING_KID`, `MANIFEST_SIGNING_KEY`) and the plugin config (`manifestKeys`). Add them to your server and Capacitor config respectively. ```txt plugins: { OtaKit: { serverUrl: "https://your-domain.com/api/v1", appId: "YOUR_OTAKIT_APP_ID", manifestKeys: [ { kid: "key-2026-01", key: "MFkwEwYH..." } ] } } ``` Keep the private signing key on the server only. The plugin should only receive the public verification keys in `manifestKeys`. If you intentionally want unsigned manifests on a custom server, set `MANIFEST_SIGNING_DISABLED=true`. When signing is not disabled, missing signing env vars are treated as a server misconfiguration and manifest requests will fail. ### Connecting CLI and plugin Point the CLI to your server with `OTAKIT_SERVER_URL`: ```txt export OTAKIT_SERVER_URL=https://your-domain.com/api/v1 export OTAKIT_SECRET_KEY=otakit_sk_... ``` In your Capacitor plugin config, set `serverUrl` to your server: ```txt plugins: { OtaKit: { serverUrl: "https://your-domain.com/api/v1", appId: "YOUR_OTAKIT_APP_ID", // manifestKeys: [{ kid, key }] } } ``` The `serverUrl` is only needed for self-hosting — it defaults to `https://otakit.app/api/v1` when omitted. Follow the standard setup guide for the rest of the plugin and CLI configuration.