# 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 --- ## Índice de Fases | Fase | Archivo | Duración estimada | |------|---------|-------------------| | Fase 0 | [fase-0-infraestructura.md](fase-0-infraestructura.md) | 1–2 semanas | | Fase 1 | [fase-1-auth-colectivo.md](fase-1-auth-colectivo.md) | 2–3 semanas | | Fase 2a | [fase-2a-lista-compra-crud.md](fase-2a-lista-compra-crud.md) | 2 semanas | | Fase 2b | [fase-2b-realtime-modo-compra.md](fase-2b-realtime-modo-compra.md) | 2–3 semanas | | Fase 3 | [fase-3-tareas-notas.md](fase-3-tareas-notas.md) | 1–2 semanas | | Fase 4 | [fase-4-busqueda-pulido.md](fase-4-busqueda-pulido.md) | 1 semana | **Total estimado: 9–13 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) │ ├── 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.) │ ├── supabase/ │ ├── migrations/ # migraciones SQL versionadas │ ├── seed.sql # datos de prueba para dev │ ├── 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 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 # restaura un dump concreto en supabase just restore keycloak # restaura un dump concreto en keycloak just logs # docker compose logs -f (prod) ``` --- ## 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 | 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.*