Next.js + Capacitor walkthrough
From zero to your first OTA update. This guide creates a Next.js app, adds Capacitor, installs the OtaKit plugin, and ships an update — all in about 10 minutes.
Create the Next.js app
npx create-next-app@latest my-app cd my-app
Add Capacitor
npm install @capacitor/core @capacitor/cli npx cap init my-app com.example.myapp
Edit capacitor.config.ts and set webDir to your build output directory. For Next.js static export, use "out":
// capacitor.config.ts
import type { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
appId: "com.example.myapp",
appName: "my-app",
webDir: "out",
};
export default config;Configure Next.js for static export:
// next.config.ts
const nextConfig = {
output: "export",
};
export default nextConfig;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 account and register your app
Sign up at the OtaKit dashboard, create an organization, and copy your API key from the Organization tab. Then:
npm install -g @otakit/cli export OTAKIT_SECRET_KEY=otakit_sk_... export OTAKIT_SERVER_URL=https://your-domain.com otakit register --slug com.example.myapp
Copy the returned appId and publicKey into your plugin config, then export:
export OTAKIT_APP_ID=your-app-id
Configure the plugin
Add the Updater config to capacitor.config.ts using the values returned by otakit register:
// capacitor.config.ts
const config: CapacitorConfig = {
appId: "com.example.myapp",
appName: "my-app",
webDir: "out",
plugins: {
Updater: {
appId: "YOUR_APP_ID",
publicKey: "YOUR_PUBLIC_KEY",
defaultChannel: "default",
updateMode: "silent",
autoUpdate: true,
appReadyTimeout: 10000
}
}
};Add notifyAppReady()
Create a client component that calls notifyAppReady() when the app loads. This tells the plugin the bundle is healthy. If it's not called within the timeout, the plugin rolls back.
// app/components/AppReady.tsx
"use client";
import { useEffect } from "react";
import { Updater } from "@otakit/capacitor-updater";
import { Capacitor } from "@capacitor/core";
export function AppReady() {
useEffect(() => {
if (Capacitor.isNativePlatform()) {
Updater.notifyAppReady();
}
}, []);
return null;
}Add it to your root layout:
// app/layout.tsx
import { AppReady } from "./components/AppReady";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<AppReady />
{children}
</body>
</html>
);
}Build and run on a device
npm run build npx cap sync npx cap run ios # or: npx cap run android
Verify the app launches. Check the Xcode / Android Studio console for OtaKit log messages confirming it checked for updates.
Ship your first OTA update
Make a visible change to your app — for example, update the heading in app/page.tsx. Then build and release:
# Bump version in package.json (e.g. 0.1.0 → 0.1.1) npm version patch # Build and ship npm run build otakit upload otakit release
Now relaunch the app on your device. It will detect the new version, download it, and apply it on the next restart.
That's it — your first OTA update is live. No app store submission, no review wait.
What's next?
- Use channel
defaultfor the simple flow, then addstaging/productionwhen needed. - Set up manifest signing for end-to-end integrity.
- Set up CI automation with GitHub Actions.
- See the CLI reference for rollback, release history, and more.