New plan file closing out the Fase 4 deferrals with specifics confirmed
by user on 2026-04-14:
- 6.1 PWA injectManifest, MINIMAL scope (precache shell only; Supabase
GETs stay network-only since $lib/sync + SyncBanner already cover
offline writes + the offline indicator)
- 6.2 manifest.webmanifest + neutral placeholder icons (192/512/180 +
maskable 512) + iOS meta tags
- 6.3 Kong rate-limiting — 10/min + 60/hour on /auth/v1/token per IP,
30/min on /auth/v1/authorize per IP, 20/hour on invitations POST
per authenticated user
- 6.4 JWT rotation — dev rotate now + .env.production.example template
+ infra/scripts/rotate-jwt.sh
- 6.5 Lighthouse PWA ≥ 90 + scripted rls-audit.sh (curl edition of
Vitest's rls-audit.test.ts)
- 6.Z verification
Explicitly out of scope (recorded at bottom of the plan):
- Push notifications (no current use case)
- Ephemeral-Docker CI (deferred)
- Runtime caching of Supabase in the SW (deferred)
- Final production icon art (placeholders land now; real art before
public launch)
README phase table + index updated; plan total now 11–16 weeks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
11 KiB
Fase 6 — Deploy readiness (PWA + rate-limit + secret rotation)
Estado: ⏳ Propuesta Duración estimada: 3–5 días Objetivo: cerrar los ítems diferidos de Fase 4 que bloquean un despliegue público del MVP.
Esta fase cierra los ítems que en Fase 4 se dejaron para "sprint de deploy": PWA real (SW custom, manifest, iconos), rate-limiting en Kong, rotación de JWT, y validación Lighthouse + auditoría RLS manual. Notificaciones push se retiran explícitamente del scope (decisión del 2026-04-14; sin caso de uso actual).
Sigue el patrón TDD donde el cambio es programático (rate-limit, SW registration, manifest served). La auditoría Lighthouse + el chequeo manual de rotación quedan como tareas documentadas — no son test automatizables.
6.0 Tests primero — escribir antes de implementar
Vitest (packages/test-utils/tests/):
rate-limit.test.ts— carga el plugin de Kong con POST a/auth/v1/token11 veces seguidas desde la misma IP, espera429en el intento 11. Segunda prueba: 21 POSTs a/rest/v1/collective_invitationsdesde el mismo usuario autenticado en menos de una hora, espera429.
Playwright (apps/web/tests/e2e/):
pwa.test.ts:- P-01:
manifest.webmanifestaccesible en/manifest.webmanifestcon status 200 yapplication/manifest+jsoncontent-type; contienename,short_name,start_url,display: standalone, al menos un icono 192×192 y uno 512×512. - P-02: tras login, un
navigator.serviceWorker.getRegistration()en la página devuelve un SW registrado y conactive.state === 'activated'.
- P-01:
Manual / scripted (Justfile):
just lighthouse— arranca el build de producción local, corre Lighthouse CLI contrahttp://localhost:3000, exige score PWA ≥ 90, score Accessibility ≥ 90, score Best Practices ≥ 90. Falla el recipe si alguno cae. Se ejecuta después dejust build + just serve.
6.1 PWA injectManifest migration (scope: minimal)
Decisión de scope: estrategia minimal confirmada por el usuario:
- Precache del shell + assets generados por Vite (JS/CSS/fonts/iconos)
- Sin runtime caching de Supabase — las rutas
/rest/v1/*siguen netowrk-only - Las escrituras offline ya las cubre
$lib/sync(pending_opsIndexedDB) +onlineevent flush
Cambios:
- Crear
apps/web/src/sw.ts(noservice-worker.ts— SvelteKit intercepta ese nombre y bloquea imports externos; ver gotcha #10 en CLAUDE.md):/// <reference lib="webworker" /> import { precacheAndRoute } from 'workbox-precaching'; declare const self: ServiceWorkerGlobalScope; precacheAndRoute(self.__WB_MANIFEST); // skip waiting on first install so updates land without a double reload self.addEventListener('install', () => void self.skipWaiting()); self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim())); - Actualizar
apps/web/vite.config.ts— cambiar@vite-pwa/sveltekitastrategies: 'injectManifest':SvelteKitPWA({ strategies: 'injectManifest', srcDir: 'src', filename: 'sw.ts', injectManifest: { globPatterns: ['client/**/*.{js,css,ico,png,svg,webp,woff,woff2}'], }, manifest: { … }, // see 6.2 devOptions: { enabled: true, type: 'module', navigateFallback: '/' } }) - Retirar el comentario en
vite.config.tsque justificagenerateSW(ya no aplica). - Añadir
workbox-precachingcomo dep deapps/websi no lo trae ya@vite-pwa/sveltekit. - Probar con
just build + just serveque/sw.jsse sirve y precachea el manifest.
Criterio de aceptación: navigator.serviceWorker.getRegistration() devuelve un SW activo tras cargar la app en una build de producción. La app carga offline sin errores visibles (los fetch de Supabase fallan graciosamente y la SyncBanner aparece).
6.2 Manifest + iconos + meta tags iOS
apps/web/static/manifest.webmanifest(o dejar queSvelteKitPWAlo genere desde la config):{ "name": "Colectivo", "short_name": "Colectivo", "description": "Gestión colaborativa del hogar", "theme_color": "#0f172a", "background_color": "#f7f9fb", "display": "standalone", "orientation": "portrait", "start_url": "/", "scope": "/", "icons": [ { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "/icons/icon-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] }- Iconos placeholder —
apps/web/static/icons/:icon-192.png,icon-512.png,icon-512-maskable.png,icon-180.png(iOS touch icon)- Placeholder neutro: fondo
#0f172a, glyph "C" blanca centrada. Generar vía ImageMagick / script eninfra/scripts/generate-placeholder-icons.sh. Documentar enstatic/icons/README.mdque deben reemplazarse antes del lanzamiento público.
- Añadir meta tags iOS en
src/app.html:<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-title" content="Colectivo" /> <link rel="apple-touch-icon" href="/icons/icon-180.png" />
Criterio de aceptación: en Lighthouse (just lighthouse) la categoría "Installability" pasa sus auditorías (manifest válido, iconos ≥ 192 y ≥ 512, SW registrado, start_url responde 200).
6.3 Kong rate-limiting
Límites confirmados:
| Endpoint | Límite | Ventana |
|---|---|---|
/auth/v1/token |
10 | 1 min / IP |
/auth/v1/token |
60 | 1 hora / IP |
/auth/v1/authorize |
30 | 1 min / IP |
/rest/v1/collective_invitations POST |
20 | 1 hora / usuario autenticado |
Kong ships with rate-limiting plugin; política local (sin Redis) es suficiente para single-instance. Config en infra/kong.yml:
plugins:
- name: rate-limiting
service: auth-token-login
config:
minute: 10
hour: 60
policy: local
fault_tolerant: true
- name: rate-limiting
service: auth-authorize
config:
minute: 30
policy: local
- name: rate-limiting
route: invitations-post
config:
hour: 20
policy: local
limit_by: consumer # requires JWT claim-to-consumer mapping
- Actualizar
infra/kong.ymlcon las rutas específicasauth-token-loginyauth-authorize(hoy ambas viven bajo un único servicio/auth/v1/*con path matching). Separar en routes diferenciadas. - Para
limit_by: consumeren invitaciones — Kong necesita resolver el JWTsuba un consumer. La alternativa simple:limit_by: headerusandoAuthorization(se comporta como limit-per-token, que en PKCE rota cada hora pero sirve). - Tests en
packages/test-utils/tests/rate-limit.test.tsverifican que la respuesta 11/21 es 429 con headersX-RateLimit-Remaining. - Restart de kong:
docker compose -f infra/docker-compose.dev.yml up -d --force-recreate kong.
Criterio de aceptación: los tests pasan; el 11º login desde la misma IP devuelve 429; la 21ª invitación del mismo usuario devuelve 429.
6.4 Rotación de JWT + template de producción
Dev rotation:
- Generar nuevo
SUPABASE_JWT_SECRET(≥ 32 chars):openssl rand -base64 48 | tr -d '\n' - Firmar nuevas
ANON_KEYySERVICE_ROLE_KEYcon ese secret (payloadsrole=anon/role=service_role,iss=supabase-demo,expfuturo). Script sugerido:infra/scripts/rotate-jwt.shque toma el nuevo secret y emite los tres valores. - Actualizar ambos ficheros a la vez (memory
project_env_keys_trap):- root
.env→SUPABASE_JWT_SECRET,PUBLIC_SUPABASE_ANON_KEY,SUPABASE_SERVICE_ROLE_KEY apps/web/.env.development→PUBLIC_SUPABASE_ANON_KEY
- root
docker compose up -d --force-recreate kong auth rest realtime.- Sesiones existentes quedan invalidadas — todos los usuarios de dev tienen que re-loguearse una vez. Documentado en el commit message.
Production template (no commit de secretos reales):
- Crear
apps/web/.env.production.exampley.env.production.example(root) con:# Regenerate every 90 days (or on security incident). # See infra/scripts/rotate-jwt.sh for the generation procedure. SUPABASE_JWT_SECRET=<openssl rand -base64 48 | tr -d '\n'> PUBLIC_SUPABASE_ANON_KEY=<sign with above> SUPABASE_SERVICE_ROLE_KEY=<sign with above> … - Añadir
infra/scripts/rotate-jwt.sh— bash script que:- genera un nuevo secret
- usa
node/pythonpara firmar los tres JWT (anon,service_role, optionallyauthenticatedfor tests) - imprime los valores para que el operador los pegue en el
.envde prod - no toca ficheros — el operador decide qué hacer con ellos
- Documentar el procedimiento al final de
CLAUDE.mdo en un nuevoinfra/README.md: cuándo rotar (cadencia + incidentes), cómo, qué contenedores reiniciar, qué sesiones se invalidan.
Criterio de aceptación: .env.production.example existe con placeholders + comentarios. infra/scripts/rotate-jwt.sh genera un trio válido (verificable firmando un token y descifrándolo). El stack dev sigue verde tras la rotación (cambiar el secret y que just test-all vuelva a verde confirma que no hay valores hardcodeados sueltos).
6.5 Lighthouse + auditoría RLS manual
just lighthouserecipe:lighthouse: pnpm --filter @colectivo/web build (pnpm --filter @colectivo/web run preview --port 3000 &) ; sleep 3 pnpm exec lighthouse http://localhost:3000 \ --only-categories=pwa,accessibility,best-practices \ --output=html --output-path=./lighthouse-report.html \ --chrome-flags='--headless' pkill -f 'vite preview --port 3000'- Añadir
lighthousecomo devDep enapps/web. - Documentar umbrales: PWA ≥ 90, Accessibility ≥ 90, Best Practices ≥ 90.
- RLS curl audit — script
infra/scripts/rls-audit.shque firma un JWT como Eva y hace GET a/rest/v1/shopping_items,/rest/v1/shopping_lists,/rest/v1/tasks,/rest/v1/notes,/rest/v1/collectives— todos deben devolver[]. Es la versión manual del test de integraciónrls-audit.test.ts.
Criterio de aceptación: Lighthouse report archivado en lighthouse-report.html con los tres scores ≥ 90. rls-audit.sh imprime "OK" en todas las tablas.
6.Z Verificación final
just test-all→ 0 failures (incluye los nuevos de 6.0: PWA install + rate-limit).just lighthouse→ PWA ≥ 90.infra/scripts/rls-audit.sh→ OK en todos los endpoints.- Manual — PWA instalable en iOS Safari (Add to Home Screen) y Android Chrome (prompt).
- Commit con rotación de JWT + recordatorio en el mensaje sobre re-login obligatorio.
Criterio de aceptación: todos los tests pasan; Lighthouse verde; la app se puede instalar; RLS auditado. Listo para desplegar a VPS con just deploy.
Scope explícito fuera
- Notificaciones push — retiradas del plan (decisión 2026-04-14, sin caso de uso actual).
- CI con Docker efímero corriendo
just test-all— diferido (prioridad menor ahora que el deploy está cerca). - Runtime caching de Supabase en el SW — diferido (la SyncBanner + pending_ops cubre el MVP offline story).
- Iconos finales de producción — placeholders esta vez; reemplazar antes del lanzamiento.