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

16 KiB
Raw Blame History

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 finaljust 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 12 semanas
Fase 1 fase-1-auth-colectivo.md 23 semanas
Fase 2a fase-2a-lista-compra-crud.md 2 semanas
Fase 2b fase-2b-realtime-modo-compra.md 23 semanas
Fase 3 fase-3-tareas-notas.md 12 semanas
Fase 4 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

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.