Files
motor-de-encuestas/sprints/sprint-3-infra/ETAPA_3I_PROMPT.md
2026-06-10 08:59:50 -03:00

17 KiB

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:
  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):

# 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: <ANON_KEY>"
# 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 <container> 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):
    docker exec -it supabase-db psql -U postgres -d postgres
    
  2. Verificar que la DB está vacía:
    \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:
    # 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:<POSTGRES_PASSWORD>@<VPS_IP>:5432/postgres"
    
    # O usar supabase CLI con link:
    supabase link --project-ref <self-hosted-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:

-- 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:
    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:
    #!/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:
      \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://<VPS_IP>: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):
    # Desde la máquina del dev:
    ssh -L 3001:localhost:3001 usuario@<VPS_IP>
    # Después abrir http://localhost:3001 en el navegador
    

Gate de validación:

# API por dominio con SSL
curl -s https://db.inqualityhq.com/rest/v1/ -H "apikey: <ANON_KEY>"
# 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://<VPS_IP>: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:
    grep -rE "supabase\.co|<cloud-project-ref>" --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):
    # 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_URLhttps://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ó:
    -- 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):
    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).