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.com4. 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.