React guide
This walkthrough uses Vite + React with Capacitor and the default hosted OtaKit flow.
Create the React app
npm create vite@latest my-app -- --template react-ts cd my-app npm install
Add Capacitor
npm install @capacitor/core @capacitor/cli npx cap init my-app com.example.myapp
Set webDir to dist in capacitor.config.ts:
import type { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
appId: "com.example.myapp",
appName: "my-app",
webDir: "dist",
};
export default config;Add native platforms
npm install @capacitor/ios @capacitor/android npx cap add ios npx cap add android
Install the OtaKit plugin
npm install @otakit/capacitor-updater npx cap sync
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:
npm install -g @otakit/cli otakit login
Configure the plugin
const config: CapacitorConfig = {
appId: "com.example.myapp",
appName: "my-app",
webDir: "dist",
plugins: {
OtaKit: {
appId: "YOUR_OTAKIT_APP_ID",
}
}
};Add notifyAppReady()
Call notifyAppReady() once your app has rendered on a native device:
// 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:
// src/App.tsx
import { AppReady } from "./AppReady";
export default function App() {
return (
<>
<AppReady />
<main>...</main>
</>
);
}Build and run on a device
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.
Ship your first OTA update
Make a visible change, rebuild, and release:
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:
// 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.