Skip to content

PWA & assets

Chapter 9. The infrastructure serves bytes; some of them make each store an installable app. This chapter covers the PWA the origin synthesizes and how it serves static assets safely.

A fresh store is a complete installable PWA with nothing uploaded — the origin synthesizes the manifest, the icons, and the service worker on the fly. This page covers that generation, plus how the origin serves static assets safely. Code: apps/origin/src/handlers/assets.ts, packages/builder-core/src/theme/{manifest,icon,monogram,service-worker}.ts.

The PWA is generated, not authored

  • Manifest (manifest.ts) — built from the store name, brand colour, and config/pwa.json; it is fully synthesized, never a merge of a merchant-authored manifest. Validation caps + universal shortcuts.
  • Icons (icon.ts, monogram.ts) — a fallback ladder: a merchant upload (validated, real dimensions) → else a generated monogram (a 5×7 bitmap font, contrast-picked for WCAG) → else a solid PNG. Both use a hand-rolled, zero-dependency PNG/CRC32 encoder.
  • Service worker (service-worker.ts) — default-on; a theme's own sw.js wins. Its cache name is versioned by the live theme version, so a publish evicts orphaned caches. It's registered via a snippet whose sha256 is pinned in the CSP (worker-src 'self').

Serving assets safely

handleAssets (assets.ts) — the branch order is load-bearing and security-critical:

  • Manifest-first — a bogus-but-valid-looking hash 404s before any DB or S3 read.
  • Content-type is neutralized — a merchant can't serve text/html/image/svg+xml from /assets/* (stored-XSS guard); everything is nosniff, and a miss returns a JS-safe 404, never the HTML 404 page.

Well-known routes

PathServesCache
/manifest.jsonsynthesized PWA manifestmax-age=3600 + tag
/sw.jsservice worker (theme's own else default, versioned)max-age=3600
/app-icon.pngmerchant-uploaded app icon (validated)max-age=3600
/icon-96|192|512.pnggenerated monogram icons (PNG-cache memoized)max-age=3600
/screenshot-wide|narrow.pnguploaded or generated install screenshotsmax-age=3600
/favicon.icotheme favicon → app-icon → generated monogrammax-age=3600

Purge by URL, for now

These well-known paths are purged by URL (WELL_KNOWN_PWA_PATHS) after a store-wide change, because Cloudflare cache-tag purge is an Enterprise feature and there's no tag→URL index yet. A process-wide PNG LRU absorbs the miss-storm right after a purge on the single-process origin.