Oier Bravo Urtasun cb07f67a69 feat(realtime): full Fase 2b.0 test suite + infra hardening
Extend the 2b.0 red-phase coverage: presence, RLS isolation, offline-queue
contract, and Playwright scaffolds for the shopping-session UX. Only the
presence test is blocked on an upstream Realtime bug (documented below); all
other Realtime paths are proven end-to-end.

Tests
- realtime-postgres-changes.test.ts: now uses afterEach socket disconnect
  (Vitest singleFork kept Phoenix sockets alive between files, leaking state)
- realtime-isolation.test.ts (new): R-I-01 David (guest, same collective)
  receives events; R-I-02 Eva (non-member) receives NONE — RLS is enforced
  server-side per subscribed JWT
- realtime-presence.test.ts (new, describe.skip): two-client roster + leave
  assertions ready, gated on upstream bug
- sync-queue.test.ts (new, describe.skip): 7 placeholders pinning the
  apps/web/src/lib/sync/queue.ts contract (Q-01..Q-04 enqueue / in-order
  flush / retry; F-01..F-03 online-event flush + last-write-wins)
- apps/web/tests/e2e/{session,realtime,offline}.test.ts (new, describe.skip):
  S-01..S-03, R-E-01, R-E-02, O-01, O-02 pinning the Modo Compra UI contract
- vitest.config.ts: fileParallelism=false, drop singleFork so each file gets
  a fresh worker fork — prevents RealtimeClient singleton leakage

Infra
- db-init/00-role-passwords.sh: pre-create `realtime` schema with
  AUTHORIZATION supabase_admin. Realtime's per-tenant migrator creates tables
  INSIDE the schema but does not create the schema itself; without this the
  first tenant connect fails with "schema realtime does not exist"
- docker-compose.dev.yml: adopt the upstream supabase/supabase Realtime env
  shape — SEED_SELF_HOST=true + RUN_JANITOR=true + RLIMIT_NOFILE=10000 +
  drop the custom `command: eval seeds` (that bypasses the normal supervisor
  startup and was observed to cause GenServer crashes under load).
  Version pinned to v2.76.5 to match the official docker-compose.

Known upstream bug
- Realtime v2.76.5 and v2.83.0 both crash on presence_diff:
  `(UndefinedFunctionError) RealtimeChannel.handle_out/3 is undefined`.
  postgres_changes + isolation unaffected (they don't traverse handle_out).
  Presence tests stay `describe.skip` until a fixed upstream is released.

Totals: 59 Vitest passed (+ 9 skipped awaiting implementation/upstream),
16 pgTAP, 15 Playwright (+ 7 E2E scaffolded for 2b UI) = 90 green.

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

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

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.

Description
No description provided
Readme 4.2 MiB
Languages
HTML 54.5%
TypeScript 20.5%
Svelte 13.7%
PLpgSQL 8.1%
Shell 2.3%
Other 0.9%