Which engine reads this, and what happens to the user who already installed the previous build?
The first half settles every manifest and install question: behaviour differs per engine and has changed more than once, so the published docs and the model's memory both lag. The second half settles every caching and deploy question: a PWA is the web app whose users keep yesterday's code, so every change is judged by what it does to the installed cohort, not a first visit.
Stale beliefs to drop
Each row was reproduced by agents working without this skill.
| Belief | Current fact | Evidence |
|---|---|---|
| Verify with Lighthouse's PWA audit | Lighthouse has no PWA category since 12.0.0 (2024-04-22); 13.5.0 rejects --only-categories=pwa. Verify per Prove it |
observed 2026-09-24 |
| iOS ignores manifest icons | Safari uses manifest icons when no apple-touch-icon exists and purpose is any or unset (iOS 15.4+); apple-touch-icon wins when both exist. Still ship the touch icon; check purpose when the manifest is the only source |
documented |
A long Cache-Control on sw.js delays updates in the browser |
updateViaCache defaults to imports: the browser refetches sw.js past its HTTP cache on every in-scope navigation. The header bites only at a CDN edge that honours max-age/s-maxage, or under updateViaCache: 'all' |
observed, Chrome 154 |
Denylisting /api/ keeps API calls away from the shell |
navigateFallback answers navigations only; fetch() never gets the shell. What /api/-only misses is every other navigation the server owns: OAuth/OIDC callbacks, server-rendered routes, webhooks, admin pages |
observed |
| The manifest is complete once installable | Without screenshots Chrome shows the plain install prompt, not the richer dialog |
documented |
Scope
This skill owns install, the manifest, the service worker, offline, updates, client-side push and store wrappers. It stops at:
- web-perf - the first load of a route with no service worker in control. A worker that changes first paint is a web-perf symptom whose cause lives here.
- cloudflare-workers-deployments (or the host's docs) - how a header lands at the edge. This skill names the header and the emitted URL it must be on.
- Server-side push - VAPID keys, subscription storage, sending.
Wire it into the framework you have
Detect the framework and build tool from the repo before recommending anything. One default each; the escape hatch is for when the default does not fit.
- Vite SPA (React, Vue, Svelte without Kit):
vite-plugin-pwa,generateSW,registerType: 'prompt'with a rendered prompt. Escape hatch:injectManifestfor custom fetch logic. - Next.js: manifest via the App Router
manifest.ts; caching via Serwist (@serwist/next).next-pwaand@ducanh2912/next-pwaare unmaintained webpack plugins - do not add them. Escape hatch: a hand-written push-only worker when there is no offline requirement. - TanStack Start: a typical
VitePWA()config builds green but writesdist/sw.js, outside the.output/public/Nitro serves, whileregisterSW.jsstill points at/sw.js. Default: the SPA-mode recipe in references/toolchain.md, then assert the file landed. Escape hatch: generate the worker withworkbox-buildfrom a post-build step. Rsbuild builds get no Vite plugin at all. - Foldkit: SPA mode is the Vite SPA path. In SSR/SSG mode a page from one
build with JS from another is refused terminally (inert page, shield dialog,
no recovery), so never serve cached HTML across deploys: navigations
NetworkOnly, offline fallback a page precached with the same build id.
Look up every version before pinning it (pnpm view <pkg> version time.modified);
recall is always behind. See references/toolchain.md.
Rules that hold regardless of tool
- Denylist every navigation the server owns from
navigateFallback, not just/api/. An OAuth callback answered by the shell never exchanges its code. - Never runtime-cache cross-origin
no-corsresponses without a bound. Each opaque entry costs megabytes of quota in Chrome (2 KB responses measured ~9 MiB each). WorkboxNetworkFirstandStaleWhileRevalidatecache opaque responses by default; onlyCacheFirstrefuses them. Pair withExpirationPluginmaxEntriesandpurgeOnQuotaError: true, or use CORS. - Keep
sw.jsat one stable URL and serve itCache-Control: no-cachefrom the CDN too - the browser skips its own cache, the edge does not. - Judge every caching change by the installed cohort: what does a user on the previous worker see after this deploy, online and offline?
Prove it
Lighthouse has no PWA audit. Run these instead:
- Build, then run the checker on the directory the host serves (EXECUTE):
node <skill-dir>/scripts/pwa-check.mjs --dir <deployed-output-dir>. Add--url <origin>once deployed, to read headers on the emitted URLs. Exit 1 means a FAIL: most often a registered worker missing from the served directory. Fix before anything else. - Prove installability in Chrome: DevTools > Application > Manifest
(Installability section), or
Page.getInstallabilityErrorsover CDP in CI (references/verify.md). - Prove updates: deploy twice with a tab open and watch the new worker reach
waitingand the prompt appear. - iOS, Android WebAPK and store builds need a real device; state which checks ran and which did not.
References
| When the task involves... | Read |
|---|---|
Manifest members, id/scope/start_url, icons, screenshots |
references/manifest.md |
| Why an install prompt or install option does not appear, per engine | references/installability.md |
Caching strategies, navigateFallback, offline fallback, quota, removing a worker |
references/service-workers.md |
| Users stuck on an old deploy, the waiting worker, update prompts, CDN headers | references/updates.md |
| Which plugin per framework, versions, the TanStack Start and Foldkit recipes | references/toolchain.md |
| iPhone/iPad: Home Screen apps, icons, status bar, storage, push | references/ios.md |
Play Store / TWA / Bubblewrap / PWABuilder / assetlinks.json, App Store |
references/store.md |
| Verifying in CI or by hand, Playwright constraints, device checklist | references/verify.md |
scripts/check-currency.mjs re-checks the dated claims listed in
scripts/currency-claims.json (maintenance, not project work);
tests/pwa-check.bats covers the checker; evals/evals.json holds the test
prompts. Evidence labels:
observed = reproduced on a real build or stable browser on the date given;
documented = vendor docs or spec only, not observed here.