# ETAPA 3I — Migración a Supabase self-hosted en Dokploy ## 1. Objetivo Migrar de Supabase Cloud a Supabase self-hosted en Dokploy, manteniendo Cloud operativo en paralelo durante la transición y como respaldo durante 30 días post-cierre. **Esta etapa es operativa, no de código. Nivel 3 extendido por tocar infraestructura.** --- ## 2. Modo de ejecución — diferente a etapas anteriores Esta etapa **NO se ejecuta de una tirada**. Son 6 pasos atómicos, cada uno con su gate de validación. Después de cada paso, el agente reporta lo verificado y **espera confirmación explícita del director** antes de avanzar al siguiente. Si un paso falla, el rollback es el del propio paso (documentado). No avanzar al siguiente hasta que el actual cierre limpio. --- ## 3. DECISIONES YA TOMADAS — no consultar de nuevo - Supabase self-hosted **completo** (Postgres + Auth + Storage + Realtime + Studio + Kong), no Postgres pelado. Compatibilidad 100% con código actual. - **Partida limpia:** no se migran datos de Cloud. Se aplican las 12 migraciones del repo sobre una DB vacía. No hay nada en Cloud que conservar (testing ya truncado en 2E). - **Cloud y self-hosted en paralelo** durante la transición. Cloud se mantiene 30 días post-validación como respaldo, después se elimina. - **Studio detrás de SSH** (no expuesto a internet sin auth). API por subdominio propio con SSL. - **Backups locales** en el mismo VPS (diarios, retención 30 días, con restore probado). Backup remoto queda como deuda técnica antes del primer dato real. - **VPS objetivo:** el mismo VPS de Dokploy donde corre la app Next.js (17 GB RAM disponibles, según notas del proyecto). - **Dominio nuevo:** `db.inqualityhq.com` o `supabase.inqualityhq.com` para el API (decidir cuál en el paso 4). Studio sin dominio público. --- ## 4. PASOS ### PASO 1 · Levantar Supabase self-hosted en Dokploy **Qué hacer:** 1. Acceder a Dokploy. 2. Crear un nuevo proyecto "supabase-prod" (o nombre equivalente). 3. Usar el template oficial de Supabase self-hosted (Dokploy lo tiene como template prefijado, o se carga el `docker-compose.yml` oficial de Supabase desde https://supabase.com/docs/guides/self-hosting/docker). 4. Generar los secrets críticos **localmente, no usar los defaults del template:** - `POSTGRES_PASSWORD` — generar con `openssl rand -base64 32` - `JWT_SECRET` — generar con `openssl rand -base64 64` - `ANON_KEY` y `SERVICE_ROLE_KEY` — generar a partir del JWT_SECRET con la herramienta oficial: https://supabase.com/docs/guides/self-hosting/docker#generate-api-keys - `DASHBOARD_USERNAME` y `DASHBOARD_PASSWORD` — generar fuerte (16+ caracteres) 5. **NO usar las claves de ejemplo** que vienen en la documentación de Supabase. Esas están publicadas y son inseguras. 6. Configurar las variables de entorno del stack en Dokploy. 7. Iniciar el stack. 8. Esperar que todos los contenedores estén `healthy`. **Gate de validación (verificar TODO antes de avanzar al paso 2):** ```bash # Mostrame el output de estos checks: docker ps --filter "label=com.docker.compose.project=supabase-prod" \ --format "table {{.Names}}\t{{.Status}}" # Esperado: ~8 contenedores, todos "Up (healthy)" o "Up" # Postgres responde docker exec supabase-db psql -U postgres -c 'SELECT version();' # Esperado: versión de Postgres # Kong responde curl -s http://localhost:8000/rest/v1/ -H "apikey: " # Esperado: respuesta JSON (no 404, no 502) # Studio responde localmente (todavía no por dominio) curl -s -o /dev/null -w "%{http_code}" http://localhost:3001 # Esperado: 200 o 401 (auth requerida) ``` **Qué reportar:** - Confirmación de los 4 checks arriba con outputs reales. - **NUNCA pegar los secrets en el reporte.** Mostrar solo que existen, no su valor. Ejemplo: `JWT_SECRET configurado (longitud: 64 chars) ✓` - Recursos consumidos: `docker stats --no-stream` con los 8 contenedores. **Rollback si falla:** - Detener y borrar el stack en Dokploy. - Investigar logs: `docker logs ` del que falle. - Volver a empezar el paso 1. **DETENERSE. Esperar confirmación del director antes del paso 2.** --- ### PASO 2 · Aplicar las 12 migraciones del repo **Qué hacer:** 1. Conectarse al Postgres self-hosted desde el VPS (no desde fuera): ```bash docker exec -it supabase-db psql -U postgres -d postgres ``` 2. Verificar que la DB está vacía: ```sql \dt public.* -- Esperado: 0 tablas (o solo las del seed de Supabase, no las del proyecto) ``` 3. Salir del psql. 4. Desde el directorio del repo del proyecto (en local del dev, no en el VPS), aplicar las migraciones usando la CLI de Supabase apuntada a la nueva DB: ```bash # Configurar conexión a la nueva DB self-hosted # (la DB URL incluye el password generado en paso 1) export SUPABASE_DB_URL="postgresql://postgres:@:5432/postgres" # O usar supabase CLI con link: supabase link --project-ref --db-url $SUPABASE_DB_URL # Aplicar migraciones supabase db push --db-url $SUPABASE_DB_URL ``` **Alternativa si la CLI tiene problemas:** copiar el contenido de cada archivo `supabase/migrations/*.sql` y ejecutarlo manualmente con `psql`, en orden de timestamp. **Gate de validación:** ```sql -- Conteo de objetos creados (esperado: 12 tablas + N funciones) SELECT count(*) FROM pg_tables WHERE schemaname='public'; -- Esperado: ≥10 tablas (surveys, responses, answers, consent_records, questions, etc.) SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname='public' AND p.proname LIKE 'rpc_%'; -- Esperado: 9 funciones rpc_* (las del dashboard de Etapa 2E) -- Seed cargado SELECT count(*) FROM public.surveys; -- Esperado: 1 (la encuesta piloto) SELECT count(*) FROM public.questions; -- Esperado: 44 SELECT count(*) FROM public.question_options; -- Esperado: ≥200 (las opciones de las preguntas single/multiple choice) -- RLS habilitada SELECT tablename FROM pg_tables WHERE schemaname='public' AND rowsecurity = false; -- Esperado: vacío (todas las tablas con RLS habilitada) -- Test funcional: una RPC del dashboard devuelve suppressed con N=0 SELECT rpc_dashboard_kpis('30000000-0000-0000-0000-000000000001'::uuid, '{}'::jsonb); -- Esperado: status='suppressed' ``` **Qué reportar:** - Outputs de los 6 queries arriba. - Si algún número está fuera de lo esperado: detener, no continuar. **Rollback si falla:** - `TRUNCATE` no aplica todavía (no hay nada que conservar). - Borrar la DB y recrearla: ```bash docker exec supabase-db psql -U postgres -c 'DROP DATABASE postgres;' # Reiniciar el contenedor para que reaplique el init ``` - Reaplicar migraciones desde cero. **DETENERSE. Esperar confirmación del director antes del paso 3.** --- ### PASO 3 · Backups locales con prueba de restore **Qué hacer:** 1. Crear directorio de backups en el VPS: `mkdir -p /var/backups/supabase-prod && chmod 700 /var/backups/supabase-prod` 2. Crear script de backup `/usr/local/bin/supabase-backup.sh`: ```bash #!/bin/bash set -euo pipefail TS=$(date +%Y%m%d_%H%M%S) OUT=/var/backups/supabase-prod/supabase_$TS.sql.gz docker exec supabase-db pg_dump -U postgres postgres | gzip > "$OUT" chmod 600 "$OUT" # Retención 30 días find /var/backups/supabase-prod -name "supabase_*.sql.gz" -mtime +30 -delete echo "Backup OK: $OUT ($(du -h "$OUT" | cut -f1))" ``` 3. `chmod +x /usr/local/bin/supabase-backup.sh` 4. Probar manualmente: ejecutarlo una vez, verificar que el archivo se crea. 5. Agregar a crontab: `0 3 * * * /usr/local/bin/supabase-backup.sh >> /var/log/supabase-backup.log 2>&1` 6. **Prueba de restore obligatoria:** - Crear una DB temporal de prueba: `docker exec supabase-db createdb -U postgres restore_test` - Restaurar el último backup en esa DB: `gunzip < /var/backups/supabase-prod/supabase_*.sql.gz | docker exec -i supabase-db psql -U postgres restore_test` - Verificar que tiene los mismos objetos que la DB principal: ```sql \c restore_test SELECT count(*) FROM public.questions; -- Esperado: 44 (mismo que la principal) ``` - Eliminar la DB de prueba: `docker exec supabase-db dropdb -U postgres restore_test` **Gate de validación:** - Script ejecutable y verificado manualmente. - Backup más reciente: `ls -lh /var/backups/supabase-prod/` muestra al menos un archivo. - Cron line confirmada: `crontab -l | grep supabase-backup` - Restore probado: la DB de prueba mostró el conteo correcto antes de eliminarse. **Qué reportar:** - Tamaño del primer backup (informativo: nos da una idea de cuánto va a crecer). - Confirmación de los 4 puntos del gate. **Rollback si falla:** - Eliminar el script y el cron. - Re-investigar y volver a configurar. **Deuda técnica explícita** (agregar a TECH_DEBT.md como TECH-DEBT-006): > Backup remoto pendiente. Hoy solo local en el mismo VPS. Si el VPS muere, se pierden > los datos. **Bloqueante antes del primer dato real de empresa.** Recomendación: > rsync nocturno a S3-compatible (Backblaze B2 o Wasabi, ~$5/mes). **DETENERSE. Esperar confirmación del director antes del paso 4.** --- ### PASO 4 · Dominio + SSL para API, Studio detrás de SSH **Qué hacer:** 1. **Decidir nombre del subdominio para API.** Recomendación: `db.inqualityhq.com`. 2. Crear el registro DNS A apuntando al IP del VPS (esto se hace en el panel de DNS del dominio, fuera de Dokploy). 3. En Dokploy, configurar el reverse proxy / Traefik: - Ruta `db.inqualityhq.com` → contenedor `supabase-kong:8000`. - SSL automático con Let's Encrypt. 4. Esperar a que el certificado SSL se emita (1-5 min normalmente). 5. **Bloquear Studio del exterior:** - El contenedor `supabase-studio` (puerto 3001) NO debe tener regla de exposición pública en Dokploy/Traefik. - Verificar con: `curl http://:3001` desde fuera del VPS → debe **fallar** (connection refused o timeout). 6. **Acceso a Studio vía SSH tunnel** (documentar el comando para el director): ```bash # Desde la máquina del dev: ssh -L 3001:localhost:3001 usuario@ # Después abrir http://localhost:3001 en el navegador ``` **Gate de validación:** ```bash # API por dominio con SSL curl -s https://db.inqualityhq.com/rest/v1/ -H "apikey: " # Esperado: respuesta JSON, no error de certificado # SSL válido curl -vI https://db.inqualityhq.com/ 2>&1 | grep "SSL certificate verify" # Esperado: "SSL certificate verify ok" # Studio NO accesible desde fuera curl -s -o /dev/null -w "%{http_code}" http://:3001 --max-time 5 # Esperado: timeout o connection refused (no 200) # Studio sí accesible vía túnel SSH (documentar instrucciones para el director) ``` **Qué reportar:** - URL del API funcionando con SSL. - Confirmación de que Studio no está expuesto. - Comando exacto para que el director acceda a Studio por SSH tunnel. **Rollback si falla:** - Quitar el registro DNS. - Quitar la regla de Traefik. - Volver a usar IP+puerto temporalmente. **DETENERSE. Esperar confirmación del director antes del paso 5.** --- ### PASO 5 · Cambiar variables de entorno de la app Next.js + deploy **ESTE ES EL PASO MÁS CRÍTICO. El error más fácil de cometer es apuntar la app a la DB nueva pero dejar alguna variable apuntando a Cloud — comportamiento mixto, datos inconsistentes.** **Qué hacer:** 1. Listar TODAS las variables de entorno de la app actual relacionadas con Supabase: ``` NEXT_PUBLIC_SUPABASE_URL NEXT_PUBLIC_SUPABASE_ANON_KEY SUPABASE_SERVICE_ROLE_KEY (server-only) IP_HASH_SALT (independiente, no tocar) ``` Verificar que **no hay otras** referencias a Cloud en el código: ```bash grep -rE "supabase\.co|" --include="*.ts" --include="*.tsx" \ --include="*.env*" . # Esperado: vacío o solo en comentarios ``` 2. Generar un snapshot de las variables ACTUALES (sin pegarlas en el reporte): ```bash # En el dashboard de Dokploy de la app Next.js, exportar las env vars actuales # Guardarlas en un archivo local seguro como backup ``` 3. Actualizar en Dokploy las 3 variables: - `NEXT_PUBLIC_SUPABASE_URL` → `https://db.inqualityhq.com` - `NEXT_PUBLIC_SUPABASE_ANON_KEY` → la nueva (de paso 1) - `SUPABASE_SERVICE_ROLE_KEY` → la nueva (de paso 1) 4. Trigger redeploy de la app Next.js. 5. Esperar que termine el build y arranque. **Gate de validación:** - App Next.js arranca sin errores (`docker logs` del contenedor de la app). - Abrir `https://economia-cuidado.inqualityhq.com` — el consent screen carga. - En la consola del navegador, verificar que las requests van a `db.inqualityhq.com`, NO a `*.supabase.co`. - Verificar que NO hay errores 401/403 en el Network tab del navegador. **Qué reportar:** - Confirmación de que la app arranca. - Captura de Network tab mostrando requests a `db.inqualityhq.com`. - Confirmación de que no hay errores en consola del navegador. **Rollback si falla:** - Volver las 3 variables a los valores anteriores (los del archivo backup del punto 2). - Trigger redeploy. - La app vuelve a apuntar a Cloud. **Cloud sigue vivo, así que rollback es completo.** **DETENERSE. Esperar confirmación del director antes del paso 6.** --- ### PASO 6 · Validación funcional end-to-end + ventana de observación **Qué hacer:** 1. Director ejecuta manualmente el flujo completo: - Abre `https://economia-cuidado.inqualityhq.com?s=30000000-0000-0000-0000-000000000001` - Acepta los 2 consentimientos. - Completa el formulario hasta el final. - Envía. - Verifica que llegó al "gracias por participar" o equivalente. 2. Agente verifica en la nueva DB que se persistió: ```sql -- Conectado a la self-hosted DB SELECT count(*) FROM responses WHERE survey_id = '30000000-0000-0000-0000-000000000001'; -- Esperado: ≥1 SELECT count(*) FROM answers a JOIN responses r ON a.response_id = r.id WHERE r.survey_id = '30000000-0000-0000-0000-000000000001'; -- Esperado: ≥1 (varias filas) SELECT count(*) FROM consent_records; -- Esperado: ≥1 -- Verificar qi_zone se popula (Etapa 2E) SELECT qi_zone, count(*) FROM responses GROUP BY qi_zone; -- Esperado: al menos una fila con qi_zone NO NULL ``` 3. Abrir `/dashboard?survey_id=...` y verificar que los widgets muestran suppressed (N<5 todavía). 4. Cargar 4 respuestas más para alcanzar N=5 y verificar que al menos un widget pasa a estado `ok` (validación de que el k-anonimato libera datos al cruzar el umbral). 5. **Ventana de observación de 24-48h:** - Sin tráfico real (todavía no hay piloto), durante este período el director y/o el agente monitorean los logs por errores no esperados. 6. Truncar las respuestas de prueba (igual que en Etapa 2E): ```sql TRUNCATE answers, responses, consent_records RESTART IDENTITY CASCADE; ``` **Gate de validación final:** - Flujo end-to-end funcionó. - Datos llegaron a la DB correcta (self-hosted). - qi_zone se populó (Etapa 2E está funcionando). - Dashboard responde con datos reales (sale de suppressed al cruzar k=5). - 24-48h sin errores en logs. - Tablas truncadas, queda solo el seed. **Qué reportar:** - Capturas del flujo end-to-end completado. - Outputs de los queries de verificación. - Confirmación del período de observación sin errores. - Confirmación de que las tablas se truncaron al final. **DETENERSE. Esperar confirmación del director.** **Recién con esta confirmación, Cloud queda como respaldo congelado por 30 días.** --- ## 5. DoD de la etapa completa - [ ] Los 6 pasos completados con sus gates de validación. - [ ] Director confirmó cada paso antes del siguiente. - [ ] Supabase self-hosted operativo en Dokploy. - [ ] App Next.js apuntando a la nueva DB. - [ ] Backups locales diarios configurados, restore probado. - [ ] Studio no expuesto a internet (acceso solo por SSH tunnel). - [ ] API por subdominio con SSL. - [ ] TECH-DEBT-006 agregado (backup remoto pendiente antes de dato real). - [ ] Cloud sigue operativa pero NO recibe tráfico. Marcada para eliminación a 30 días. ## 6. Qué reportar al final de la etapa ``` Migración Supabase Cloud → self-hosted completada el [fecha]. Self-hosted operativo en: - API: https://db.inqualityhq.com - Studio: solo vía SSH tunnel - Backups: /var/backups/supabase-prod (diarios, retención 30d, restore probado) App Next.js: - Apuntando a self-hosted ✓ - Flujo end-to-end validado ✓ - Sin errores en ventana de observación de [X] horas ✓ Cloud: - Sigue activa, sin tráfico. - Marcada para eliminación: [fecha + 30 días]. Pendiente / deuda técnica: - TECH-DEBT-006: backup remoto antes del primer dato real. Decisiones proactivas (Nivel 2): - [si las hubo] ``` ## 7. Qué NO hacer - ❌ Avanzar al siguiente paso sin confirmación del director. - ❌ Pegar secrets (passwords, keys) en reportes — solo confirmar que existen. - ❌ Eliminar la cuenta de Supabase Cloud antes de los 30 días post-validación. - ❌ Tocar código de la aplicación (esta etapa es 100% infraestructura). - ❌ Usar los secrets de ejemplo de la documentación de Supabase — todos generados localmente. - ❌ Exponer Studio a internet sin auth. - ❌ Saltarse la prueba de restore (un backup sin restore probado no es un backup).