Remove direct POSTGRES_DATABASE_URL fallback; Postgres via Hyperdrive only (#351)

The Worker now connects to Postgres exclusively through the HYPERDRIVE
binding. Local dev uses the binding's localConnectionString (committed in
wrangler.jsonc, pointing at the throwaway Docker Postgres from
docs/LOCAL_POSTGRES.md) instead of a POSTGRES_DATABASE_URL Worker var.

Closes the Codex finding about unpooled direct connections from deployed
Workers: the not-recommended direct-connection config is no longer possible.
POSTGRES_DATABASE_URL remains as a Node-side env var for drizzle-kit and
the D1->Postgres migration script only.
This commit is contained in:
Ben Senescu 2026-07-05 18:11:11 -04:00 committed by GitHub
parent 86407c97e1
commit b22dc13b51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 39 additions and 20 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -57,9 +57,7 @@ export const pgDb = new Proxy(
* stale-connection risk) and do NOT call `sql.end()`: the WorkersHyperdrive
* 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<T>(fn: () => Promise<T>): Promise<T> {
if (getDatabaseProvider() !== "postgres") {

View File

@ -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).",
);
}

1
src/env.d.ts vendored
View File

@ -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;
};

View File

@ -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": [
{