Shipping installable PWAs with Next.js 16: the 2026 guide
What it takes to ship a real, installable PWA on Next.js 16 today. Manifest, icons, service worker, push notifications, and the iOS quirks that trip teams up. With Quicktalog as the worked example.

A PWA is a website that can install itself onto your home screen. On iOS and Android, an installed PWA launches in its own window, works offline if you built it to, and can receive push notifications. Done right, users will not know it is not a native app. This guide covers what you need to ship a real, installable PWA in Next.js 16 today.
Why a PWA instead of React Native
Three reasons we still reach for PWAs in 2026:
- Distribution is a URL. No App Store review. No TestFlight. Ship, share a link, users install.
- One codebase, three platforms. Your existing Next.js app is most of the way there.
- Updates are instant. Push a deploy, every user is on the new version on their next open.
The tradeoff is platform integration. iOS still does not support every Web Push feature. Some native APIs, like rich widgets or complex background processing, are limited or missing in browsers. For content, tools, and most business apps, a PWA is plenty. For a game or a deeply integrated system app, it is not.
What makes an app installable
A web app becomes installable when it has:
- A web manifest with the right fields.
- A service worker registered on the page.
- HTTPS, or localhost for development.
- Proper icons at the sizes iOS and Android expect.
Miss any of these and the install prompt will not appear.
The manifest, the Next.js way
In App Router, the manifest is a file at app/manifest.ts. No JSON file, no link tag in the head. Next.js picks it up automatically and serves it at /manifest.webmanifest.
import { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "Reactify Solutions",
short_name: "Reactify",
description: "Web, mobile, AI, and analytics for modern businesses.",
start_url: "/",
display: "standalone",
background_color: "#000000",
theme_color: "#1b998b",
icons: [
{
src: "/icon-192x192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "/icon-512x512.png",
sizes: "512x512",
type: "image/png",
},
{
src: "/icon-maskable-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
};
}Three fields do the heavy lifting:
display: "standalone"is what makes the app open without browser chrome.start_url: "/"controls what loads on launch. Match the entry screen of your app.iconsmust include a 192x192 and 512x512 at minimum, and themaskablepurpose for Android adaptive icons.
Icons, where iOS makes you work
Android is forgiving. iOS is not. For a clean install experience on iPhone:
- 180x180
apple-touch-iconinapp/apple-icon.png. Next.js picks it up automatically. - A splash screen for each iPhone resolution, linked via
apple-touch-startup-imagemeta tags. - A transparent-background icon will show a black background on iOS. Use a solid fill.
Yes, it is more work than Android. No, you cannot skip it without your app looking broken on iPhone.
Service workers without losing a week
For most Next.js apps, the right answer is Serwist or next-pwa plugins. Both wrap Workbox, which handles the annoying parts of service worker lifecycle, cache management, and update flow.
Pick a caching strategy per route type:
- Static assets (JS, CSS, images): cache-first with a long expiry. They are hashed, so cache misses are not a concern.
- HTML pages: stale-while-revalidate. Users get an instant response and the next visit is fresh.
- API calls: network-first with a short timeout, falling back to cache if offline.
Resist the urge to cache everything. Users hate stale data more than they hate loading spinners.
Push notifications
Web Push works on Chrome, Firefox, and since iOS 16.4 on Safari for installed PWAs. The flow has four steps.
- Ask permission with
Notification.requestPermission(). - Subscribe to the push service with your VAPID public key.
- Save the subscription server-side.
- Send push payloads via the
web-pushlibrary from Node.
// client: subscribe
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
});
await fetch("/api/push/subscribe", {
method: "POST",
body: JSON.stringify(subscription),
});
}
// server: send
import webPush from "web-push";
webPush.setVapidDetails(
"mailto:ops@reactify-solutions.com",
process.env.VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!,
);
export async function sendPush(subscription: webPush.PushSubscription, body: string) {
await webPush.sendNotification(subscription, body);
}iOS quirks to remember:
- Only installed PWAs can receive push. A browser tab cannot.
- The user must interact with the page before the permission prompt can fire.
- Silent notifications are not supported.
Install prompts
On Android, you can show a custom install button by capturing the beforeinstallprompt event:
"use client";
import { useEffect, useState } from "react";
export function InstallButton() {
const [evt, setEvt] = useState<any>(null);
useEffect(() => {
const handler = (e: Event) => {
e.preventDefault();
setEvt(e);
};
window.addEventListener("beforeinstallprompt", handler);
return () => window.removeEventListener("beforeinstallprompt", handler);
}, []);
if (!evt) return null;
return (
<button onClick={() => evt.prompt()}>Install this app</button>
);
}On iOS there is no programmatic prompt. Users install via Safari's Share menu, then "Add to Home Screen." Your job is to tell them that, preferably only when you are sure they are on iOS and not already installed.
Offline strategy
True offline support is more than caching. Decide what your app should do when the network is gone:
- Read-only content: cache pages, show them offline. Easy.
- Forms and writes: queue the submission and replay it when the network comes back. Background Sync helps on Chrome. On iOS, you are on your own.
- Realtime-critical features: show a clear offline state and disable them. Do not pretend.
Most apps need a thin offline layer, not a full offline-first architecture.
Testing
Before you ship, run:
- Lighthouse PWA audit in Chrome DevTools. It will flag every missing manifest field.
- Real device install on iPhone and Android. Emulators lie about install behavior.
- Airplane mode test. Toggle offline and see what breaks.
- Update test. Deploy a new version and verify the service worker picks it up. This is where most PWAs silently fail.
A worked example: Quicktalog
Quicktalog is a PWA. We did not build it as one. We made it installable after the fact because half our users asked for "an app." The work came in three chunks:
- One day to add the manifest, icons, and a minimal service worker.
- Half a day to add the install prompt and the iOS instructions.
- Two days to handle push notifications and the offline-friendly catalog viewer.
The conversion rate on the install prompt sits around 8%. For an app distributed as a link, on a product used by 1,400+ businesses, that is meaningful retention.
When to go native instead
Reach for React Native or native when you need:
- Widgets, Siri shortcuts, or deep Apple Wallet integration.
- Background tasks that run without the app open for long stretches.
- A UI that needs frame-perfect platform-native feel.
Everything else, try a PWA first. Shipping is faster, the team stays smaller, and the maintenance story is the same as your web app.
If you are weighing PWA against React Native for a product you are about to build, we would be happy to compare notes.
