The drag handle + dndzone + reorderItems batch UPDATE were already wired
on the list detail view (Fase 5.10). One observable contract was missing:
when another client reorders an item, the local realtime UPDATE feed
shallow-merges the new payload onto the existing row but does NOT
re-position the row in the array — so the new order is invisible until
a reload. Fix: sort the `items` array by sort_order after every realtime
UPDATE / INSERT merge. Cheap (n is small per list) and matches the cold-
load order.
E2E (reorder-items.test.ts):
* RO-01: create 3 items, write a new sort_order via the same PATCH the
in-page reorderItems() helper does, reload, assert the reorder
persisted.
* RO-02: dual-context, Ana reorders, Borja sees the new order via
realtime within the suite's 10s window — without this commit's
sort fix the test failed because Borja's DOM order stayed at the
old order.
Drag emulation in headless Chromium is unreliable for svelte-dnd-action
(native HTML5 DnD). Both tests drive the same write path the page uses
(reorderItems = batch PATCH on sort_order) via fetch from inside the
browser context, using the session token the SDK has already stored.
The realtime + reload paths are unchanged.
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.