> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR-017: PWA service-worker scoping under Vercel Microfrontends

> Status: Proposed (becomes Accepted on Phase 3 activation per MICROFRONTENDS_RUNBOOK.md) Date: 2026-04-22 Participants: Platform Architecture (cloud agent draft…

**Status:** Proposed (becomes Accepted on Phase 3 activation per `MICROFRONTENDS_RUNBOOK.md`)
**Date:** 2026-04-22
**Participants:** Platform Architecture (cloud agent draft, Jeremy Bloom approval at activation)
**Related:** [`ADR-013`](ADR-013-pwa-strategy.md) (amended by this ADR), [`ADR-008`](ADR-008-vite-spa-vs-ssr.md), Phase 3 of `docs/recommendations/DEV_VELOCITY_AND_ARCHITECTURE_PLAN_2026-04-22.md` (PR #137)

***

## Context

[`ADR-013`](ADR-013-pwa-strategy.md) established the platform's PWA strategy: a single Workbox service worker registered at scope `/`, with CacheFirst for static assets, NetworkFirst for REST, and NetworkOnly for auth. That ADR assumes **a single app** generating one `sw.js` and one precache manifest.

Phase 3 (`docs/development/MICROFRONTENDS_RUNBOOK.md`) introduces a second Vercel project (`encoreos-public`) that handles `/auth`, `/employee-setup`, `/pending-activation`, `/pm/kiosk/:siteId/*`, `/portal/*`, `/p/:token`, `/packet/:token`, `/gr/whistleblower-report`, `/chatbot/widget`. Both projects use `vite-plugin-pwa` today, so each will independently generate a `sw.js`.

**The problem:** only one service worker can claim scope `/` per origin. If both Vercel projects emit `sw.js` at the root with `scope: '/'`, whichever one installs second will fail (or, worse, supersede the first and start serving cached chunks from the wrong project).

This is a real, known issue with Vite + microfrontends and is the headline reason the recommendations doc (§ 3.5) flagged the PWA scope problem as a "non-trivial migration cost".

## Options Considered

### Option A: Keep `encoreos-app` SW at scope `/`; disable SW on `encoreos-public`

* `encoreos-app` continues to emit `sw.js` at `scope: '/'` exactly as today.
* `encoreos-public` removes `vite-plugin-pwa` (or sets `registerType: 'none'`).
* Result: the public zone has **no offline support, no PWA install prompt, no precache**. That is acceptable for the public zone because:
  * Auth pages are short-lived and use `NetworkOnly` for the auth endpoint anyway.
  * The kiosk hardware is wall-powered, on a known network, and reloads on tap — offline is not a real requirement.
  * Public form portals (`/p/*`, `/packet/*`, `/portal/*`) need server reachability to submit anyway.

**Pros:** Zero risk to the existing PWA install base. ADR-013 is preserved verbatim for the authenticated zone (the only place users actually use PWA features). Smallest possible delta.

**Cons:** Public-zone users cannot install the public zone as a PWA on its own. (No real demand for this today.)

### Option B: Each zone owns a sub-scope SW

* `encoreos-app` keeps `scope: '/'`.
* `encoreos-public` registers `sw.js` at `scope: '/auth/'`, `scope: '/portal/'`, etc. — one SW registration per route family.
* Vercel's microfrontends asset prefix machinery handles routing the SW file requests correctly per zone.

**Pros:** Each zone can have a tailored offline experience.

**Cons:** Browsers limit the number of active SW registrations per origin. Multiple sub-scope SWs is a non-standard pattern and historically has produced inconsistent behavior across Chrome / Safari / Firefox. The kiosk path (`/pm/kiosk/...`) is awkward — the literal scope must match the kiosk route, which today is `/pm/kiosk/:siteId`, so the scope would be `/pm/kiosk/` (which is fine, but a single missing trailing slash breaks it). High testing surface for low value.

### Option C: A single shared SW lives in `encoreos-app`; precaches both zones' assets

* Only `encoreos-app` generates a SW. It precaches its own chunks AND fetches and precaches `encoreos-public`'s critical chunks via the Vercel asset-prefix URLs.
* Requires custom Workbox plugin work to discover `encoreos-public`'s manifest at build time.

**Pros:** One SW; full offline support across both zones.

**Cons:** Tight build-time coupling between two Vercel projects (the whole point of microfrontends is to decouple them). Custom Workbox config is fragile. We'd own a one-off integration that doesn't appear elsewhere in the ecosystem.

## Decision

**Adopt Option A.** At Phase 3 activation, the wiring PR will:

1. Leave `encoreos-app`'s `vite.config.ts` PWA configuration **unchanged**. It continues to register `sw.js` at scope `/` exactly as documented in ADR-013.
2. In `encoreos-public`'s build config (when that project is created with its own `vite.config.ts` or a shared config gated on `VERCEL_PROJECT_ID`), disable `vite-plugin-pwa` entirely. Concretely: skip the `VitePWA({...})` plugin invocation when the build target is `encoreos-public`.
3. The `encoreos-public` `index.html` does NOT include `<link rel="manifest">` and does NOT register a service worker.
4. The Workbox `runtimeCaching` rule already in place that excludes `/auth/*` from caching becomes redundant (the SW does not run on those routes anymore once `encoreos-public` owns them) but stays in place as defense-in-depth.

Upgrade path: if a future requirement demands offline behavior in the public zone (e.g., a kiosk that must remain functional when the network drops mid-session), revisit Option B for *just that one* sub-path. Do not preemptively adopt Option B for all public-zone routes.

## Consequences

### Positive

* ADR-013's behavior in the authenticated zone (which is where \~100% of PWA usage actually happens today) is **preserved unchanged**. Zero risk to existing PWA install base.
* No multi-SW complexity. Only one origin-level SW exists at any time.
* The smallest possible delta between today's behavior and Phase 3 activation.

### Negative

* Users cannot install `encoreos-public` as a standalone PWA. (Not a stated requirement; revisit if it becomes one.)
* The kiosk experience loses the precache and offline behavior that ADR-013 provides for the authenticated app. Since kiosks are wall-powered and on a known network, this is acceptable. If a specific kiosk deployment ever needs offline, that's a future ADR amendment scoped to that one route.

### Mitigations

* Add a startup-time assertion in `encoreos-public`'s entry script that warns if a SW is somehow already registered for the origin (e.g., a stale install from before activation) and unregisters it. Keeps users from getting stuck on stale chunks during the rollout window.
* The Phase 3 cutover plan (`MICROFRONTENDS_RUNBOOK.md` step 4) explicitly soaks the preview deployment for ≥24 hours to surface any cross-zone SW interactions before production cutover.

## Related Documents

* [`ADR-013`](ADR-013-pwa-strategy.md) — preserved; this ADR amends only the multi-zone scenario
* [`ADR-008`](ADR-008-vite-spa-vs-ssr.md) — preserved
* [`docs/development/MICROFRONTENDS_RUNBOOK.md`](../../development/MICROFRONTENDS_RUNBOOK.md) — operational runbook for activation
* `docs/recommendations/DEV_VELOCITY_AND_ARCHITECTURE_PLAN_2026-04-22.md` § 3.5 — the original problem statement (lands via PR #137)
* [Vercel Microfrontends docs — local development](https://vercel.com/docs/microfrontends/local-development)
* [Workbox — registering a service worker with scope](https://developer.chrome.com/docs/workbox/understanding-the-service-worker-lifecycle/)
