- 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>
16 KiB
Fase 0 — Infraestructura y Entornos
Duración estimada: 1–2 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:
# 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:
# 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:
# 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):
RESEND_API_KEY=...
EMAIL_FROM=noreply@tudominio.com
Backups (solo prod):
# 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.ymlcon 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
- PostgreSQL 15 — puerto
keycloak/realm-export.json— realmcolectivopreconfigurado 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)
- Client
- SvelteKit dev server en puerto
5173, apuntando a variables de.env.dev supabase/seed.sql— crea los usuarios de prueba enauth.userscon los mismos UUIDs que Keycloak, un colectivo de ejemplo, listas e ítems- Generación automática de tipos TypeScript:
supabase gen types typescript Justfileconjust dev,just db-reset,just db-types,just kc-export,just kc-open
Usuarios de prueba en Keycloak (dev):
| Usuario | 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.jsonversionado. Al hacerjust devarrancan 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.ymlexistente:- 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
colectivodesde dev (just kc-export) e importarlo en el Keycloak de producción via Admin Console — sin--import-realmen prod .env.productionen 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 webpara 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:
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:
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):
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 3600sen 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-srcdebe incluir tantohttps://comowss://paraapi.tudominio.com— sin elwss://el Service Worker no puede abrir la conexión WebSocket.- Los headers
X-Forwarded-HostyX-Forwarded-Porten 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 PRdeploy.yml: build de imagen SvelteKit → GitHub Container Registry → deploy vía SSH en merge amain- Secrets de GitHub:
SSH_KEY,VPS_HOST,GHCR_TOKEN
0.5 Monorepo
- Turborepo configurado con pipeline
build → test → deploy packages/typescomo paquete internoapps/webconsumiendopackages/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:
# 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_dumpcon formato custom (-Fc) para compatibilidad conpg_restore - Comprime con gzip
- Guarda en
$BACKUP_LOCAL_DIR/<db>/<tipo>/ - Si
BACKUP_B2_ENABLED=true, sube el fichero a B2 conrcloneob2 cli - Imprime checksum SHA256 del fichero generado
infra/scripts/backup-cron.sh — wrapper para cron, decide el tipo automáticamente:
# 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:
# 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_restoresobre el contenedor correcto - Reinicia los servicios tras la restauración
- Imprime resumen: tablas restauradas, filas por tabla
Crontab en el host
# 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_DIRcon permisos correctos - Implementar
backup.shcon soporte para supabase y keycloak, compresión y subida opcional a B2 - Implementar
backup-cron.shcon lógica de rotación y purga por tipo - Implementar
restore.shcon 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, ejecutarrestore.shen 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.