Progressive Web Apps (PWAs)
Status: 🟩 COMPLETE Last updated: 2026-06-19 Plain-English tagline: A regular website that can install like a native app, work offline, and send push notifications. Add a manifest file and a service worker — and a normal Next.js site becomes installable.
In plain English
A Progressive Web App is a normal website that has been given a few extra capabilities, making it behave more like a native mobile or desktop app. Specifically:
- Installable. Users can “Add to home screen” on mobile, or “Install” from the browser address bar on desktop. After installing, the site opens in its own window without browser chrome (no URL bar, no tabs).
- Works offline. If the user loses connectivity, a cached version of the site continues to work.
- Push notifications. Like a native app, can send notifications even when not open.
- Background sync, native-feeling animations, hardware access (camera, location, file system) within limits.
It’s still a website — same Next.js code, same hosting. You don’t ship to the App Store. Users discover it via a normal URL and then optionally install. The “progressive” part of the name is the idea that the site works for everyone (it’s just a website) and the extra capabilities show up for users with capable browsers (almost everyone in 2026).
PWAs are a great middle ground between “just a website” and “fully native app.” Cheap to add (often a weekend’s effort), no app-store reviews, no native code, no separate iOS/Android codebases. The trade-off: PWAs have access to fewer native APIs than full native apps, and Apple’s support has historically been the weakest.
Why it matters
- Native-app feel without the cost. Build once as a webapp, optionally install for the “app” experience.
- Engagement. Installed PWAs typically have much higher return rates than uninstalled bookmarks.
- Offline access. A travel app, reading app, or notes app dramatically improves with offline support.
- No app store gatekeepers. Ship updates instantly; no review queues.
- Discoverable. Users find your PWA via Google like any other site, not buried in app store search.
For a project like St Mark’s Bible Quest — a reading/study app — PWA features (offline reading, installable, fast revisits) are a significant UX win.
What technically makes a PWA
Three required ingredients:
1. HTTPS
Service workers won’t run on insecure connections. On Vercel this is automatic. On localhost, browsers make an exception for development.
2. A web app manifest (manifest.json or manifest.webmanifest)
A small JSON file describing your app: name, icons, colors, how it should open.
{
"name": "St Mark's Bible Quest",
"short_name": "Bible Quest",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#1d4ed8",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/icon-512-maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}Linked from your HTML:
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#1d4ed8">In Next.js App Router, you can create a app/manifest.ts file that returns this object — Next.js generates the manifest automatically.
3. A service worker
A JavaScript file that runs in a background thread (not on the page), separate from the page’s main thread. The service worker can:
- Intercept network requests and serve them from a cache
- Handle push notifications
- Background sync (queue work to retry when the user comes online)
- Run periodic tasks
A minimal service worker that caches the shell of your site:
// public/sw.js
const CACHE_NAME = "site-v1";
const URLS_TO_CACHE = ["/", "/offline", "/styles.css"];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(URLS_TO_CACHE))
);
});
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((cached) => cached || fetch(event.request))
);
});Register it from your page:
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js");
}Writing service workers from scratch is tedious. Most projects use a library (Workbox, or the next-pwa plugin) to generate one.
The display modes
The manifest’s display value controls how the installed app appears:
| Value | Look |
|---|---|
browser | Like a regular browser tab (no app feel) |
minimal-ui | Custom title bar, no other browser UI |
standalone | No browser UI at all (looks like a native app) — most common choice |
fullscreen | Edge to edge (for games, video) |
standalone is the typical choice for “this should feel like an app.”
Installation flow
How users actually install your PWA:
Desktop (Chrome, Edge, Brave)
An install icon appears in the address bar. Clicking it prompts to install. Once installed, the app appears in the Start menu / Applications folder.
Android (Chrome)
A banner or menu option offers “Add to Home Screen” or “Install app.” After install, an icon goes on the home screen.
iOS (Safari)
Different — Safari requires users to manually tap the Share icon → “Add to Home Screen.” Apple has historically been less PWA-friendly than other platforms, though support has improved.
You can also prompt installation programmatically with the beforeinstallprompt event, but browsers limit how often you can do this — they don’t want users spammed.
Offline strategies
Several patterns for handling offline:
Cache-first
For static assets (CSS, JS, fonts, images, logo): check cache first; fall back to network. Once cached, the asset is essentially free on revisits.
Network-first, fall back to cache
For dynamic content (HTML pages, API responses): try network first (fresh data); if it fails, use cache. User sees fresh content when online; cached content when offline.
Stale-while-revalidate
Serve from cache immediately (fast); fetch fresh in background; update cache for next time. Best for content that’s “usually up to date is fine.”
Offline page fallback
If both network and cache fail, show a designated offline page (/offline). Better than the browser’s default error.
Workbox provides recipes for all of these.
A concrete example: making a Next.js app a PWA
The shortest path with next-pwa:
npm install next-pwa// next.config.js
const withPWA = require("next-pwa")({
dest: "public",
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === "development"
});
module.exports = withPWA({
// your Next.js config
});Create app/manifest.ts:
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "My App",
short_name: "MyApp",
start_url: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#1d4ed8",
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" }
]
};
}Add icons (192Ă—192 and 512Ă—512 PNGs) to public/.
Build and deploy. The site is now installable. Service worker is auto-generated and registered. Static assets cache. Refreshing while offline shows cached content.
That’s the whole thing for a basic PWA. ~15 minutes of work for a real engagement boost.
Push notifications — separate beast
Push notifications work but require additional setup:
- VAPID keys — generate a server-side key pair
- Subscribe the user —
pushManager.subscribe()after permission - Store the subscription — send to your backend
- Send pushes — your backend sends a message via the Web Push API to the user’s browser
- Service worker handler — receives the push, calls
showNotification()
It’s not hard but it’s more than a manifest + service worker. Services like OneSignal, Firebase Cloud Messaging, or Web Push libraries simplify it. For most apps it’s a phase 2 feature.
iOS Safari only added Web Push support in iOS 16.4 (March 2023), and only for installed PWAs. Coverage is now reasonable but not universal.
Limitations vs native apps
PWAs are limited compared to native:
| Capability | PWA | Native |
|---|---|---|
| Background processing | Limited | Full |
| Native widgets | None | Yes |
| Some hardware APIs (Bluetooth, USB) | Limited | Full |
| App store presence | Optional (via wrappers) | Yes |
| Deep OS integration | Limited | Full |
| App size | Often smaller | Often larger |
| Update mechanism | Instant (web) | Via app store |
| Dev workflow | Standard web | Native toolchain |
For content-heavy apps, dashboards, productivity tools, social apps — PWAs are often sufficient. For graphics-heavy games or apps needing deep OS integration — native is still better.
Common gotchas
-
HTTPS required. Service workers won’t register over plain HTTP (except localhost). Vercel handles this automatically.
-
Manifest icon errors. Browsers complain about specific sizes (especially 192 and 512). Generate icons via tools like RealFaviconGenerator.
-
Maskable icons. Modern Android crops icons. A “maskable” icon variant has safe-zone padding so the crop doesn’t chop content. Include both regular and maskable variants.
-
Service worker scope. A service worker registered at
/sw.jscontrols everything under/. Registered at/app/sw.js, only controls/app/. Place at the root unless you have a specific reason not to. -
Service worker caching too aggressively. A cache-first service worker for HTML means users see old content even after you deploy. Use network-first for pages or set short cache TTLs.
-
The “stuck on old version” trap. Even with a new service worker deployed, users running the old version stay on the old cache until the new SW activates. Workbox’s
skipWaiting+clientsClaimcan speed this up; tradeoffs apply. -
Auth + service workers. Cached responses don’t include user-specific data. If you cache
/dashboard, all users see the same cached version. Either don’t cache personalized routes, or cache per-user. -
iOS quirks. Safari has historically lagged. Things that work on Chrome/Edge may not on Safari. Test on iOS specifically.
-
localStoragedoesn’t sync with the service worker’s storage. The service worker has its ownCache APIand IndexedDB. Coordinate carefully. -
Installation prompts can’t be triggered arbitrarily. Browsers require user engagement before showing them. Don’t expect to control the prompt freely.
-
Push permission is sticky. Once a user denies notifications, getting them back is hard (settings buried deep). Ask thoughtfully; only when there’s real value.
-
PWA debugging is harder than regular web debugging. Chrome DevTools → Application tab has service worker controls. Get familiar with “Update on reload” during dev.
-
The
manifest.jsoncontent type. Some servers serve it asapplication/json; the spec wantsapplication/manifest+json. Most browsers accept either; some don’t. -
next-pwarequires extra care with App Router. It was designed for Pages Router originally; check current compatibility notes. -
App store submission of PWAs. Possible via wrappers (PWABuilder for Microsoft Store, Bubblewrap for Play Store). Apple’s App Store doesn’t accept pure PWAs.
See also
- HTML 🟩 — manifest link tag lives here
- JavaScript 🟩 — service workers are JS
- The DOM 🟩 — service workers have NO DOM access
- Next.js 🟩 🟦 — has built-in manifest support
- Responsive design 🟩 — PWAs must work mobile-first
- Accessibility (a11y) 🟩 — same rules apply
- Vercel 🟩 🟦 — auto-HTTPS makes PWAs easy
- How-to: Set up dark mode 🟩 — PWA users notice theme support
- Glossary: Worker (Web Worker, Service Worker)
Sources
- web.dev — Learn PWA
- MDN — Progressive Web Apps
- Workbox docs
- Next.js —
app/manifest - PWABuilder — generate manifests, package for stores
- whatwebcando.today — what web APIs are supported in current browsers