Appearance
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, andconfig/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 ownsw.jswins. 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+xmlfrom/assets/*(stored-XSS guard); everything isnosniff, and a miss returns a JS-safe 404, never the HTML 404 page.
Well-known routes
| Path | Serves | Cache |
|---|---|---|
/manifest.json | synthesized PWA manifest | max-age=3600 + tag |
/sw.js | service worker (theme's own else default, versioned) | max-age=3600 |
/app-icon.png | merchant-uploaded app icon (validated) | max-age=3600 |
/icon-96|192|512.png | generated monogram icons (PNG-cache memoized) | max-age=3600 |
/screenshot-wide|narrow.png | uploaded or generated install screenshots | max-age=3600 |
/favicon.ico | theme favicon → app-icon → generated monogram | max-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.