Files
collective-lists/README.md
Oier Bravo Urtasun f396897cb5 test: full test suite (Vitest + pgTAP + Playwright) + TDD plan restructure
Add a 4-layer test stack covering RLS, triggers, and UI flows for Fases 0–2a,
then restructure every plan file so future fases start with tests and end with
a verification gate.

Test suite
- packages/test-utils: Vitest integration tests signing HS256 JWTs via jose so
  each test acts as a specific seed user (createClientAs + createAdminClient)
- supabase/tests: pgTAP for accept_invitation(), item_frequency trigger, and
  promote-on-admin-leave; each file self-installs pgtap extension
- apps/web/tests: Playwright E2E with live Keycloak login per test (storageState
  caching doesn't rehydrate Supabase's session state reliably)
- just test-all chains the three suites; test-db forwards POSTGRES_PASSWORD
  as PGPASSWORD with ON_ERROR_STOP=1 so failures abort the chain

Supabase auth gotcha
- PostgREST queries inside onAuthStateChange deadlock on GoTrue's navigator.locks
  auth lock (getAccessToken → getSession → initializePromise waits on the same
  lock that's held during event dispatch). Fix is two defenses: a pass-through
  lock in $lib/supabase, and a setTimeout(0) defer in root +layout.svelte to
  push loadUserCollectives out of the callback's microtask chain. Either alone
  is insufficient; both together unblock the Playwright suite.

Env key rotation
- apps/web/.env.development had a stale demo anon key signed with a different
  secret than root .env; Vite inlined that into the browser bundle so Kong (which
  uses the root .env value) rejected every request with 401. Aligned the two
  files and added a memory entry to flag this for the next rotation.

Plan restructure (TDD)
- Every fase now opens with X.0 Tests primero and closes with X.Z Verificación
  final. Completed fases (0, 1, 2a) show the pattern retroactively with the
  tests that currently cover them; pending fases (2b, 3, 4) list the tests to
  write before implementation.

Docs
- CLAUDE.md status line reports 54 + 12 + 15 = 81 green tests, adds gotchas
  #11 (auth-lock deadlock) and #12 (no storageState caching)
- README.md adds a TDD methodology section and the test-all command
- .gitignore excludes Playwright's generated reports and auth state

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

279 lines
16 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 |
| Suite de tests (integración + pgTAP + E2E) | ✅ Completa — 81 tests verdes (54 Vitest + 12 pgTAP + 15 Playwright), lanzables con `just test-all` |
| Fase 2b — Realtime y Modo Compra | ⏳ Pendiente |
| Fase 3 — Tareas y Notas | ⏳ Pendiente |
| Fase 4 — Búsqueda y Pulido | ⏳ Pendiente |
---
## 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 |
**Total estimado: 913 semanas**
---
## 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.*