# Runbook de corte SQLite → Postgres (Fase 5) Checklist operativo para migrar UN ambiente (dev, luego staging, luego producción -- nunca saltar directo a producción). Usa [`api/scripts/migrate-sqlite-to-postgres.ts`](../api/scripts/migrate-sqlite-to-postgres.ts). ## 0. Antes de empezar - [ ] Postgres del ambiente aprovisionado (`db/provision/`) y migrado **sin** datos de demo: `./db/update.sh all --context-filter='!dev'`. - [ ] Backup fresco del volumen SQLite actual (`data/app.db`, `data/platform.db`) guardado aparte, fuera del volumen que se va a apagar. - [ ] Credenciales `_owner` de los 3 módulos disponibles en el entorno (`DATABASE_URL_PLATFORM_OWNER`, `DATABASE_URL_IAM_OWNER`, `DATABASE_URL_CORE_OWNER`). ## 1. Pre-flight (sin ventana de mantenimiento, se puede correr en caliente) ```bash cd api deno run --allow-ffi --allow-net --allow-read --allow-write --allow-env \ scripts/migrate-sqlite-to-postgres.ts --dry-run \ --app-db=../data/app.db --platform-db=../data/platform.db ``` Si el pre-flight reporta `[FAIL]`, **no continuar** -- corregir los datos en SQLite (duplicados, huérfanas, tenant_id inválido, fechas mal formateadas) y repetir hasta que todo salga `[OK]`. ## 2. Ventana de mantenimiento (corte real) Hoy no existe un "modo mantenimiento" en la app. Opciones, de menor a mayor invasividad: - Parar el contenedor/proceso `api` (nadie puede escribir mientras está abajo -- los fronts mostrarán error de conexión). - Responder 503 temporal en nginx para `/v1/*` mientras se corre el ETL. Elegir una, documentar la hora exacta de inicio. ## 3. Migración ```bash cd api set -a && source /ruta/al/.env.del.ambiente && set +a deno run --allow-ffi --allow-net --allow-read --allow-write --allow-env \ scripts/migrate-sqlite-to-postgres.ts \ --app-db=/ruta/a/app.db --platform-db=/ruta/a/platform.db ``` El script hace: pre-flight → carga (una transacción por base/esquema, todo o nada) → `setval()` de secuencias → verificación de conteos y sumas de dinero. Si CUALQUIER paso falla, no queda un estado a medias en Postgres (la transacción de esa base se revierte completa), pero SQLite sigue siendo la fuente de verdad -- no se ha cortado nada todavía. ## 4. Criterios go/no-go Antes de apuntar la app a Postgres y apagar SQLite: - [ ] El script terminó con `Migración completa y verificada.` (exit code 0). - [ ] Conteos de filas origen=destino en **todas** las tablas listadas (no solo las 9 de ejemplo del script -- ampliar `verifyCounts()` si el ambiente tiene datos en tablas no cubiertas ahí). - [ ] Diferencia de sumas de dinero dentro de la tolerancia ($0.05) -- si no cuadra, decidir explícitamente si se acepta el redondeo REAL→NUMERIC o se investiga antes de continuar. - [ ] Spot-check manual de 2-3 registros conocidos (un trabajador, un preupuesto, un préstamo) comparando app vieja vs. Postgres. Si algo no cuadra: **no cortar**. Volver a levantar la app contra SQLite (no se tocó), investigar, y repetir desde el paso 1 en otro intento. ## 5. Cutover - [ ] Actualizar `DATABASE_URL_*`/`REDIS_URL_*` del servicio `api` a los valores del ambiente Postgres/Redis recién migrado. - [ ] Levantar `api` -- el fail-fast de arranque (`/v1/health`) debe responder `{"ok":true,...}` antes de reabrir tráfico. - [ ] Reabrir tráfico (quitar el 503/levantar el contenedor). - [ ] Login de prueba con un usuario real del ambiente. ## 6. Después del corte - [ ] Conservar el volumen SQLite (`data/`) como respaldo frío por un período de retención definido (ej. 30 días) antes de borrarlo. - [ ] Correr `./db/provision/verify-isolation.sh` contra el ambiente para confirmar que el aislamiento de roles/RLS/ACLs sigue intacto. - [ ] Repetir todo el runbook en el siguiente ambiente (dev → staging → producción), nunca en paralelo.