From PWA to native app: ship your PWA to the stores with Capacitor
Your PWA already runs everywhere except the App Store. Capacitor wraps it as a native iOS and Android app you can submit, and OtaKit keeps it updated over the air like a PWA.
A PWA already runs everywhere — except where a lot of your users look for apps: the App Store and Google Play. Capacitor closes that gap. It wraps your PWA as a real native app you can submit to both stores, gives it access to device APIs a browser can't reach, and keeps your single web codebase intact.
This guide takes an existing PWA to installable native builds and adds OtaKit so the app updates over the air — the same “always current” feel a PWA has on the web.
Mental model: your PWA stays exactly what it is on the web. Capacitor adds a native wrapper for the stores; OtaKit handles updates inside that wrapper.
1. Point Capacitor at your build output
A PWA is just a web app with a manifest and a service worker, so there's nothing special to change — identify the folder your build produces (dist, build, or out) and make sure assets use relative paths.
npm install @capacitor/core npm install -D @capacitor/cli npx cap init "My App" com.example.myapp --web-dir dist npm install @capacitor/ios @capacitor/android
2. Rethink the service worker
This is the one PWA-specific gotcha. On the web, your service worker caches assets and handles offline. Inside a Capacitor app, the web layer already loads from local files, and a service worker's caching can fight with native updates — you can end up serving a stale cache after an OTA update lands.
The clean approach: let Capacitor serve the bundle from disk, and let OtaKit handle updates instead of the service worker's cache. Either disable the service worker on native, or scope its caching so it doesn't shadow the bundle. Detect the platform to branch:
import { Capacitor } from "@capacitor/core";
if (!Capacitor.isNativePlatform() && "serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js"); // web only
}3. Build the native apps
npm run build npx cap add ios npx cap add android npx cap sync
Run with npx cap open ios / npx cap open android. Your PWA now runs as a native app, with the same UI it always had.
4. Add live updates with OtaKit
This is what restores the PWA's best trait — being always up to date — in the native wrapper:
npm install @otakit/capacitor-updater npx cap sync
// capacitor.config.ts
const config: CapacitorConfig = {
appId: "com.example.myapp",
appName: "My App",
webDir: "dist",
plugins: {
OtaKit: { appId: "YOUR_OTAKIT_APP_ID" },
},
};import { Capacitor } from "@capacitor/core";
import { OtaKit } from "@otakit/capacitor-updater";
if (Capacitor.isNativePlatform()) OtaKit.notifyAppReady();npm install -g @otakit/cli otakit login npm run build otakit upload --release
Now a change you deploy to your PWA on the web can ship to the installed native app the same day with otakit upload --release — no store review for web-layer changes. Native changes (new plugins, permissions) still go through the store.
Best of both worlds: the reach and updateability of a PWA, plus a store presence and native device access. See why OTA is compliant.
Where to go next
The setup guide covers the full flow. Then read how OTA works and background vs foreground update UX.