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.

1

Create the Next.js app

npx create-next-app@latest my-app
cd my-app
2

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;
3

Add native platforms

npm install @capacitor/ios @capacitor/android
npx cap add ios
npx cap add android
4

Install the OtaKit plugin

npm install @otakit/capacitor-updater
npx cap sync
5

Create an OtaKit account and app

Sign up at the OtaKit dashboard, create an organization and app, then install the CLI and sign in:

npm install -g @otakit/cli

otakit login

Copy the OtaKit appId from the dashboard. The CLI will read your bundle output directory from capacitor.config.ts, so no separate OtaKit project file is required.

6

Configure the plugin

Add the OtaKit config to capacitor.config.ts using the appId from the dashboard:

// capacitor.config.ts
const config: CapacitorConfig = {
  appId: "com.example.myapp",
  appName: "my-app",
  webDir: "out",
  plugins: {
    OtaKit: {
      appId: "YOUR_OTAKIT_APP_ID",
      appReadyTimeout: 10000,
      // Optional:
      // updateMode: "manual",
      // updateMode: "immediate",
    }
  }
};

Optional advanced path: otakit register --slug com.example.myapp can create the app from the CLI, but the normal hosted flow starts in the dashboard.

7

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 { OtaKit } from "@otakit/capacitor-updater";
import { Capacitor } from "@capacitor/core";

export function AppReady() {
  useEffect(() => {
    if (Capacitor.isNativePlatform()) {
      OtaKit.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>
  );
}
8

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.

9

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:

# Build and ship
npm run build
otakit upload --release

Now relaunch the app on your device. It will detect the new version, download it, and apply it on the next cold launch by default.

That's it — your first OTA update is live. No app store submission, no review wait.

What's next?