Migra a koigrid

Mueve tu stack desde AWS/Vercel/Railway/Render pieza a pieza — cada paso es reversible. Postgres migra casi sin downtime con replicación lógica (CDC): haces el cutover cuando el lag es 0.

1. Postgres

RESTORE GESTIONADO (lo más fácil): sube tu dump y koigrid lo importa — POST /databases/:id/restore-dump/upload-url {gzip:true} → PUT del .sql.gz → POST /databases/:id/restore-dump {dumpKey}, o koigrid db restore-dump <id> --file dump.sql --wait (el CLI comprime y sube en streaming). Pre-siembra las extensiones (arregla el desajuste de extensions.vector de Supabase), falla rápido con la CAUSA RAÍZ, es atómico (un fallo deja la BD intacta, así que el reintento da la misma causa), conserva tu dump para que reintentar no cueste re-subirlo, y da la propiedad a tu rol de app para que los datos se lean desde el primer momento. Alternativa manual: dump + restore. Casi sin downtime: la replicación lógica vuelca tu origen en koigrid en streaming; cutover con lag 0. Para un restore grande, conéctate al puerto DIRECTO de escritura (no al pool de PgBouncer) — el pooling en modo transacción rompe COPY y los restores largos.

# 1) Create the managed database — match your SOURCE major version (14-17) to avoid dump mismatches
koigrid db create prod --version 17
DATABASE_URL="$(koigrid db connection <id> --json | jq -r .connection.uri)"

# --- Simple (dump + restore) ---
# For a BIG restore, connect to the DIRECT write port (connection.writeUri), NOT the pool: transaction
# pooling breaks COPY and long restores (missing tables/indexes).
pg_dump "$SOURCE_URL" | psql "$DATABASE_URL"        # or: drizzle-kit migrate / prisma migrate deploy
# ex-Supabase? install extensions into their schema:
#   POST /databases/:id/extensions {"name":"uuid-ossp","schema":"extensions"}  (koigrid does it as superuser)

# --- Near-zero-downtime (logical replication / CDC) — the managed flow, measured on a real 66 GB RDS ---
# a) SOURCE: logical replication on (RDS: parameter group with rds.logical_replication=1 → one reboot, ~1 min
#    with Multi-AZ failover), a replication role WITH BYPASSRLS and SELECT on every table, and a publication:
#      CREATE ROLE koi_repl LOGIN PASSWORD '…' BYPASSRLS;  GRANT rds_replication TO koi_repl;   -- RDS
#      GRANT SELECT ON ALL TABLES IN SCHEMA public TO koi_repl;
#      ALTER DEFAULT PRIVILEGES FOR ROLE <owner> IN SCHEMA public GRANT SELECT ON TABLES TO koi_repl;
#      CREATE PUBLICATION mig FOR TABLES IN SCHEMA public;
#    and let koigrid's database host reach your source on 5432 (security group / firewall).
# b) copy the SCHEMA to koigrid first (tables must exist), then FREEZE schema changes on the source until cutover:
pg_dump --schema-only --no-owner --no-privileges "$SOURCE_URL" | psql "$DIRECT_URL"
# c) PREFLIGHT — it catches the 4 traps that otherwise give a wrong or stuck copy with no visible error:
#    RLS without BYPASSRLS · NOT VALID CHECKs · schema drift · a role that cannot SELECT a table
koigrid db subs <id> preflight --source "$SOURCE_REPL_URL" --publication mig
# d) subscribe (refused if the preflight blocks). origin none = ready for a two-way rollback later:
koigrid db subs <id> create mig --source "$SOURCE_REPL_URL" --publication mig --origin none
# e) wait for the verdict — read it, do not infer it from the lag (tables copy in parallel, lag reads ~0 meanwhile):
koigrid db subs <id> ls          # → tablas 268/268 · cutover.ready=true
#    Budget DISK: the initial copy writes ~2x your data in WAL; koigrid grows the disk on its own, but plan for it.
# f) VERIFY row by row (the only check that catches a table copied to zero by RLS). It is a job: it counts in
#    batches, each with its own time limit enforced by Postgres, and the CLI waits until it finishes.
#    --count-source = a READ REPLICA of your source, to keep count(*) off your primary:
koigrid db subs <id> verify --source "$SOURCE_REPL_URL" --publication mig --count-source "$SOURCE_REPLICA_URL"
# --- After migrating: automatic password rotation (AWS Secrets Manager rotation substitute) ---
# ZERO-DOWNTIME dual-role rotation. One-time: switch the pooler to reloadable mode, then schedule:
koigrid db rotation <id> migrate                 # brief reconnect, once
koigrid db rotation <id> set --interval 30       # rotate every 30 days (or: koigrid db rotation <id> rotate)
# apps wiring DATABASE_URL='${{db.<name>.DATABASE_URL}}' auto-redeploy with the fresh password.

2. Almacenamiento (S3)

Tu bucket ya habla S3 — copia los objetos con rclone o aws-cli y reapunta el endpoint + claves de tu cliente.

koigrid storage buckets create assets
koigrid storage keys                     # → accessKey / secretKey / endpoint (shown once)

# copy objects from your old S3 (rclone remote 'koigrid' → https://s3.koigrid.com, path-style)
rclone copy s3old:my-bucket koigrid:assets

CERO cambios de código: koigrid soporta virtual-hosted-style (bucket.s3.koigrid.com), así que el AWS SDK estándar funciona tal cual — pon AWS_ENDPOINT_URL_S3 + tus claves como variables de entorno y deja tu código igual:

# Your app's S3 code stays EXACTLY as-is. Just set env vars:
AWS_ENDPOINT_URL_S3=https://s3.koigrid.com
AWS_ACCESS_KEY_ID=<from koigrid storage keys>
AWS_SECRET_ACCESS_KEY=<from koigrid storage keys>
AWS_REGION=us-east-1                # any value; koigrid is region-agnostic

# koigrid supports virtual-hosted-style (bucket.s3.koigrid.com), so the standard
# new S3Client() works with no forcePathStyle and no code change. Use the bucketName
# koigrid returns (koi-<org>-<name>-xxxx) as the Bucket.

3. App

Despliega desde un repo git, una imagen de contenedor, o tu carpeta local (--dir, sin git ni Docker). Fija env vars y luego un dominio propio. El CDN está activo por defecto y cachea tu HTML en el edge honrando el Cache-Control (envía s-maxage=N; el ISR de Next.js ya lo hace) — latencia de borde estilo CloudFront, sin config extra; las apps protegidas y los previews nunca se cachean en el edge.

# from a git repo (add --repo-token for a private repo):
koigrid apps deploy web --repo https://github.com/you/app
# ...or straight from your local folder (no git, no Docker):
koigrid apps deploy web --dir ./

# env vars (invalid lines are reported, not fatal; saved even before the first deploy):
curl -X POST $API/apps/<id>/env -H "Authorization: Bearer koi_KEY" \
  -d '{"env":{"DATABASE_URL":"...","API_KEY":"..."}}'

# custom domain + automatic TLS:
koigrid apps domains web app.yourdomain.com

4. Cron jobs

Tus endpoints de EventBridge/cron pasan a jobs HTTP — koigrid pega a la URL según el cron con tu cabecera secreta.

# an EventBridge/cron endpoint → an HTTP job (koigrid calls the URL on schedule):
koigrid jobs create nightly --schedule "0 3 * * *" \
  --url https://app.yourdomain.com/api/cron/report --header "x-cron-secret: $SECRET"

# ...or a container job:
koigrid jobs create backup --schedule "0 2 * * *" --image alpine --command "sh -c '…'"

5. Cutover

La base de datos es la única puerta de un solo sentido, así que la vuelta atrás se arma ANTES del cambio: una suscripción inversa (koigrid → tu base antigua, origin none) hace que volver atrás no pierda ninguna escritura. Después: parar escrituras, esperar retraso 0, promover koigrid, sincronizar las secuencias y cambiar el tráfico. Conserva lo antiguo 48-72 h.

# 0) BEFORE the night: your app running on koigrid in parallel (pointing at the replica, read-only checks),
#    and the ROLLBACK armed: a publication on koigrid + a subscription on your OLD database, origin none,
#    copy_data false — so every write made on koigrid after the switch also lands on the old one.
koigrid db pub <id> add back --all-tables
#      (on the OLD database)  CREATE SUBSCRIPTION back CONNECTION '<koigrid DIRECT_URL>' PUBLICATION back
#                             WITH (copy_data = false, origin = none, enabled = false);
# 1) stop writes on the old database (maintenance or read-only) and wait for lag 0:
koigrid db subs <id> ls
# 2) PROMOTE koigrid: stop applying the old database's changes, and enable the way back:
koigrid db subs <id> update mig --disable
#      (on the OLD database)  ALTER SUBSCRIPTION back ENABLE;
# 3) sequences are NOT replicated — without this the first INSERT fails with 'duplicate key'. Rehearse, then apply:
koigrid db subs <id> sync-sequences --source "$SOURCE_REPL_URL" --dry-run
koigrid db subs <id> sync-sequences --source "$SOURCE_REPL_URL"
#    and REFRESH MATERIALIZED VIEW where you have them.
# 4) switch the traffic: the origin of your CDN / load balancer → the koigrid app (seconds to minutes, no DNS),
#    or DNS with a low TTL. Watch errors and latency for 30 min.
# ROLLBACK: point the traffic back. The old database already has every write made on koigrid.
# Keep the old stack for 48-72 h before deleting anything. Bandwidth is included and flat.