Tutorial11 min read

Convert your Next.js app to iOS and Android with Capacitor

Turn an existing Next.js app into real iOS and Android apps with Capacitor — static export config, native project setup, plugins, safe areas — then ship JS/CSS changes instantly with OtaKit live updates.

You already have a Next.js app. Capacitor lets you ship it to the App Store and Google Play as a real native app — the same React code, the same components, wrapped in a native shell with access to the camera, filesystem, push notifications, and every other device API. You don't rewrite anything in Swift or Kotlin, and you don't learn React Native.

This guide takes an existing Next.js project to installable iOS and Android builds, then wires up OtaKit so you can push JavaScript, CSS, and content changes straight to installed devices over the air — without waiting on a store review every time.

Mental model: Capacitor turns your web build into a native app. OtaKit keeps that web layer up to date after it ships, in minutes instead of days.

Prerequisites

  • An existing Next.js 14/15 app (App Router or Pages Router).
  • Node 20+, plus Xcode (for iOS) and Android Studio (for Android).
  • A Next.js app that can render without a Node server at runtime — see the next section.

1. Configure Next.js for static export

Capacitor bundles static web assets into the native app, so it needs an exported build rather than a running Node server. Next.js produces exactly that with output: 'export'. Add it to your config:

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "export",
  images: { unoptimized: true }, // the Image Optimization API needs a server
};

export default nextConfig;

Running next build now emits a fully static site into out/. The tradeoff is that server-only features — Route Handlers, Server Actions, ISR, middleware — don't run inside the native shell. In practice you move those to an API you call over HTTPS, which is exactly how a mobile app should talk to your backend anyway.

2. Add Capacitor to the project

npm install @capacitor/core
npm install -D @capacitor/cli

# initialize — appId is your reverse-domain bundle id
npx cap init "My App" com.example.myapp --web-dir out

npm install @capacitor/ios @capacitor/android

The --web-dir out flag points Capacitor at the folder Next.js exports to. Your capacitor.config.ts should read:

// capacitor.config.ts
import type { CapacitorConfig } from "@capacitor/cli";

const config: CapacitorConfig = {
  appId: "com.example.myapp",
  appName: "My App",
  webDir: "out",
};

export default config;

3. Generate the native projects

npm run build          # produces out/
npx cap add ios
npx cap add android
npx cap sync           # copies the web build into both native projects

You now have real ios/ and android/ folders. Open them with npx cap open ios or npx cap open android and run on a simulator or device straight from Xcode / Android Studio. A useful habit is one script that does the whole loop:

// package.json
"scripts": {
  "mobile": "next build && npx cap sync"
}

4. Handle iOS safe areas

The one layout gotcha worth fixing early: content sliding under the notch and home indicator. Opt into the safe-area insets from a single root wrapper rather than sprinkling them everywhere:

:root {
  --safe-top: env(safe-area-inset-top);
  --safe-bottom: env(safe-area-inset-bottom);
}
body {
  padding-top: var(--safe-top);
  padding-bottom: var(--safe-bottom);
}

Add viewport-fit=cover to your viewport meta so the insets report real values, and you're done.

5. Add live updates with OtaKit

Here's the payoff. Because everything your users see is that exported web layer, you can update it over the air. Install the plugin and point it at your OtaKit app:

npm install @otakit/capacitor-updater
npx cap sync
// capacitor.config.ts
const config: CapacitorConfig = {
  appId: "com.example.myapp",
  appName: "My App",
  webDir: "out",
  plugins: {
    OtaKit: { appId: "YOUR_OTAKIT_APP_ID" },
  },
};

Confirm each new bundle actually booted so OtaKit can roll back a broken release instead of stranding users. Call notifyAppReady() once the app has mounted — in Next.js, from a client component:

"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;
}

Then ship an update from your machine or CI:

npm install -g @otakit/cli
otakit login

npm run build
otakit upload --release

Publish your app to the stores once with the plugin configured. After that, every otakit upload --release reaches installed devices on their next launch — new UI, copy fixes, and bug fixes land the same day you write them. Native changes (new plugins, permissions, a Capacitor upgrade) still go through the store; everything in your out/ folder does not. That line is exactly what keeps OTA compliant with Apple and Google.

OtaKit doesn't meter monthly active users or bandwidth — bundles ship straight from a CDN edge, and most apps pay $0–25/mo. See how it compares in the 2026 tool comparison.

Where to go next

The Next.js guide covers the full setup, and automating releases with GitHub Actions turns every merge to main into a live update. If you're weighing Capacitor against a full rewrite, the React Native vs Capacitor comparison is a good next read.

Related docs