diff --git a/docs/LOCAL_POSTGRES.md b/docs/LOCAL_POSTGRES.md index 1e86789..bce7564 100644 --- a/docs/LOCAL_POSTGRES.md +++ b/docs/LOCAL_POSTGRES.md @@ -54,13 +54,23 @@ POSTGRES_DATABASE_URL=postgres://openseo:openseo@localhost:5433/openseo \ ## 3. Point the app at Postgres -The Cloudflare Vite runtime reads Worker vars from `.env.local`, so set both -values there (not just in your shell): +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 -POSTGRES_DATABASE_URL=postgres://openseo:openseo@localhost:5433/openseo +``` + +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: @@ -69,9 +79,13 @@ Then start the dev server as usual: pnpm dev ``` -To switch back to D1, remove those two lines (or set `DATABASE_PROVIDER=d1`) and +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 diff --git a/runbooks/d1-to-postgres-detailed.md b/runbooks/d1-to-postgres-detailed.md index 66d5ac7..a4865e5 100644 --- a/runbooks/d1-to-postgres-detailed.md +++ b/runbooks/d1-to-postgres-detailed.md @@ -83,7 +83,8 @@ after cutover would collide with migrated rows. Confirm it ends with **"All row counts match."** 3. **Cut over.** Point the deployment at Postgres (`DATABASE_PROVIDER=postgres` - plus a Hyperdrive binding or `POSTGRES_DATABASE_URL`) and deploy: + plus a Hyperdrive binding — the app only connects to Postgres through + Hyperdrive) and deploy: ```sh pnpm deploy:postgres diff --git a/runbooks/d1-to-postgres-simple.md b/runbooks/d1-to-postgres-simple.md index 2920f1f..d5f1ba5 100644 --- a/runbooks/d1-to-postgres-simple.md +++ b/runbooks/d1-to-postgres-simple.md @@ -63,8 +63,8 @@ during the window is expected — see the detailed runbook.) ## 6. Cut over Point the deployment at Postgres (`DATABASE_PROVIDER=postgres` plus a Hyperdrive -binding or `POSTGRES_DATABASE_URL`) and deploy, then smoke-test (load a project, -save a keyword, check billing): +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 diff --git a/src/db/pg/client.ts b/src/db/pg/client.ts index dfc50e8..d3d3223 100644 --- a/src/db/pg/client.ts +++ b/src/db/pg/client.ts @@ -57,9 +57,7 @@ export const pgDb = new Proxy( * stale-connection risk) and do NOT call `sql.end()`: the Workers↔Hyperdrive * socket is torn down automatically when the invocation ends, and the pooled * origin connection stays warm for reuse. Not ending it also means a streamed - * response can keep querying after the handler returns. (Without a Hyperdrive - * binding, `POSTGRES_DATABASE_URL` opens a direct connection that the Workers - * runtime still reclaims at invocation end.) + * response can keep querying after the handler returns. */ export async function withPgClient(fn: () => Promise): Promise { if (getDatabaseProvider() !== "postgres") { diff --git a/src/db/provider.ts b/src/db/provider.ts index d8eb186..86b554f 100644 --- a/src/db/provider.ts +++ b/src/db/provider.ts @@ -18,6 +18,10 @@ export function getDatabaseProvider(): DatabaseProvider { ); } +// Postgres is only reachable through the HYPERDRIVE binding — never a direct +// connection string from a Worker var. In local dev the binding resolves to +// `localConnectionString` from wrangler.jsonc (miniflare never contacts real +// Hyperdrive), so the same code path covers both. export function getPostgresConnectionString() { const hyperdrive = Reflect.get(env, "HYPERDRIVE") as | { connectionString?: string } @@ -27,12 +31,7 @@ export function getPostgresConnectionString() { return hyperdriveUrl; } - const directUrl = Reflect.get(env, "POSTGRES_DATABASE_URL"); - if (typeof directUrl === "string" && directUrl.trim()) { - return directUrl.trim(); - } - throw new Error( - "DATABASE_PROVIDER=postgres requires a HYPERDRIVE binding or POSTGRES_DATABASE_URL.", + "DATABASE_PROVIDER=postgres requires a HYPERDRIVE binding (in local dev, its localConnectionString).", ); } diff --git a/src/env.d.ts b/src/env.d.ts index 04870af..6f420dd 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -21,7 +21,6 @@ declare namespace Cloudflare { BETTER_AUTH_SECRET?: string; BETTER_AUTH_URL?: string; DATABASE_PROVIDER?: "d1" | "postgres"; - POSTGRES_DATABASE_URL?: string; HYPERDRIVE?: { connectionString: string; }; diff --git a/wrangler.jsonc b/wrangler.jsonc index 1fd7d35..6beef23 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -78,15 +78,23 @@ // a worker *secret* (`wrangler secret put DATABASE_PROVIDER` -> "postgres") so it // survives `wrangler deploy` — which resets plain vars/bindings and is what // reverted prod to the D1 default before. This Hyperdrive binding points the - // pooled connection at that Postgres. Without Hyperdrive, POSTGRES_DATABASE_URL - // is the direct-connection fallback (no edge pooling/caching). + // pooled connection at that Postgres. Hyperdrive is the ONLY way the app + // connects to Postgres — there is no direct-connection fallback. + // + // `localConnectionString` is local-dev only (ignored by `wrangler deploy`): + // it's the throwaway Docker Postgres from docs/LOCAL_POSTGRES.md, and nothing + // connects to it unless DATABASE_PROVIDER=postgres is set in .env.local. // // SELF-HOSTERS on the free D1 default: DELETE this hyperdrive block before // deploying. The id below lives in OpenSEO's Cloudflare account, so `wrangler // deploy` will fail without access to it. D1 stays the default when // DATABASE_PROVIDER is unset. "hyperdrive": [ - { "binding": "HYPERDRIVE", "id": "9d64ccfb559f44449ce52a143912f898" }, + { + "binding": "HYPERDRIVE", + "id": "9d64ccfb559f44449ce52a143912f898", + "localConnectionString": "postgres://openseo:openseo@localhost:5433/openseo", + }, ], "r2_buckets": [ {