87 lines
2.7 KiB
Markdown
87 lines
2.7 KiB
Markdown
# D1 → Postgres migration — simple runbook
|
|
|
|
_Last updated: 2026-06-29._
|
|
|
|
The happy path for moving a hosted instance from D1 to Postgres. For the full
|
|
detail — what the script converts, the low-downtime delta sync, cutover and
|
|
rollback — see
|
|
[d1-to-postgres-detailed.md](./d1-to-postgres-detailed.md).
|
|
|
|
> **Scope:** this runbook is for the OpenSEO production deployment —
|
|
> `pnpm deploy:postgres` is hardwired to alchemy stage `hosted-prod`, its domains, and
|
|
> `.env.production`. The alchemy self-host path (non-`prod` stages) has no
|
|
> Hyperdrive wiring, so Postgres is not currently available to self-hosters.
|
|
|
|
> Switching `DATABASE_PROVIDER` to `postgres` changes where the app reads and
|
|
> writes — it does **not** move existing data. This copies the data. D1 is never
|
|
> written to, so rollback is just flipping the provider back to `d1`.
|
|
|
|
## 1. Credentials → `.env.local`
|
|
|
|
The migration script auto-loads `.env.local` (no inline env vars needed):
|
|
|
|
```sh
|
|
CLOUDFLARE_ACCOUNT_ID=...
|
|
CLOUDFLARE_API_TOKEN=... # needs D1 read
|
|
POSTGRES_DATABASE_URL=postgres://user:pass@host:5432/db
|
|
# CLOUDFLARE_D1_DATABASE_ID=... # optional; otherwise read from wrangler.jsonc
|
|
```
|
|
|
|
## 2. Create the Postgres schema
|
|
|
|
Provision an empty Postgres, then apply the schema:
|
|
|
|
```sh
|
|
pnpm db:migrate:pg
|
|
```
|
|
|
|
## 3. Dry run (read-only)
|
|
|
|
Reports per-table row counts and writes nothing:
|
|
|
|
```sh
|
|
pnpm exec tsx scripts/migrate-d1-to-postgres.ts --dry-run
|
|
```
|
|
|
|
## 4. Migrate
|
|
|
|
```sh
|
|
pnpm exec tsx scripts/migrate-d1-to-postgres.ts
|
|
```
|
|
|
|
Confirm it ends with **"All row counts match."** (Re-runnable; it aborts if the
|
|
target already has data — pass `--allow-nonempty` to override.)
|
|
|
|
## 5. Catch-up sync (recommended)
|
|
|
|
Right before cutover, run the script again with `--update` to copy anything
|
|
written or changed since the bulk copy (new signups, fresh rank checks, etc.) —
|
|
cheap insurance that nothing was missed:
|
|
|
|
```sh
|
|
pnpm exec tsx scripts/migrate-d1-to-postgres.ts --update
|
|
```
|
|
|
|
Confirm **"All row counts match."** (A small mismatch from rows _deleted_ in D1
|
|
during the window is expected — see the detailed runbook.)
|
|
|
|
## 6. Cut over
|
|
|
|
Point the deployment at Postgres (`DATABASE_PROVIDER=postgres` plus a Hyperdrive
|
|
binding — the app only connects to Postgres through Hyperdrive) and deploy, then
|
|
smoke-test (load a project, save a keyword, check billing):
|
|
|
|
```sh
|
|
pnpm deploy:postgres
|
|
```
|
|
|
|
> **Optional — freeze writes** for a perfectly consistent snapshot: pause the
|
|
> rank-check cron / put the app in a read-only window from the dry run through
|
|
> cutover. Not required — the catch-up sync in step 5 covers writes made during a
|
|
> live copy.
|
|
|
|
## Rollback
|
|
|
|
Set `DATABASE_PROVIDER=d1` (remove the Postgres binding) and redeploy. D1 still
|
|
holds the original data, untouched.
|