Migrate to koigrid

Move your stack from AWS/Vercel/Railway/Render piece by piece — each step is reversible. Postgres migrates with near-zero downtime via logical replication (CDC), so you cut over when lag is 0.

1. Postgres

MANAGED RESTORE (easiest): upload your dump and koigrid imports it — POST /databases/:id/restore-dump/upload-url {gzip:true} → PUT the .sql.gz → POST /databases/:id/restore-dump {dumpKey}, or koigrid db restore-dump <id> --file dump.sql --wait (the CLI gzips and streams it). It pre-seeds extensions (fixes the Supabase extensions.vector mismatch), is fail-fast with the ROOT-CAUSE error, atomic (a failure leaves the DB untouched, so a retry reports the same cause), keeps your dump so a retry costs no re-upload, and hands ownership to your app role so the data is readable right away. Verify with the per-table row counts it returns. DIY alternative: dump + restore. Near-zero-downtime: logical replication streams your source into koigrid; cut over at lag 0. For a big restore, connect to the DIRECT write port (not the PgBouncer pool) — transaction pooling breaks COPY and long restores.

# 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. Storage (S3)

Your bucket already speaks S3 — copy objects with rclone or aws-cli, then repoint your client’s endpoint + keys.

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

ZERO code change: koigrid supports virtual-hosted-style (bucket.s3.koigrid.com), so the standard AWS SDK just works — set AWS_ENDPOINT_URL_S3 + your keys as env vars and keep your code as-is:

# 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

Deploy from a git repo, a container image, or your local folder (--dir, no git/Docker needed). Set env vars, then a custom domain. The CDN is on by default and caches your HTML at the edge honoring Cache-Control (send s-maxage=N; Next.js ISR already does) — CloudFront-style edge latency, no extra config; protected apps and previews are never edge-cached.

# 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

Your EventBridge/cron endpoints become HTTP jobs — koigrid hits the URL on schedule with your secret header.

# 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

The database is the only one-way door, so arm the way back BEFORE the switch: a reverse subscription (koigrid → your old database, origin none) means rolling back loses no writes. Then: stop writes, wait for lag 0, promote koigrid, sync sequences, switch the traffic. Keep the old stack 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.