- 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>
373 lines
16 KiB
Markdown
373 lines
16 KiB
Markdown
### 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:**
|
||
```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.
|
||
|
||
---
|