# Fase 6 — what has been built

## 6.1 PWA `injectManifest` migration

- `apps/web/src/sw.ts` — minimal custom SW: `precacheAndRoute(self.__WB_MANIFEST)` + `skipWaiting`/`clients.claim`. Named `sw.ts` (not `service-worker.ts`) to dodge SvelteKit's built-in interception (see the PWA gotchas in `CLAUDE.md`).
- `apps/web/vite.config.ts` — switched `@vite-pwa/sveltekit` to `strategies: 'injectManifest'` with `srcDir: 'src'`, `filename: 'sw.ts'`, `injectManifest.globPatterns` for the built shell.
- `apps/web/src/routes/+layout.svelte` — registers the SW via `onMount() → import('virtual:pwa-register').registerSW({ immediate: true })`. `@vite-pwa/sveltekit` does NOT auto-register; without this call the virtual module is a no-op and `navigator.serviceWorker.getRegistration()` returns `undefined`.
- `apps/web/tests/e2e/pwa.test.ts` — P-01 (manifest shape) + P-02 (SW registration via `waitForFunction` poll).

> Note: the prod build later switched back to `strategies: 'generateSW'` — see the PWA gotcha for the reason.

## 6.2 Manifest + iOS meta tags + placeholder icons

- Manifest driven by `vite.config.ts` → `SvelteKitPWA({ manifest: {…} })`. Icons declared 192×192, 512×512, 512×512 maskable. `theme_color: #0f172a`, `background_color: #f7f9fb`, `display: standalone`.
- `apps/web/static/icons/` — `icon-192.png`, `icon-512.png`, `icon-512-maskable.png`, `icon-180.png` placeholder PNGs (slate background + "C" glyph). Generator: `infra/scripts/generate-placeholder-icons.sh` (ImageMagick). Replace with final artwork before public launch — noted in `static/icons/README.md`.
- `apps/web/src/app.html` — iOS PWA meta tags: `apple-mobile-web-app-capable`, `apple-mobile-web-app-status-bar-style`, `apple-mobile-web-app-title`, `apple-touch-icon`.

## 6.3 Kong rate-limit (invitations only)

- `infra/kong.yml` — `rest-v1-invitations` route: `POST /rest/v1/collective_invitations`, rate-limit `hour: 20, policy: local, limit_by: header (Authorization)`. `strip_path: true` on a full-table path sends `/` upstream which PostgREST rejects with `PGRST117`, so the route also carries a `request-transformer` plugin that rewrites the URI back to `/collective_invitations`.
- `infra/docker-compose.dev.yml` — `KONG_PLUGINS` env var gains `rate-limiting` (was missing).
- `packages/test-utils/tests/rate-limit.test.ts` — RL-02 only (21st invitation POST = 429). Gated behind `RUN_RATE_LIMIT_TESTS=1` so it's excluded from `just test-all` (Kong counters are shared state). Run via `just test-rate-limit`, which also `--force-recreate`s Kong to reset counters first.
- **Scope dropped during execution:** Kong rate-limit on `/auth/v1/token` + `/auth/v1/authorize` removed. Reasoning: Keycloak already offers native brute-force protection on the realm (`bruteForceProtected: true`, `maxFailureWaitSeconds: 900`, `failureFactor: 30` in `keycloak/realm-export.json`), and stacking Kong on top throttles legitimate PKCE code exchanges during E2E (the same `/auth/v1/token` endpoint handles both password auth and code exchange — Kong can't distinguish). See `plan/fase-6-deploy-prep.md` §6.3 for the full rationale.

## 6.4 JWT rotation + production env template

- `infra/scripts/rotate-jwt.sh` — bash script signing HS256 `anon` + `service_role` JWTs with a freshly-generated 48-byte secret via `openssl dgst -sha256 -hmac + base64url`. Prints the three values (`SUPABASE_JWT_SECRET`, `PUBLIC_SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`) for the operator to paste into `.env`. Does NOT touch files.
- Dev secret rotated as a dry-run of the procedure. `.env` + `apps/web/.env.development` updated together (see `project_env_keys_trap` memory — both files carry `PUBLIC_SUPABASE_ANON_KEY`). Deduped a pre-existing duplicate `PUBLIC_SUPABASE_ANON_KEY=` line in `.env`.
- `.env.production.example` — committed template with placeholders + rotation cadence note.

## 6.5 Lighthouse + RLS audit tooling

- `Justfile` — `just lighthouse` recipe (prod build → node server on :3000 → `lighthouse --only-categories=pwa,accessibility,best-practices` → HTML report). Requires `just dev` to be up so the real Supabase stack is reachable.
- `Justfile` — `just rls-audit` delegates to `infra/scripts/rls-audit.sh`.
- `infra/scripts/rls-audit.sh` — signs an Eva JWT (non-member of the seed collective) with `SUPABASE_JWT_SECRET` and GETs eight REST endpoints (`collectives`, `shopping_lists`, `shopping_items`, `task_lists`, `tasks`, `notes`, `collective_members`, `collective_invitations`). Complements `rls-audit.test.ts`; all 8 must return `[]`.
