Dock
Sign in & remix
REMIX PREVIEWTemplate

Build a Progressive Web App that installs to the home screen

9-step playbook from 'I have a webapp' to 'installable PWA with offline mode + push.' Real iOS quirks, real service worker traps, real agent prompts.

· 16 min read· from trydock.ai

Build a Progressive Web App that installs to the home screen

A 9-step playbook. Open in Dock and you'll get four surfaces seeded:

- **Steps** (table) — the 9 gates as rows, owner + due + status
- **Pointers** (table) — every official MDN + web.dev doc + tool linked from this playbook
- **Brief** (doc) — the canonical write-up you maintain alongside the work
- **Audit log** (table) — one row per Lighthouse audit (date, performance, accessibility, best-practices, SEO, PWA score)

Read `Steps` top-to-bottom on first open. Each row is one of the 9 steps. Click into a step to see the tasks, pointers, and the agent prompt for that step.

Outcome

Your webapp installable to the home screen on Android + iOS + Desktop, working offline for the read-only paths, with the install prompt + push notifications wired correctly.

Estimated time: 3-7 days
Difficulty: intermediate
For: Web developers turning a webapp into an installable PWA.

What you'll need

Pre-register or install before you start.

  • Workbox (Free (npm)) — Google's service-worker library. Pre-caching, runtime caching strategies, navigation routes.
  • Web App Manifest spec (Free (spec)) — The JSON file that tells browsers your site is installable.
  • Lighthouse (Free) — Built into Chrome DevTools. Audits PWA score, performance, accessibility.
  • PWA Builder (Free) — Microsoft's tool to scaffold the manifest + service worker + icon set.
  • Web Push protocol (Free) — Standard for delivering push notifications via VAPID + service worker.

The template · 9 steps

Step 1: Confirm HTTPS + a fast baseline

Estimated time: 1-3 hr

PWAs require HTTPS. Service workers won't register on http (except localhost). Beyond HTTPS, Lighthouse PWA score requires a Performance score above thresholds — a slow site cannot be a PWA. Run a Lighthouse audit FIRST and decide whether to fix performance before adding the manifest + service worker.

Tasks

  • Confirm site serves on HTTPS (Cloudflare, Vercel, Netlify all do this for free)
  • Run Lighthouse against your prod URL (Chrome DevTools → Lighthouse → Generate report)
  • Note the Performance score; if below 80, prioritise the top 3 issues before continuing
  • Run with both Mobile + Desktop emulation; mobile is the harder bar
  • Save the baseline scores to the Audit log

Pointers

[!CAUTION] Gotchas

  • Service workers will not register on http://. localhost is the only exception. Don't waste time debugging install issues if your site is on http://.
  • Mixed content (https page loading http assets) breaks service worker fetch interception. Audit your site's network panel for any http URLs.
  • Self-signed certs work for localhost but fail in production for service worker registration in some browsers. Use Let's Encrypt / Cloudflare for real certs.

Step 2: Write the Web App Manifest

Estimated time: 2-3 hr

manifest.webmanifest (or manifest.json) is what tells the browser your site is an app. Required: name, short_name, start_url, display, icons, theme_color, background_color. The icon set is the single fiddliest part — multiple sizes including the 512x512 maskable icon for adaptive icons on Android.

Tasks

  • Create /manifest.webmanifest at your site root
  • Set name, short_name (≤12 chars for Android home screen), description
  • Set start_url (where the app opens — usually / or /home)
  • Set display: 'standalone' (most common; fullscreen / minimal-ui are alternatives)
  • Set theme_color (status bar tint) + background_color (splash screen bg)
  • Set icons: 192x192 + 512x512 minimum, both 'any' AND 'maskable' purpose
  • Add screenshots: at least one wide + one narrow for richer install prompts
  • Link manifest from index.html:

Pointers

[!CAUTION] Gotchas

  • short_name over 12 chars gets truncated by Android, often mid-word. Test in your dev browser's Chrome DevTools → Application → Manifest preview.
  • Maskable icons need a 'safe zone' (80% diameter circle in the centre). Without it, Android crops your logo on the home screen.
  • iOS ignores most of the manifest. iOS reads and tags instead. Set both.

Agent prompt for this step

Draft the Web App Manifest for this site.

Read the site's branding (logo, colours, taglines) and the public landing page. Output:

1. A complete manifest.webmanifest JSON with: name, short_name (≤12 chars), description, start_url, display, theme_color, background_color, icons (192 + 512, any + maskable), screenshots (at least one wide 1280x720 + one narrow 750x1334), categories, lang, dir.
2. The HTML <link> + meta tags to add to index.html: manifest link, theme-color, apple-touch-icon, viewport.
3. A short note on what's iOS-only vs cross-platform in the output.

Constraint: short_name MUST be ≤12 chars (Android truncates). description ≤300 chars. theme_color + background_color must be hex.

Step 3: Build the icon + splash screen set

Estimated time: 3-5 hr

PWAs need a lot of icons: 192x192 + 512x512 for Android, apple-touch-icon (180x180) for iOS, multiple favicons for desktop. iOS additionally needs splash screens at every device resolution if you want a non-blank splash on launch (a per-iPhone-size set of ~10 PNGs).

Tasks

  • Design the master icon at 1024x1024 PNG with a 80% safe-zone circle
  • Generate 192x192 + 512x512 maskable + 192x192 + 512x512 'any' versions
  • Generate apple-touch-icon at 180x180 (no transparency, square corners — iOS rounds them)
  • Generate favicons: 32x32, 16x16, favicon.ico
  • Optional: generate iOS splash screens for every supported device (iPhone SE, 12, 13, 14, 15, 16 Pro Max, iPad)
  • Reference each splash screen in HTML:

Pointers

[!CAUTION] Gotchas

  • iOS apple-touch-icon WITH alpha channel renders with a black background on the home screen. Strip alpha; use a solid colour fill behind your logo.
  • Splash screen sizes change every iPhone generation. PWA Asset Generator covers up to current; new device launches need a re-run.
  • Favicons cached aggressively; users see the old one for days after you ship a new one. Set a long-lived URL with a content hash for cache busting.

Step 4: Register a service worker with Workbox

Estimated time: 3-5 hr

The service worker is the offline + caching brain. Hand-rolling one is fine for simple sites; for anything with dynamic data, use Workbox. workbox-build / workbox-webpack-plugin / vite-plugin-pwa generate the SW from a config; you pick caching strategies per URL pattern.

Tasks

  • npm i -D workbox-webpack-plugin (or vite-plugin-pwa for Vite)
  • Configure: precache the app shell, runtime-cache the API, fallback page on offline navigation
  • Pick caching strategies: CacheFirst for static assets, NetworkFirst for HTML/API, StaleWhileRevalidate for images
  • Build → confirm sw.js is generated + linked
  • Register in main.ts: navigator.serviceWorker.register('/sw.js')
  • Test in DevTools → Application → Service Workers (status: activated and running)

Pointers

[!CAUTION] Gotchas

  • Service workers update on a 24-hour cycle by default. Users won't see your new SW until they close + reopen the tab. Use registerSW({ immediate: true }) or skipWaiting() for fast updates.
  • If your SW is at /sw.js, it scopes to /. If at /assets/sw.js, it scopes to /assets/. Always serve sw.js from the root.
  • A bad SW deployed once will brick your site for users — they get the cached broken version forever. Always implement a kill-switch (a SW that unregisters itself) before you ship.

Agent prompt for this step

Audit this webapp's network panel + asset list, then propose a workbox caching strategy per route.

For each request type:
1. /assets/* (JS, CSS, fonts) → strategy + max-age + max-entries
2. /images/* → strategy + max-age + max-entries
3. /api/* → strategy + max-age + which queries to cache vs always-network
4. / (HTML) → strategy + offline fallback page

Output a workbox config object that drops into vite-plugin-pwa's runtimeCaching array. Include comments explaining why each strategy fits.

Constraint: don't cache POST / PUT / DELETE. Don't cache responses with auth headers unless explicitly safe.

Step 5: Implement the install prompt + handle iOS Add to Home Screen

Estimated time: 2-3 hr

On Android + Desktop Chrome, the browser fires a beforeinstallprompt event you can defer + show on a button click. On iOS, there is no programmatic install prompt — you have to coach the user to tap Share → Add to Home Screen. Handle both paths.

Tasks

  • On beforeinstallprompt: e.preventDefault() + save the event for later
  • Render an Install button that calls savedEvent.prompt() on click
  • After install: hide the button (listen for appinstalled event)
  • On iOS: detect via /iPhone|iPad|iPod/ in user-agent + display.standalone === false
  • Show iOS-specific Add to Home Screen instructions on iOS Safari (Share button screenshot + steps)
  • Don't show install prompt on first visit — wait for engagement (e.g. 2nd visit or 30 sec on site)

Pointers

[!CAUTION] Gotchas

  • iOS Safari does NOT fire beforeinstallprompt. Coding only against that event leaves iOS users with no install path. Always have an iOS-specific fallback UI.
  • Showing the install prompt aggressively (within 5 sec of first visit) drops install rate. Wait for engagement signals (2nd visit, scroll past hero, completed an action).
  • Once a user dismisses the install prompt, beforeinstallprompt won't fire again for 90 days. That dismissal is per origin per device; you cannot retry sooner.

Step 6: Build an offline experience with a fallback page

Estimated time: 2-3 hr

Real PWAs handle 'no network' gracefully. Pre-cache an /offline.html fallback page; on navigation requests that fail, serve it. For dynamic content (a feed, a chat), cache the last-fetched data and show it with a 'showing cached' badge.

Tasks

  • Create /offline.html with a friendly explanation + a Retry button
  • Add to workbox precache list
  • Add a navigation route handler: NetworkOnly with offlineFallback to /offline.html
  • For data-driven views: cache the last response per route + render with a 'last updated 5 min ago' badge
  • Test: DevTools → Network → Offline → reload the page → confirm the fallback renders
  • Test: navigate around the cached pages — they should render from cache without network

Pointers

[!CAUTION] Gotchas

  • If your offline fallback links to /home but /home isn't cached, the user is stuck. Pre-cache the entire navigation path the offline page links to.
  • Cache invalidation is hard. Set max-age + max-entries on every route or your service worker grows unbounded and the browser silently evicts you.
  • Mobile carriers sometimes return 200 with an HTML captive-portal page on 'no internet'. Your SW caches that and serves it on every future request. Detect via response.url checks.

Step 7: Wire push notifications (the right way per platform)

Estimated time: 1-2 days

Web Push uses the Push API + Notifications API + a server-side push service. Android + Desktop Chrome / Firefox / Edge: works out of the box with VAPID keys. iOS: required iOS 16.4+ AND the user must Add to Home Screen first; web push doesn't work in Safari tab on iOS. Plan for the platform split.

Tasks

  • Generate VAPID keypair (web-push CLI or your hosting platform)
  • Server: store public + private VAPID keys; expose endpoint to register subscriptions
  • Client: navigator.serviceWorker.ready → registration.pushManager.subscribe({ applicationServerKey: VAPID_PUBLIC })
  • Client: POST the subscription to your server; store keyed by user_id
  • Server: web-push library to send notifications
  • Service worker: self.addEventListener('push', e => self.registration.showNotification(...))
  • iOS-specific: detect iOS, only request push permission AFTER Add to Home Screen + iOS 16.4+
  • Don't request notification permission on page load — wait for user action

Pointers

[!CAUTION] Gotchas

  • iOS requires the user to Add to Home Screen FIRST; web push from a tab is not supported. Detect both standalone display + iOS version before showing your Notify Me button.
  • Notification.requestPermission() must run from a user gesture (click handler). Calling it on page load shows the prompt but Chrome instantly denies + flags your origin as abusive.
  • Subscription objects expire silently (typically every 60-90 days, faster for iOS). Implement a re-subscribe flow when pushManager.getSubscription() returns null.

Step 8: Hit Lighthouse 90+ across PWA + Performance + Accessibility

Estimated time: 1-2 days iterating

Lighthouse is the canonical PWA score. Aim for 90+ across PWA, Performance, Accessibility, Best Practices, SEO. Below 90 in any category = you ship the PWA, but it's a 'cheap PWA'. Above 90 puts you in App Store-grade territory.

Tasks

  • Run Lighthouse, save baseline scores in Audit log
  • PWA: confirm manifest, service worker, HTTPS, theme color, icons all green
  • Performance: target LCP <2.5s, CLS <0.1, INP <200ms (Core Web Vitals)
  • Accessibility: tab through every page, fix every Lighthouse axe issue
  • Best Practices: address every CSP / third-party / source map warning
  • SEO: meta description, alt text, valid heading order, structured data
  • Re-run after each fix; commit with the score in the message

Pointers

[!CAUTION] Gotchas

  • Lighthouse scores fluctuate ±5 points per run on the same code. Use the median of 3-5 runs, not a single number.
  • Performance score is heavily affected by the network throttle. Lighthouse defaults to Slow 4G; that's the score Google uses for ranking, not your fast WiFi number.
  • Best Practices flags console errors, including ones from third-party SDKs you don't control. Set window.onerror to filter them out or accept the score hit.

Step 9: Submit to app stores via TWA + PWABuilder

Estimated time: 2-4 days per platform

PWAs can be wrapped as native app store submissions: Trusted Web Activity (TWA) for Google Play (Chrome wrapper around your PWA), PWABuilder for Microsoft Store, and recently Apple's App Store accepts PWA-wrapped apps via Capacitor. App store presence widens your audience but doubles the maintenance.

Tasks

  • Decide which app stores to target (Google Play has the lowest overhead via TWA)
  • Use PWABuilder to scaffold the TWA Android Studio project
  • Configure Digital Asset Links so the TWA opens without browser UI
  • Test the TWA on a real Android device
  • For Microsoft Store: PWABuilder generates a Windows app package
  • For Apple App Store: wrap with Capacitor (separate effort) or skip and rely on Add to Home Screen
  • Submit each platform via its existing playbook (we have separate playbooks for App Store + Play Store gates)

Pointers

[!CAUTION] Gotchas

  • TWAs need Digital Asset Links set up correctly or the user sees a browser address bar inside the 'app'. Test by installing the .apk on a device and watching for the chrome bar.
  • iOS App Store Review used to reject PWA wrappers wholesale. As of 2024 they accept Capacitor-wrapped apps if there's at least minimal native integration. Pure WebView wrappers still fail.
  • Microsoft Store accepts most PWAs via PWABuilder almost automatically. It's the easiest store to be in but the smallest audience.

Hand the template to your agent

Paste the prompt below into your agent's permanent system prompt so the agent reads, writes, and maintains this workspace as you work through the steps.

You are an agent on the "Build a Progressive Web App" playbook workspace at your-org/build-a-progressive-web-app.

Your role: maintain the four surfaces (Steps, Pointers, Brief, Audit log) as the user works through the 9-step playbook.

Cadence:
- When the user marks a step Done, append a line to the Brief.
- After every Lighthouse run (the user pastes the JSON or runs npx lighthouse), append a row to Audit log: date + 5 scores + delta-from-previous.
- When PWA score drops below 90, draft a Brief note flagging the regression + suspected cause.

First MCP tool calls:
1. list_surfaces(workspace_slug="build-a-progressive-web-app")
2. list_rows(workspace_slug="build-a-progressive-web-app", surface_slug="steps")
3. get_doc(workspace_slug="build-a-progressive-web-app", surface_slug="brief")

Do NOT modify canonical step titles. Append substeps as new rows.

FAQ

Do I need a service worker if I just want the install prompt?

Yes. The browser's install criteria require BOTH a valid Web App Manifest AND a registered service worker that handles fetch events. A manifest alone is enough for some early Android variants but Chrome + Edge + Safari Add-to-Home-Screen all gate on the SW too. Use a minimal Workbox config if you don't need offline.

Does my PWA work on iOS?

Mostly. iOS supports the manifest (start_url, display: standalone, apple-touch-icon), service workers, and as of iOS 16.4+ Web Push (only after Add to Home Screen). It does NOT support the beforeinstallprompt event, badging, share target, or background sync. Plan for an iOS-specific install instruction UI.

How big is too big for the precache?

Aim for under 5 MB total precache; under 2 MB is ideal. Over 10 MB and Lighthouse flags it as 'service worker is too aggressive', users on slow networks have a 30-60 sec install delay before the SW activates, and quotas start kicking in on lower-end Android.

Should I ship to app stores or stay PWA-only?

Depends on the audience. If users find your app via Google search or a marketing site, stay PWA-only — Add to Home Screen + an iOS install banner converts at 1-3% of engaged users. If users expect to find apps in stores (B2C consumer apps, productivity apps), ship the TWA to Google Play and consider Microsoft Store. Apple App Store via Capacitor is a heavier lift.

Can my AI agents help with the PWA?

Yes. Agents are particularly useful for: drafting the manifest from your branding + screenshots, picking workbox caching strategies from a network audit, drafting the offline fallback page copy, designing the iOS Add to Home Screen instructions UI, generating the icon + splash screen set from a single source. The playbook ships agent prompts inline.

Remix this into Dock

Make this yours. Edit, extend, run agents on it.

Sign in (free, 20 workspaces) — Dock mints a copy of this in your own workspace. The original stays untouched.

Sign in & remix

No Dock account? Sign-in is signup. Magic-link in 30 seconds.