ESC

Type to search the knowledge base.

PWA Install and Offline Shell

System design for PWA installability and offline shell — manifest, SW lifecycle, caching, update UX, and limits.

advanced4 min read
  • 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.onLine false
  • Disable actions that can’t queue
  • If offline-first product: outbox pattern

Install UX

  1. Meet browser criteria (engagement, manifest, SW)
  2. Capture beforeinstallprompt
  3. Show in-app install CTA after value moment
  4. 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:

  1. Detect waiting worker
  2. Show “Update available” toast
  3. On accept: skipWaiting + reload
  4. 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 (/app vs whole origin)
  • Cache-Control on 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

  1. Aggressive skipWaiting vs surprising mid-session reloads
  2. Large precache vs install time / storage
  3. PWA vs native capabilities (push, filesystem)
  4. 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.

Further reading