← back to Open Seo
docs/LOCAL_POSTGRES.md
120 lines
# Running OpenSEO on Postgres locally
OpenSEO runs on **Cloudflare D1 (SQLite) by default**. Postgres is an opt-in
backend for installs that outgrow D1's storage ceiling. The application code is
written once against a provider-aware `db` layer (see `src/db/`), so the only
difference at runtime is the `DATABASE_PROVIDER` flag and a connection string.
This guide sets up a throwaway Postgres in Docker so you can develop and test the
Postgres path locally. **You do not need this for normal development** — D1 is the
default and the path most contributors should use.
## Prerequisites
- Docker Desktop (or Docker Engine)
- The normal local dev setup from [`LOCAL_DEVELOPMENT.md`](./LOCAL_DEVELOPMENT.md)
## 1. Start a Postgres container
Port `5433` is used on the host to avoid clashing with a system Postgres on the
default `5432`.
```sh
docker run --name openseo-postgres \
-e POSTGRES_USER=openseo \
-e POSTGRES_PASSWORD=openseo \
-e POSTGRES_DB=openseo \
-p 5433:5432 \
-d postgres:16
```
Wait until it accepts connections:
```sh
docker exec openseo-postgres pg_isready -U openseo -d openseo
```
The connection string is:
```
postgres://openseo:openseo@localhost:5433/openseo
```
## 2. Apply the Postgres migrations
The Postgres schema is hand-written (it is the one structural artifact
`db:generate` does not regenerate) and migrations live in `drizzle-pg/`. Apply
them with `POSTGRES_DATABASE_URL` set — `drizzle-kit` reads it from the shell
environment:
```sh
POSTGRES_DATABASE_URL=postgres://openseo:openseo@localhost:5433/openseo \
pnpm db:migrate:pg
```
## 3. Point the app at Postgres
The Cloudflare Vite runtime reads Worker vars from `.env.local`, so set the
provider flag there (not just in your shell):
```sh
# .env.local
DATABASE_PROVIDER=postgres
```
The connection string comes from the `HYPERDRIVE` binding: in local dev,
miniflare resolves it to the `localConnectionString` committed in
`wrangler.jsonc`, which already points at the Docker container from step 1.
(In deployed Workers the same binding resolves to real Hyperdrive — the app
never connects to Postgres except through this binding.) If your local Postgres
lives elsewhere, override without touching the config:
```sh
CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE=postgres://... pnpm dev
```
Then start the dev server as usual:
```sh
pnpm dev
```
To switch back to D1, remove that line (or set `DATABASE_PROVIDER=d1`) and
restart.
> `POSTGRES_DATABASE_URL` (step 2) is only read by Node-side tooling —
> `drizzle-kit` and `scripts/migrate-d1-to-postgres.ts`. The app itself ignores
> it.
## 4. Verify
```sh
# Tables created by the migrations
docker exec openseo-postgres psql -U openseo -d openseo -c "\dt"
# Inspect rows the app writes (e.g. after creating a project / saving keywords)
docker exec openseo-postgres psql -U openseo -d openseo -c "select count(*) from projects;"
```
## Schema changes
When you change a table, update **both** dialects:
- SQLite: `src/db/*.schema.ts` (+ `pnpm db:generate:d1`)
- Postgres: `src/db/pg/*.schema.ts` (+ `pnpm db:generate:pg`)
`src/db/schema-parity.test.ts` fails CI if the two dialects drift (mismatched
tables, columns, nullability, primary keys, unique/partial indexes, or FK
`onDelete`). It compares the schema definitions, **not** the generated
migrations — so after editing the Postgres schema, always run `pnpm db:generate:pg`
and commit the new `drizzle-pg/` migration, or a Postgres deploy will be missing
the change even though the parity test is green.
## Teardown
```sh
docker rm -f openseo-postgres
```
This deletes the container and all its data. Re-run from step 1 for a clean slate.