Two new sections, both hooked into the precedence-aware features store:
* /settings — "Visible sections" block with one toggle per SectionKey
bound to setUserFeature(). When a collective-level override is
present the toggle is disabled and a one-line "Hidden by the
collective" hint replaces the visible state — making it obvious
why the per-user toggle is moot.
* /collective/manage — "Collective sections" block (admin-only,
inside the existing isAdmin guard) with one toggle per SectionKey
bound to setCollectiveFeature(). Flipping any of them propagates
to every member in realtime via the publication added in the
previous commit.
Both helpers fall back to the default-ON semantics of section_enabled
when the JSONB key is missing, so untouched rows render exactly as
before fase 12.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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 generateSW + manifest + iconos placeholder + Kong rate-limit en invitaciones + rotación JWT dev + rotate-jwt.sh + .env.erosi.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. |
| Fase 7 — Collective-flow E2E tests | ✅ Completa (12 Playwright nuevos O-01..MC-05; 3 bugs latentes corregidos: RPC atómica create_collective, restore de pendingInvitationToken tras login, hidratación tardía de $currentCollective en manage; migración 013 trigger role/aud default). |
| Fase 8 — Prod auth hardening | ✅ Completa (migración 014 extiende role/aud guard a UPDATE; logout() RP-initiated Keycloak end-session + ruta pública /logged-out). |
| MVP2 — Fase 9 → 14 | 📋 Planificada (rama mvp2) |
| ↳ Fase 9 — Polish + Dark Mode | 📋 |
| ↳ Fase 10 — Spec completion | 📋 |
| ↳ Fase 11 — Item tags + importance UX | 📋 |
| ↳ Fase 12 — Section visibility | 📋 |
| ↳ Fase 13 — Server administration | 📋 (depende de Fase 12) |
| ↳ Fase 14 — PWA hardening | 📋 (depende de Fase 9.3) |
| Suite de tests (pgTAP + Vitest + Playwright) | ✅ 259 tests verdes (45 pgTAP + 140 integración + 15 unit + 59 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í:
X.0 Tests primero— se escriben los tests (Vitest + pgTAP + Playwright) que definen el éxito de la fase, antes de cualquier implementación.X.1 … X.N— implementación en el orden que mejor sirva a los tests.X.Z Verificación final—just test-alldebe 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 | 1–2 semanas |
| Fase 1 | fase-1-auth-colectivo.md | 2–3 semanas |
| Fase 2a | fase-2a-lista-compra-crud.md | 2 semanas |
| Fase 2b | fase-2b-realtime-modo-compra.md | 2–3 semanas |
| Fase 3 | fase-3-tareas-notas.md | 1–2 semanas |
| Fase 4 | fase-4-busqueda-pulido.md | 1 semana |
| Fase 5 | fase-5-mobile-ux.md | 1–2 semanas |
| Fase 6 | fase-6-deploy-prep.md | 3–5 días |
| Fase 7 | fase-7-collective-flow-tests.md | 1 semana |
| MVP2 — Fase 9 | fase-9-polish-dark-mode.md | 1–2 semanas |
| MVP2 — Fase 10 | fase-10-spec-completion.md | 2–3 semanas |
| MVP2 — Fase 11 | fase-11-item-tags-importance.md | 1–2 semanas |
| MVP2 — Fase 12 | fase-12-section-visibility.md | 1 semana |
| MVP2 — Fase 13 | fase-13-server-admin.md | 2–3 semanas |
| MVP2 — Fase 14 | fase-14-pwa-hardening.md | 3–5 días |
Total estimado MVP (Fase 0–6): 11–16 semanas. MVP2 (Fase 9–14, rama mvp2): 10–14 semanas. Fase 7+8 entre MVP y MVP2 como cleanup post-launch.
Los mockups de referencia viven en 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
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 | 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 elsubde Keycloak). - El
subdel realm de Keycloak es un UUID v4 estándar — verificado en las pruebas de aceptación. - No es necesario gestionar
JWT_SECRETmanualmente 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-webconfidencial: 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
openidpersonalizado en Keycloak: GoTrue v2.158.1 envíascope=profile emaila Keycloak ignorandoGOTRUE_EXTERNAL_KEYCLOAK_SCOPES. El endpoint userinfo de Keycloak requiereopeniden el token. Solución: scope de cliente personalizado llamadoopenidconinclude.in.token.scope=trueconfigurado como scope por defecto encolectivo-web. GOTRUE_DISABLE_SIGNUP: "false": El flag bloquea también la creación de usuarios vía OIDC. Debe serfalse; Keycloak es el guardián del registro.auth.userspre-sembrado con UUIDs de Keycloak:seed.sqlinsertaauth.users(coninstance_id='00000000-0000-0000-0000-000000000000'y campos de token como strings vacíos, no NULL) yauth.identities. Garantiza queauth.uid()coincida con las claves foráneas en tablas públicas.- Kong 2.8 no interpola variables de entorno:
infra/kong-start.shejecutaperlpara sustituir${VAR}enkong.ymlantes 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-configurationdebe ser accesible públicamente para que Supabase pueda validar tokens. No poner Keycloak detrás de autenticación básica. - Habilitar
SSL Required: external requestsen 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
localStoragey se refresca automáticamente; no requiereupdateToken()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_SLOTSen PostgreSQL yREALTIME_MAX_CONNECTIONSantes 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_conflictspara depuración.
Estimación Total
| Fase | Duración |
|---|---|
| Fase 0 — Infraestructura | 1–2 semanas |
| Fase 1 — Auth y Colectivo | 2–3 semanas |
| Fase 2a — Lista de Compra CRUD | 2 semanas |
| Fase 2b — Realtime y Modo Compra | 2–3 semanas |
| Fase 3 — Tareas y Notas | 1–2 semanas |
| Fase 4 — Búsqueda y Pulido | 1 semana |
| Total | 9–13 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.