PWA Install and Offline Shell
System design for PWA installability and offline shell — manifest, SW lifecycle, caching, update UX, and limits.
- system-design
- interview
- architecture
- pwa
- offline
Scope the problem
In scope: making a web app installable, serving an offline shell, update flows, and cache strategy for app chrome.
Out of scope: full offline-first domain sync (see notes app design); native store packaging details.
Goals
| Goal | Signal |
|---|---|
| Installable | manifest + SW + icons; beforeinstallprompt UX |
| Offline usable | shell loads; honest offline pages |
| Fresh enough | updates applied without trapping users on ancient builds |
| Safe | HTTPS; cache poisoning avoided |
Building blocks
Web App Manifest + Service Worker + HTTPS + Icons
Manifest (sketch)
{
"name": "Notes",
"short_name": "Notes",
"start_url": "/?source=pwa",
"display": "standalone",
"background_color": "#0b0b0c",
"theme_color": "#0b0b0c",
"icons": [
{ "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
]
}
Service worker lifecycle
install → activate → fetch handler
↘ skipWaiting / clients.claim (carefully)
install: precache shell assets (HTML shell, app CSS/JS hashed, offline fallback).
activate: delete old cache names.
fetch: route strategies.
// conceptual
const SHELL = "shell-v12";
self.addEventListener("install", (e) => {
e.waitUntil(caches.open(SHELL).then((c) => c.addAll(PRECACHE_URLS)));
});
Caching strategy map
| Resource | Strategy |
|---|---|
| Precached shell | cache-first |
| Fingerprinted static | cache-first |
| API GET | network-first + fallback cache |
| API mutations | network-only |
| Navigation | network-first → offline.html |
Precache list must use hashed filenames from build manifest (Workbox injectManifest).
Offline UX
- Offline fallback page with retry
- Banner when
navigator.onLinefalse - Disable actions that can’t queue
- If offline-first product: outbox pattern
Install UX
- Meet browser criteria (engagement, manifest, SW)
- Capture
beforeinstallprompt - Show in-app install CTA after value moment
- Don’t spam on first paint
iOS: limited install prompt — guide Add to Home Screen.
Update UX (critical)
Problem: SW can leave users on old app for weeks.
Pattern:
- Detect
waitingworker - Show “Update available” toast
- On accept:
skipWaiting+ reload - For breaking API changes: force update sooner
registration.addEventListener("updatefound", () => {
const nw = registration.installing;
nw?.addEventListener("statechange", () => {
if (nw.state === "installed" && navigator.serviceWorker.controller) {
promptUserToRefresh();
}
});
});
Security & correctness
- Never cache personalized HTML with service worker as public shell blindly
- Scope SW carefully (
/appvs whole origin) Cache-Controlon SW file itself should revalidate (max-age=0)- HTTPS only
Performance
- Precache small shell only
- Runtime cache with quotas (delete old images)
- Avoid SW for large streaming video
Tradeoffs
- Aggressive skipWaiting vs surprising mid-session reloads
- Large precache vs install time / storage
- PWA vs native capabilities (push, filesystem)
- Offline shell only vs full offline data
Interview close
Manifest + SW precache shell → network-first navigations with offline fallback → hashed assets → update prompt with skipWaiting → install CTA at value moment. Domain offline sync is a separate layer.
Precache manifest generation
Wire the SW to the bundler’s asset list so hashed files are exact:
// workbox-style conceptual
precacheAndRoute(self.__WB_MANIFEST);
Manual precache arrays rot every deploy — treat generation as CI-required.
Testing matrix
| Scenario | Expect |
|---|---|
| First visit online | SW installs; next load controlled by strategy |
| Airplane mode after visit | shell + offline page |
| New deploy waiting | prompt; reload gets new shell |
| API while offline | queued or clear error |
Storage pressure
Browsers evict cache under quota pressure. Design offline UX to degrade: shell always; user data only if you separately implement IndexedDB domain storage. Don’t promise “full offline app” with shell-only precache.
Related on this site
- Design Offline-first Notes App
- Frontend Caching Strategies
- Offline Status Indicator
- System Design Interview Framework