Files
collective-lists/plan/fase-0-infraestructura.md
Oier Bravo Urtasun f5903ef442 Add planning docs, design system, and Stitch mockups
- CLAUDE.md: project guidance (stack, domain model, auth, i18n, gotchas)
- DESIGN.md: full design system spec derived from Stitch mockups (slate
  monochrome, Inter, shadcn-svelte, screen patterns for all 5 views)
- README.md: development plan with Justfile commands
- analysis/: functional spec (domain model, use cases, business rules)
- plan/: phase-by-phase implementation plans (Fase 0–4)
- .stitch/designs/: generated mockups (lists, shopping session, notes,
  tasks, list detail with frequency chips)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 06:35:07 +02:00

373 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
### Fase 0 — Infraestructura y Entornos
**Duración estimada: 12 semanas**
**Objetivo: tener los dos entornos funcionando antes de escribir producto.**
#### 0.1 Variables de entorno
Todas las variables viven en `.env.dev` (dev) y `.env.production` (prod, fuera del repo). El fichero `.env.example` documenta cada variable con su propósito.
**Keycloak:**
```env
# Credenciales del administrador de Keycloak
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=...
# Realm configurado para la app
KEYCLOAK_REALM=colectivo
# Client OIDC que usa SvelteKit
KEYCLOAK_CLIENT_ID=colectivo-web
KEYCLOAK_CLIENT_SECRET=... # solo en prod; en dev el client es public
# URL base de Keycloak (usada por SvelteKit y Supabase)
# Dev: http://localhost:8080
# Prod: https://auth.tudominio.com
KEYCLOAK_URL=http://localhost:8080
KEYCLOAK_ISSUER=${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}
```
**Supabase:**
```env
# Claves de Supabase (generadas al arrancar; rotar en prod)
SUPABASE_URL=http://localhost:54321
SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=... # solo para Edge Functions y scripts de admin
JWT_SECRET=... # debe coincidir con la firma JWT de Keycloak en prod
# OIDC — Keycloak como proveedor externo de Supabase Auth
SUPABASE_AUTH_EXTERNAL_KEYCLOAK_ENABLED=true
SUPABASE_AUTH_EXTERNAL_KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID}
SUPABASE_AUTH_EXTERNAL_KEYCLOAK_SECRET=${KEYCLOAK_CLIENT_SECRET}
SUPABASE_AUTH_EXTERNAL_KEYCLOAK_URL=${KEYCLOAK_ISSUER}
```
**SvelteKit:**
```env
# Públicas (expuestas al navegador — prefijo PUBLIC_)
PUBLIC_SUPABASE_URL=http://localhost:54321
PUBLIC_SUPABASE_ANON_KEY=...
PUBLIC_KEYCLOAK_URL=http://localhost:8080
PUBLIC_KEYCLOAK_REALM=colectivo
PUBLIC_KEYCLOAK_CLIENT_ID=colectivo-web
# Privadas (solo servidor SvelteKit)
SUPABASE_SERVICE_ROLE_KEY=...
```
**Email (Resend):**
```env
RESEND_API_KEY=...
EMAIL_FROM=noreply@tudominio.com
```
**Backups (solo prod):**
```env
# Volumen local donde se guardan los dumps antes/además de B2
BACKUP_LOCAL_DIR=/mnt/backups # ruta en el host montada como volumen
# Política de retención — número máximo de backups de cada tipo
BACKUP_RETENTION_DAILY=7 # 7 diarios → cubre la última semana
BACKUP_RETENTION_WEEKLY=4 # 4 semanales → cubre el último mes
BACKUP_RETENTION_MONTHLY=3 # 3 mensuales → configurable, por defecto 3 meses
# Subida a Backblaze B2 (opcional pero recomendado como segunda copia)
BACKUP_B2_ENABLED=true
B2_BUCKET=colectivo-backups
B2_KEY_ID=...
B2_APPLICATION_KEY=...
```
#### 0.2 Entorno de desarrollo local
- [ ] `docker-compose.dev.yml` con todos los servicios:
- **PostgreSQL 15** — puerto `5432`
- **Supabase Auth (GoTrue)** — configurado con Keycloak como OIDC externo
- **Supabase Realtime** — puerto `4000`
- **Supabase Storage** — puerto `5000`
- **Kong API Gateway** — puerto `54321` (entrada principal de Supabase)
- **Supabase Studio** — puerto `54323`
- **Keycloak 24** — puerto `8080`, arranca con `--import-realm keycloak/realm-export.json`
- [ ] `keycloak/realm-export.json` — realm `colectivo` preconfigurado con:
- Client `colectivo-web` (public, PKCE habilitado, redirect URIs para dev)
- **5 usuarios de prueba** creados y listos (ver tabla abajo)
- Roles de realm: `app-user` (asignado a todos por defecto)
- [ ] SvelteKit dev server en puerto `5173`, apuntando a variables de `.env.dev`
- [ ] `supabase/seed.sql` — crea los usuarios de prueba en `auth.users` con los mismos UUIDs que Keycloak, un colectivo de ejemplo, listas e ítems
- [ ] Generación automática de tipos TypeScript: `supabase gen types typescript`
- [ ] `Justfile` con `just dev`, `just db-reset`, `just db-types`, `just kc-export`, `just kc-open`
**Usuarios de prueba en Keycloak (dev):**
| Usuario | Email | Contraseña | Rol en colectivo de prueba |
|---------|-------|-----------|---------------------------|
| `ana` | ana@dev.local | `test1234` | Administradora |
| `borja` | borja@dev.local | `test1234` | Miembro |
| `carmen` | carmen@dev.local | `test1234` | Miembro |
| `david` | david@dev.local | `test1234` | Invitado |
| `eva` | eva@dev.local | `test1234` | Sin colectivo (prueba onboarding) |
> Estos usuarios están en el `realm-export.json` versionado. Al hacer `just dev` arrancan disponibles sin configuración manual.
#### 0.3 Entorno de producción (VPS)
> **El entorno de producción ya está montado y dockerizado.** Keycloak, nginx y su PostgreSQL propio ya están operativos. Esta sección cubre únicamente lo que hay que añadir.
- [ ] Incorporar los servicios de Supabase al `docker-compose.prod.yml` existente:
- PostgreSQL dedicado para Supabase — contenedor independiente, volumen propio, **no compartir con Keycloak**
- GoTrue (Auth), Realtime, Storage, Kong en la red interna Docker existente
- Kong expuesto en un puerto interno (ej: `127.0.0.1:54321`) — nginx hace proxy hacia él, no expuesto directamente
- [ ] Añadir el contenedor SvelteKit al compose, escuchando en un puerto interno (ej: `127.0.0.1:3000`) — nginx hace proxy hacia él
- [ ] Configurar nginx externamente (fuera del repo) para los nuevos servicios — ver bloques de ejemplo abajo
- [ ] Exportar el realm `colectivo` desde dev (`just kc-export`) e importarlo en el Keycloak de producción via Admin Console — sin `--import-realm` en prod
- [ ] `.env.production` en el VPS con todas las variables (ver sección 0.1), fuera del repo
- [ ] Scripts de backup — ver sección **0.6 Sistema de Backups** para el detalle completo
- [ ] Script de deploy: `ssh vps "cd /app && git pull && docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d --no-deps web"`
- `--no-deps web` para actualizar solo el contenedor SvelteKit sin reiniciar Supabase ni Keycloak
##### Configuración nginx de referencia
Tres bloques independientes: app, API/Supabase y Keycloak. Ajustar puertos y dominios según la configuración real del VPS.
**`/etc/nginx/sites-available/colectivo-web`** — SvelteKit PWA:
```nginx
server {
listen 443 ssl http2;
server_name tudominio.com;
ssl_certificate /etc/letsencrypt/live/tudominio.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tudominio.com/privkey.pem;
# Headers de seguridad
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy
"default-src 'self'; \
script-src 'self' 'wasm-unsafe-eval'; \
style-src 'self' 'unsafe-inline'; \
connect-src 'self' https://api.tudominio.com wss://api.tudominio.com https://auth.tudominio.com; \
img-src 'self' data: blob:; \
font-src 'self'; \
worker-src 'self' blob:;"
always;
# Service Worker — nunca cacheado por nginx
location = /service-worker.js {
proxy_pass http://127.0.0.1:3000;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Redirección HTTP → HTTPS
server {
listen 80;
server_name tudominio.com;
return 301 https://$host$request_uri;
}
```
**`/etc/nginx/sites-available/colectivo-api`** — Kong/Supabase con WebSocket para Realtime:
```nginx
server {
listen 443 ssl http2;
server_name api.tudominio.com;
ssl_certificate /etc/letsencrypt/live/api.tudominio.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.tudominio.com/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
# Supabase Realtime — WebSocket (wss://api.tudominio.com/realtime/v1/websocket)
# CRÍTICO: sin estos headers el Modo Compra no funciona en producción
location /realtime/ {
proxy_pass http://127.0.0.1:54321/realtime/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # ← habilita upgrade a WebSocket
proxy_set_header Connection "upgrade"; # ← mantiene la conexión viva
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s; # ← evita que nginx cierre conexiones WS inactivas
proxy_send_timeout 3600s;
}
# Resto de la API de Supabase (REST, Auth, Storage)
location / {
proxy_pass http://127.0.0.1:54321;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name api.tudominio.com;
return 301 https://$host$request_uri;
}
```
**`/etc/nginx/sites-available/colectivo-auth`** — Keycloak (si aún no está configurado):
```nginx
server {
listen 443 ssl http2;
server_name auth.tudominio.com;
ssl_certificate /etc/letsencrypt/live/auth.tudominio.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/auth.tudominio.com/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
# Keycloak necesita el host original para construir redirect URIs correctamente
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host; # ← crítico para Keycloak
proxy_set_header X-Forwarded-Port 443; # ← crítico para Keycloak
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_buffer_size 128k; # Keycloak genera headers grandes (tokens)
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
}
server {
listen 80;
server_name auth.tudominio.com;
return 301 https://$host$request_uri;
}
```
> **Notas importantes:**
> - El bloque `/realtime/` debe ir **antes** del bloque `/` en el server de API — nginx aplica la regla más específica primero.
> - `proxy_read_timeout 3600s` en el bloque de Realtime es necesario porque las conexiones WebSocket del Modo Compra son de larga duración. Sin él, nginx cierra la conexión a los 60 segundos (valor por defecto) y el cliente tiene que reconectar continuamente.
> - En el CSP de SvelteKit, `connect-src` debe incluir tanto `https://` como `wss://` para `api.tudominio.com` — sin el `wss://` el Service Worker no puede abrir la conexión WebSocket.
> - Los headers `X-Forwarded-Host` y `X-Forwarded-Port` en Keycloak son obligatorios. Sin ellos, Keycloak construye las redirect URIs con el puerto interno (8080) en lugar del 443, y el flujo OIDC falla.
#### 0.4 CI/CD
- [ ] `ci.yml`: lint + typecheck + tests unitarios en cada PR
- [ ] `deploy.yml`: build de imagen SvelteKit → GitHub Container Registry → deploy vía SSH en merge a `main`
- [ ] Secrets de GitHub: `SSH_KEY`, `VPS_HOST`, `GHCR_TOKEN`
#### 0.5 Monorepo
- [ ] Turborepo configurado con pipeline `build → test → deploy`
- [ ] `packages/types` como paquete interno
- [ ] `apps/web` consumiendo `packages/types`
#### 0.6 Sistema de Backups
##### Estrategia de rotación
Se guardan tres tipos de backup, cada uno con su propia ventana de retención:
| Tipo | Frecuencia | Retención por defecto | Variable de control |
|------|-----------|----------------------|---------------------|
| Diario | Cada día a las 02:00 | 7 copias | `BACKUP_RETENTION_DAILY` |
| Semanal | Lunes a las 03:00 | 4 copias | `BACKUP_RETENTION_WEEKLY` |
| Mensual | Día 1 de cada mes a las 04:00 | 3 copias | `BACKUP_RETENTION_MONTHLY` |
Con los valores por defecto se conservan hasta ~3 meses de historia. Cambiando `BACKUP_RETENTION_MONTHLY=6` se extiende a 6 meses sin tocar nada más.
##### Volumen local
Los dumps se guardan en una estructura de directorios clara en el volumen montado en `BACKUP_LOCAL_DIR`:
```
/mnt/backups/
├── supabase/
│ ├── daily/
│ │ ├── supabase_daily_2026-04-10.sql.gz
│ │ └── supabase_daily_2026-04-11.sql.gz
│ ├── weekly/
│ │ └── supabase_weekly_2026-04-07.sql.gz
│ └── monthly/
│ └── supabase_monthly_2026-04-01.sql.gz
└── keycloak/
├── daily/
├── weekly/
└── monthly/
```
##### Scripts
**`infra/scripts/backup.sh`** — backup manual inmediato de una BD concreta:
```bash
# Uso: ./backup.sh <supabase|keycloak> [daily|weekly|monthly]
# Sin segundo argumento, genera un dump con timestamp libre (para backups manuales pre-deploy)
# Ejemplo: ./backup.sh supabase
# Ejemplo: ./backup.sh keycloak monthly
```
- Conecta al contenedor Docker correcto según el primer argumento
- Ejecuta `pg_dump` con formato custom (`-Fc`) para compatibilidad con `pg_restore`
- Comprime con gzip
- Guarda en `$BACKUP_LOCAL_DIR/<db>/<tipo>/`
- Si `BACKUP_B2_ENABLED=true`, sube el fichero a B2 con `rclone` o `b2 cli`
- Imprime checksum SHA256 del fichero generado
**`infra/scripts/backup-cron.sh`** — wrapper para cron, decide el tipo automáticamente:
```bash
# Lógica de decisión:
# - Si es día 1 del mes → tipo = monthly
# - Si es lunes → tipo = weekly
# - En cualquier otro caso → tipo = daily
# Llama a backup.sh para supabase y keycloak secuencialmente
# Tras el backup, llama a la función de purga para eliminar copias antiguas
# según BACKUP_RETENTION_DAILY/WEEKLY/MONTHLY
# Registra resultado en /var/log/backup-colectivo.log
```
**`infra/scripts/restore.sh`** — restaura un dump en una BD:
```bash
# Uso: ./restore.sh <supabase|keycloak> <ruta-al-fichero.sql.gz>
# Ejemplo: ./restore.sh supabase /mnt/backups/supabase/daily/supabase_daily_2026-04-10.sql.gz
```
- Pide confirmación explícita antes de ejecutar (`¿Seguro? Esto sobreescribirá la BD actual [s/N]`)
- Detiene los servicios dependientes de la BD antes de restaurar (GoTrue, Realtime, Kong para supabase)
- Descomprime y ejecuta `pg_restore` sobre el contenedor correcto
- Reinicia los servicios tras la restauración
- Imprime resumen: tablas restauradas, filas por tabla
##### Crontab en el host
```cron
# Backups de producción — colectivo
0 2 * * * /app/infra/scripts/backup-cron.sh >> /var/log/backup-colectivo.log 2>&1
```
Un solo entry en cron. El script decide internamente si es daily, weekly o monthly.
##### Tareas de la Fase 0
- [ ] Crear volumen Docker (o directorio montado) en `BACKUP_LOCAL_DIR` con permisos correctos
- [ ] Implementar `backup.sh` con soporte para supabase y keycloak, compresión y subida opcional a B2
- [ ] Implementar `backup-cron.sh` con lógica de rotación y purga por tipo
- [ ] Implementar `restore.sh` con confirmación, parada de servicios y resumen post-restauración
- [ ] Configurar crontab en el host del VPS
- [ ] Verificar backup completo: ejecutar `backup.sh supabase`, comprobar fichero generado, ejecutar `restore.sh` en un entorno de prueba
- [ ] Verificar purga: crear backups de prueba con fechas antiguas, ejecutar `backup-cron.sh`, confirmar que se eliminan los que exceden la retención
**Criterio de aceptación:** `just dev` levanta todo el stack en < 3 minutos. Los 5 usuarios de prueba pueden hacer login con Keycloak y llegar a la app. Deploy a producción funciona con un `git push`. Un backup manual de supabase y keycloak genera ficheros válidos que se restauran correctamente.
---