Files
collective-lists/README.md
Oier Bravo Urtasun 2db5aaac14 Fase 6: deploy readiness (PWA + invitation rate-limit + JWT rotation)
Closes the deferrals from Fase 4 that gated a public deploy of the MVP.

PWA install
- Custom SW at apps/web/src/sw.ts (named sw.ts, not service-worker.ts, so
  SvelteKit doesn't intercept it and block Workbox imports).
- vite.config.ts switched to injectManifest strategy with precache of the
  built shell; no runtime caching of Supabase (offline writes stay on the
  existing $lib/sync pending_ops queue).
- Root +layout.svelte onMount() calls registerSW({ immediate: true }) from
  virtual:pwa-register — the plugin does not auto-register.
- Web manifest with 192/512/512-maskable icons + iOS meta tags
  (apple-mobile-web-app-capable, apple-touch-icon, etc.) in app.html.
- Placeholder icons generated via ImageMagick; replace before public launch
  (noted in apps/web/static/icons/README.md).

Kong rate-limit
- /rest/v1/collective_invitations POST: 20/hour per Authorization header.
  Anti-spam on invitation creation is the only rate-limit that survived —
  auth rate-limits were dropped during execution (see plan §6.3): Keycloak
  already provides bruteForceProtected=true on the realm, and stacking a
  Kong limit on /auth/v1/token throttled legitimate PKCE code exchanges
  during E2E.
- Added 'rate-limiting' to KONG_PLUGINS in docker-compose.dev.yml.
- Discovered: strip_path=true on a full-table path sends "/" upstream and
  PostgREST rejects POST to root with PGRST117. Route carries a
  request-transformer plugin that rewrites the URI back.

JWT rotation + prod template
- infra/scripts/rotate-jwt.sh signs anon + service_role JWTs with a fresh
  48-byte secret via openssl. Prints three values for the operator to
  paste; does not touch files.
- Dev secret rotated as a dry-run. Updated both .env (local, gitignored)
  and apps/web/.env.development. Existing sessions are invalidated — all
  users must re-login once.
- .env.production.example committed with placeholders + rotation notes.

Lighthouse + RLS audit tooling
- `just lighthouse` boots the prod build + Supabase stack and runs
  lighthouse CLI against PWA/A11y/Best-Practices.
- `just rls-audit` runs infra/scripts/rls-audit.sh which signs an Eva JWT
  (non-member) and GETs 8 REST endpoints — all must return []. Complements
  the Vitest rls-audit.test.ts suite.

Tests
- 236 green: 34 pgTAP + 140 Vitest integration + 15 unit + 46 Playwright.
- 1 rate-limit test gated behind RUN_RATE_LIMIT_TESTS=1 (Kong counters are
  shared state); run via `just test-rate-limit` which force-recreates Kong
  first to reset counters.
- 3 skipped in `just test-all` (2 Realtime presence upstream bug, 1 gated
  rate-limit).

Deliberately out of scope: push notifications, CI ephemeral Docker, final
production icons, runtime caching of Supabase (SyncBanner + pending_ops
already covers offline).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 04:48:04 +02:00

285 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan de Desarrollo — App Gestión del Colectivo
> **Stack:** SvelteKit + Supabase self-hosted + PWA
> **Infraestructura:** Docker Compose (dev local + producción VPS)
> **Última actualización:** Abril 2026
---
## Estado actual
| Fase | Estado |
|------|--------|
| Fase 0 — Infraestructura | ✅ Completa |
| Fase 1 — Auth y Colectivo | ✅ Completa |
| Fase 2a — Lista de Compra CRUD | ✅ Completa |
| Fase 2b — Realtime y Modo Compra | ✅ Completa (presence bloqueado por bug upstream de `supabase/realtime`) |
| Fase 3 — Tareas y Notas | ✅ Completa |
| Fase 4 — Búsqueda y Pulido | ✅ Búsqueda + RLS audit completos; PWA install/rate-limit/JWT rotation cerrados en Fase 6 |
| Fase 5 — Rediseño UX Mobile | ✅ Completa en scope automatizable (shell + masthead + big-button create + row redesign + selection mode + sticky detail chrome); presence avatars en cards + M-swipe/SEL-long-press touch E2E diferidos a WebKit install |
| Fase 6 — Deploy readiness | ✅ Completa (PWA injectManifest + manifest + iconos placeholder + Kong rate-limit en invitaciones + rotación JWT dev + rotate-jwt.sh + `.env.production.example` + rls-audit.sh + `just lighthouse`). Rate-limit auth retirado — Keycloak brute-force nativa cubre el caso. Push notifications y iconos finales fuera de scope. |
| Suite de tests (pgTAP + Vitest + Playwright) | ✅ 236 tests verdes (34 pgTAP + 140 integración + 15 unit + 46 E2E) + 1 rate-limit gated (`just test-rate-limit`). 3 skipped en `just test-all`. |
---
## Metodología por fase (TDD)
Desde la Fase 1 en adelante, cada fase está estructurada así:
1. **`X.0 Tests primero`** — se escriben los tests (Vitest + pgTAP + Playwright) que definen el éxito de la fase, **antes** de cualquier implementación.
2. **`X.1 … X.N`** — implementación en el orden que mejor sirva a los tests.
3. **`X.Z Verificación final`** — `just test-all` debe devolver 0 failures. Sólo entonces la fase se considera cerrada.
Las fases ya cerradas (0, 1, 2a) reflejan este patrón retroactivamente: la sección `X.0` enumera los tests que hoy cubren la fase, y `X.Z` confirma que `just test-all` pasa.
## Índice de Fases
| Fase | Archivo | Duración estimada |
|------|---------|-------------------|
| Fase 0 | [fase-0-infraestructura.md](plan/fase-0-infraestructura.md) | 12 semanas |
| Fase 1 | [fase-1-auth-colectivo.md](plan/fase-1-auth-colectivo.md) | 23 semanas |
| Fase 2a | [fase-2a-lista-compra-crud.md](plan/fase-2a-lista-compra-crud.md) | 2 semanas |
| Fase 2b | [fase-2b-realtime-modo-compra.md](plan/fase-2b-realtime-modo-compra.md) | 23 semanas |
| Fase 3 | [fase-3-tareas-notas.md](plan/fase-3-tareas-notas.md) | 12 semanas |
| Fase 4 | [fase-4-busqueda-pulido.md](plan/fase-4-busqueda-pulido.md) | 1 semana |
| Fase 5 | [fase-5-mobile-ux.md](plan/fase-5-mobile-ux.md) | 12 semanas |
| Fase 6 | [fase-6-deploy-prep.md](plan/fase-6-deploy-prep.md) | 35 días |
**Total estimado: 1116 semanas (con Fase 6 deploy prep)**
Los mockups de referencia viven en [`design/`](design/) (organizados por pantalla — ver `design/slate_collective/DESIGN.md` para la dirección "Monolith Editorial").
---
## Stack Tecnológico Confirmado
| Capa | Tecnología | Justificación |
|------|-----------|---------------|
| Frontend / PWA | SvelteKit 2 | Reactividad nativa, bundle pequeño, PWA de primera clase |
| Backend / BaaS | Supabase self-hosted | Realtime, RLS, Storage — Auth delegada a Keycloak |
| Identidad / Auth | Keycloak self-hosted | IdP OIDC/OAuth2, gestión de usuarios, federación futura |
| Base de datos | PostgreSQL 15 (vía Supabase) | Full-text search nativo, `pg_cron` para jobs |
| Tiempo real | Supabase Realtime (WebSocket) | Presencia + cambios en < 1s sin infraestructura extra |
| Offline | IndexedDB vía `idb` | PWA-compatible, sin dependencias nativas |
| PWA | `@vite-pwa/sveltekit` + Workbox | Service Worker, caché offline, instalable |
| Infraestructura dev | Docker Compose + Supabase CLI | Entorno local con Studio + Keycloak + usuarios de prueba |
| Infraestructura prod | Docker Compose + nginx (existente) | Reverse proxy ya operativo en el VPS, gestionado externamente |
| CI/CD | GitHub Actions | Lint, test, deploy vía SSH |
| Email transaccional | Resend | SMTP para invitaciones (Keycloak también lo usa para reset) |
| Backups | `pg_dump` → Backblaze B2 (S3) | Retención 30 días, coste mínimo |
---
## Estructura del Repositorio
```
colectivo/
├── apps/
│ └── web/ # SvelteKit — web + PWA
│ ├── src/
│ │ ├── lib/
│ │ │ ├── supabase.ts # cliente Supabase singleton (PKCE, GoTrue auth)
│ │ │ ├── auth.ts # login() / logout() via supabase.auth.signInWithOAuth
│ │ │ ├── stores/ # Svelte stores globales
│ │ │ ├── sync/ # lógica offline + cola de operaciones
│ │ │ └── components/ # componentes UI reutilizables
│ │ ├── routes/ # rutas SvelteKit
│ │ └── service-worker.ts # SW personalizado (Workbox)
│ ├── tests/
│ │ ├── fixtures/
│ │ │ ├── users.ts # constantes de usuarios semilla
│ │ │ └── global-setup.ts # login por usuario vía Keycloak, cachea storageState
│ │ └── e2e/ # tests Playwright (A, C, D series)
│ ├── static/
│ │ ├── manifest.webmanifest
│ │ └── icons/ # iconos PWA (512, 192, 180...)
│ └── vite.config.ts
├── packages/
│ ├── types/ # tipos TypeScript compartidos
│ │ └── src/
│ │ ├── database.ts # tipos generados desde schema Supabase
│ │ └── domain.ts # tipos de dominio (Colectivo, Item, etc.)
│ └── test-utils/ # infraestructura de tests de integración
│ └── src/
│ ├── seed-constants.ts # UUIDs fijos de usuarios y colectivo de prueba
│ ├── supabase-clients.ts # createClientAs(userId) + createAdminClient()
│ └── db-helpers.ts # sql() vía pg.Pool (bypassa RLS para setup/teardown)
├── supabase/
│ ├── migrations/ # migraciones SQL versionadas
│ ├── seed.sql # datos de prueba para dev
│ ├── tests/ # tests pgTAP (SQL puro, sin mocking)
│ ├── functions/ # Edge Functions (invitaciones, etc.)
│ └── config.toml # configuración Supabase CLI + OIDC Keycloak
├── keycloak/
│ ├── realm-export.json # realm "colectivo" exportado — importado al arrancar dev
│ └── themes/ # tema personalizado (opcional, v2)
├── infra/
│ ├── docker-compose.dev.yml # Supabase + Keycloak dev (Kong en puerto 8001)
│ ├── docker-compose.prod.yml # producción en VPS
│ ├── kong.yml # Kong declarativo (rutas API, con ${VAR} placeholders)
│ ├── kong-start.sh # entrypoint Kong: sustituye ${VAR} vía perl antes de arrancar
│ ├── db-init/
│ │ └── 00-role-passwords.sh # ajusta passwords de roles internos de Supabase al arrancar
│ └── scripts/
│ ├── backup.sh # backup manual de una BD concreta
│ ├── backup-cron.sh # ejecutado por cron: decide tipo (diario/semanal/mensual) y purga
│ ├── restore.sh # restaura un dump concreto en una BD
│ └── deploy.sh # SSH + docker compose pull + up
├── .github/
│ └── workflows/
│ ├── ci.yml # lint + test en PR
│ └── deploy.yml # deploy en push a main
├── .env.example # todas las variables documentadas con valores de ejemplo
├── Justfile # comandos de desarrollo frecuentes
├── turbo.json # Turborepo pipeline
└── package.json # workspaces
```
---
## Justfile — Comandos de referencia
```bash
just setup # preparación del entorno local (idempotente, seguro relanzar)
just dev # levanta docker-compose.dev.yml + SvelteKit dev server
just serve # build + servidor Node prod en :3000 contra el stack Docker dev
just stop # para todos los servicios (Docker stack + servidor Vite)
just dev-stop # para solo el stack de Docker
just dev-clean # para Docker y elimina todos los volúmenes (reset completo)
just db-reset # drop + migrate + seed (dev)
just db-migrate # aplica migraciones pendientes (dev)
just db-seed # aplica supabase/seed.sql a la BD dev en marcha
just db-types # genera tipos TS desde schema Supabase → packages/types
just kc-export # exporta realm de Keycloak → keycloak/realm-export.json
just kc-open # abre Keycloak Admin Console en el navegador (localhost:8080)
just deploy # ejecuta infra/scripts/deploy.sh (prod)
just backup supabase # backup manual inmediato de supabase
just backup keycloak # backup manual inmediato de keycloak
just restore supabase <file> # restaura un dump concreto en supabase
just restore keycloak <file> # restaura un dump concreto en keycloak
just logs # docker compose logs -f (prod)
just test-all # ejecuta todas las suites (pgTAP + Vitest + Playwright); requiere just dev activo
just test-integration # Vitest: tests de integración RLS (requiere just dev activo)
just test-db # pgTAP: tests SQL directamente contra la BD dev
just test-e2e # Playwright: tests E2E en modo headless (requiere just dev activo)
just test-e2e-headed # Playwright: tests E2E con navegador visible (debugging)
```
---
## Endpoints y credenciales en desarrollo local
| Servicio | URL | Credenciales (solo dev) |
|----------|-----|-------------------------|
| App SvelteKit | http://localhost:5173 | — |
| Supabase Studio | http://localhost:54323 | — |
| Supabase API (Kong) | http://localhost:8001 | apikey: `PUBLIC_SUPABASE_ANON_KEY` del `.env` |
| Keycloak Admin | http://localhost:8080/admin | `admin` / `admin` |
| PostgreSQL | localhost:5432 | `postgres` / `postgres` |
**Secretos dev (generados en `.env`, nunca usar en producción):**
| Variable | Valor dev |
|----------|-----------|
| `POSTGRES_PASSWORD` | `postgres` |
| `SUPABASE_JWT_SECRET` | `super-secret-jwt-token-with-at-least-32-characters-long` |
| `KEYCLOAK_ADMIN` / `KEYCLOAK_ADMIN_PASSWORD` | `admin` / `admin` |
| `KEYCLOAK_CLIENT_SECRET` | `gotrue-dev-secret` (secreto del cliente `colectivo-web` en GoTrue) |
**Usuarios de prueba** (definidos en `keycloak/realm-export.json` y `supabase/seed.sql`):
| Usuario | Email | Contraseña | Rol en el colectivo de prueba |
|---------|-------|-----------|-------------------------------|
| `ana` | ana@dev.local | `test1234` | Admin |
| `borja` | borja@dev.local | `test1234` | Member |
| `carmen` | carmen@dev.local | `test1234` | Member |
| `david` | david@dev.local | `test1234` | Guest |
| `eva` | eva@dev.local | `test1234` | (sin colectivo — para testing de onboarding) |
---
## Decisiones Previas al Inicio
Antes de arrancar la Fase 0, confirmar:
| Decisión | Estado |
|----------|--------|
| Estrategia JWT Keycloak → Supabase (Opción A vs B) | ✅ **Opción B — GoTrue como proxy OIDC** |
| Base de datos de Keycloak en prod | ✅ **PostgreSQL independiente** |
| Entorno de producción dockerizado | ✅ **Ya operativo** |
| Reverse proxy en producción | ✅ **nginx existente, gestionado externamente** |
| Self-registration habilitado en Keycloak o invitación obligatoria | ✅ **Self-registration habilitado en Keycloak** |
| Invitaciones: email o link manual | ✅ **Link-only — admin genera link y lo comparte manualmente** |
| Proveedor SMTP | ✅ **No necesario para invitaciones** — Resend queda solo para Keycloak reset-password si se activa |
| Almacenamiento de backups | Pendiente — **Backblaze B2 recomendado** |
| Dominio y DNS del VPS | Pendiente |
| Subdominio para Keycloak | Pendiente — `auth.tudominio.com` recomendado |
| Registry de imágenes Docker | Pendiente — **GitHub Container Registry recomendado** |
| OAuth Google (federated en Keycloak) | Pendiente — configurar en Keycloak, no en Supabase |
| OAuth Apple (federated en Keycloak) | Pendiente — requiere Apple Developer Program ($99/año) |
---
## Advertencias Técnicas
**Keycloak y JWT con Supabase RLS (Opción B — GoTrue como proxy OIDC):**
- GoTrue recibe el token de Keycloak, lo valida contra el discovery endpoint de Keycloak, y emite su propio JWT de Supabase. `auth.uid()` resuelve al UUID de GoTrue (mapeado desde el `sub` de Keycloak).
- El `sub` del realm de Keycloak es un UUID v4 estándar — verificado en las pruebas de aceptación.
- No es necesario gestionar `JWT_SECRET` manualmente con la clave pública de Keycloak. GoTrue usa el endpoint OIDC de Keycloak para validar tokens.
- El logout (`supabase.auth.signOut()`) invalida la sesión de GoTrue. La sesión SSO de Keycloak persiste hasta que expire — comportamiento esperado para esta app.
**Integración GoTrue + Keycloak — requisitos no obvios (ya implementados):**
- **Cliente `colectivo-web` confidencial:** GoTrue necesita un secreto de cliente (`KEYCLOAK_CLIENT_SECRET=gotrue-dev-secret`) para el intercambio de código. Sin esto, GoTrue falla con "missing OAuth secret".
- **Scope `openid` personalizado en Keycloak:** GoTrue v2.158.1 envía `scope=profile email` a Keycloak ignorando `GOTRUE_EXTERNAL_KEYCLOAK_SCOPES`. El endpoint userinfo de Keycloak requiere `openid` en el token. Solución: scope de cliente personalizado llamado `openid` con `include.in.token.scope=true` configurado como scope por defecto en `colectivo-web`.
- **`GOTRUE_DISABLE_SIGNUP: "false"`:** El flag bloquea también la creación de usuarios vía OIDC. Debe ser `false`; Keycloak es el guardián del registro.
- **`auth.users` pre-sembrado con UUIDs de Keycloak:** `seed.sql` inserta `auth.users` (con `instance_id='00000000-0000-0000-0000-000000000000'` y campos de token como strings vacíos, no NULL) y `auth.identities`. Garantiza que `auth.uid()` coincida con las claves foráneas en tablas públicas.
- **Kong 2.8 no interpola variables de entorno:** `infra/kong-start.sh` ejecuta `perl` para sustituir `${VAR}` en `kong.yml` antes de arrancar Kong. Sin esto, Kong carga la cadena literal y rechaza todas las peticiones con 401.
**Keycloak self-hosted en producción:**
- Keycloak 24+ requiere mínimo 512MB de RAM asignados. En un VPS pequeño (2GB), monitorizar el consumo conjunto con Supabase.
- El endpoint `/realms/{realm}/.well-known/openid-configuration` debe ser accesible públicamente para que Supabase pueda validar tokens. No poner Keycloak detrás de autenticación básica.
- Habilitar `SSL Required: external requests` en el realm de producción. Nunca desactivarlo.
**OAuth federado (Google, Apple):**
- Con Keycloak, Google y Apple se configuran como Identity Providers en Keycloak, no en Supabase. El flujo es: usuario → Keycloak → Google/Apple → Keycloak → app. Supabase solo ve el token de Keycloak.
- Esto simplifica la configuración de Supabase pero requiere configurar los Identity Providers en Keycloak Admin Console.
**iOS Safari y PWA:**
- La autenticación usa el flujo PKCE de Supabase OAuth (`signInWithOAuth({ provider: 'keycloak' })`). No hay keycloak-js ni iframes de refresco silencioso — compatible con Safari de base.
- La sesión de GoTrue se persiste en `localStorage` y se refresca automáticamente; no requiere `updateToken()` manual.
- Background Sync API no está soportada en Safari. Implementar fallback manual con el evento `online`.
- Push notifications solo disponibles desde iOS 16.4 y únicamente con la PWA instalada en pantalla de inicio.
- Probar el Modo Compra en un iPhone real en paralelo al desarrollo, no solo en DevTools.
**Supabase Realtime self-hosted:**
- Revisar `MAX_REPLICATION_SLOTS` en PostgreSQL y `REALTIME_MAX_CONNECTIONS` antes de producción.
- Las suscripciones de Presence consumen más recursos que Postgres Changes. Limitar a listas con sesión activa.
**Conflictos de sincronización:**
- Last-write-wins es suficiente para este dominio. No implementar CRDT en el MVP.
- Registrar conflictos detectados en tabla `sync_conflicts` para depuración.
---
## Estimación Total
| Fase | Duración |
|------|----------|
| Fase 0 — Infraestructura | 12 semanas |
| Fase 1 — Auth y Colectivo | 23 semanas |
| Fase 2a — Lista de Compra CRUD | 2 semanas |
| Fase 2b — Realtime y Modo Compra | 23 semanas |
| Fase 3 — Tareas y Notas | 12 semanas |
| Fase 4 — Búsqueda y Pulido | 1 semana |
| **Total** | **913 semanas** |
---
*Plan generado como base para Claude Code. Stack: SvelteKit + Supabase self-hosted + Keycloak + PWA. Cada fase puede usarse como contexto independiente para sesiones de trabajo.*