Skip to content

feat(postgres): per-request facade for serverless runtimes#421

Merged
saevarb merged 37 commits intomainfrom
tml-2369-ppg-add-a-hyperdrive-driver
May 7, 2026
Merged

feat(postgres): per-request facade for serverless runtimes#421
saevarb merged 37 commits intomainfrom
tml-2369-ppg-add-a-hyperdrive-driver

Conversation

@wmadden
Copy link
Copy Markdown
Contributor

@wmadden wmadden commented May 4, 2026

closes TML-2369

Intent

Make Prisma Next deployable on serverless / per-request Postgres runtimes by shipping a sibling postgresServerless facade alongside the existing postgres() factory. Cloudflare Workers + Hyperdrive is the primary tested target and primary documented path, but the facade is generic across per-request runtimes (Lambda, Vercel Edge/Serverless, Deno Deploy, Bun edge) — what differs across runtimes is only how the user sources the connection string.

The original framing of this work was "build a Hyperdrive driver". On investigation that framing was misleading: Hyperdrive is the standard Postgres wire protocol terminated at Cloudflare's edge, not a separate transport. The actual gap is per-request lifecycle ergonomics for the runtime-environment-class, not a new driver. This PR fills that gap with a new wrapper-level facade, an end-to-end Cloudflare Worker example, and durable architectural + user-facing documentation.

Change map

The story

  1. Audit first; pick the wrapper topology before writing code. A throwaway spike confirmed pg + pg-cursor work in workerd under nodejs_compat (cursor open / read-batches / early-break / close all clean). The existing PostgresDirectDriverImpl (pgClient PostgresBinding kind) already implements the per-request lifecycle Hyperdrive needs — lazy client.connect(), no pg.Pool, explicit client.end(), mutex-serialized acquireConnection for transaction affinity. Conclusion: the gap is at the wrapper level, not the driver level. No driver-layer changes shipped in this PR.

  2. Ship the wrapper as a sibling facade, not a new package. A new entrypoint @prisma-next/postgres/serverless exports postgresServerless<Contract>({ contractJson, extensions, middleware }). Construction shape mirrors the existing postgres() factory; the runtime surface is intentionally narrower — sql, context, stack, contract, and connect(), with orm, runtime(), and transaction() deliberately omitted. db.connect({ url }) returns a fresh Runtime & AsyncDisposable per call (no closure cache); each connection routes through the existing pgClient driver path with a freshly constructed pg.Client. The runtime carries [Symbol.asyncDispose] so per-request teardown is await using-clean.

  3. Build the example as the dogfood. examples/prisma-next-cloudflare-worker deploys via wrangler.jsonc with nodejs_compat, points at a Hyperdrive binding whose localConnectionString resolves to a local Docker Postgres, and exercises all three query surfaces (SQL DSL, ORM, withTransaction) plus a cursor-streaming + early-break path. The integration test runs under vitest-pool-workers (workerd in-process) and is wired into CI with a Docker Postgres bring-up step.

  4. Settle the architecture decision in a durable place. The asymmetry between postgres() (long-lived) and postgresServerless() (per-request) — same authoring surface, different runtime surface, different cursor default — is recorded as ADR 207. The user-facing version of the same story lives in the new deployment guide. spec.md (under projects/) is transient and won't survive close-out; the ADR is where future contributors will look when they ask "why does the API look like this?" or when MySQL/MongoDB serverless lands and needs to make the same call.

Behavior changes & evidence

  • Adds a postgresServerless facade for per-request Postgres runtimes. Construction shape mirrors the existing postgres() factory; the returned client exposes sql, context, stack, contract, and connect(), and intentionally omits orm, runtime(), and transaction() — those convenience surfaces depend on a closure-cached runtime that is unsafe in per-request lifecycles.

  • Adds a deployable Cloudflare Worker example exercising the facade end-to-end. examples/prisma-next-cloudflare-worker/ mirrors examples/prisma-next-demo (minus pgvector) and runs against a Hyperdrive-fronted local Postgres. Module-scope db = postgresServerless<Contract>(...) is built once per isolate; per request, await using runtime = await db.connect({ url: env.HYPERDRIVE.connectionString }) acquires and disposes the underlying connection.

  • Cursor streaming is enabled by default on the per-request facade and proven against materialization. The serverless facade leaves cursor unset (defaulting to enabled in the driver); the long-lived postgres() facade keeps its existing cursor: { disabled: true } default. The cursor integration test seeds 10 000 posts and uses pg_stat_statements as a side channel to assert that fewer than 500 rows are transmitted across the wire when the worker yields-and-breaks after ~100 rows — a bound that fails decisively if the cursor were disabled (would record ~10 000 rows transmitted).

  • Records the per-environment facade asymmetry as a durable architectural decision (ADR 207) and as user-facing guidance (Serverless Deployment Guide). ADR 207 captures the lifecycle invariants that motivate the asymmetry, the four concrete asymmetries (shared static surface; AsyncDisposable runtime; no closure-cached convenience members; cursor defaults), and rejects three alternatives explicitly: AsyncLocalStorage-hidden runtime, single facade with per-call disposable runtime everywhere, per-product facades (postgresWorkers / postgresLambda / …). The deployment guide is the practical version of the same story for users.

Compatibility / migration / risk

  • No regressions for existing Node deployments. All existing postgres({ url|pg|binding }) callers see no behavior change — the new code path is import-time-opt-in via @prisma-next/postgres/serverless. git diff origin/main..HEAD -- packages/3-extensions/postgres/src/runtime/postgres.ts packages/3-extensions/postgres/src/runtime/binding.ts is empty; the existing 27-case postgres.test.ts suite passes unchanged.
  • No driver-layer changes. The audit confirmed the existing PostgresDirectDriverImpl (pgClient binding) already implements the lifecycle the serverless facade needs.
  • Bundle size: 254 KiB compressed (gzip), well under the 1 MB self-imposed budget and Cloudflare's 3 MB free-tier ceiling. Largest contributors: pg, pg-protocol, pg-types, pg-cursor, pg-pool (statically imported by @prisma-next/driver-postgres even though postgresServerless does not construct a Pool at runtime), pg-cloudflare (auto-pulled by pg when running under workerd), and @cloudflare/unenv-preset polyfills.
  • Cold-start best-effort: ~35 ms cold / ~13 ms warm p50 against wrangler dev + local Docker Postgres. Production cold-start over real Hyperdrive will be slower; re-measure during wrangler deploy smoke verification.

Follow-ups / open questions

  • AC-12 (real wrangler deploy smoke) — NOT VERIFIED. Needs a Cloudflare account + Hyperdrive entitlement + Postgres origin (PPg / Neon / RDS / Supabase). Plan task 4.2 + close-out tasks 4.5–4.7 land when that infra is provisioned. AC-verification scoreboard at projects/cloudflare-hyperdrive-runtime/assets/ac-verification.md records 19 PASS / 0 FAIL / 1 NOT VERIFIED with full evidence pointers.
  • TML-2377 filed in [PN] Rough Edges: ORM class-table-inheritance variant queries on @@base + @@map discriminator schemas reference non-existent columns. Pre-existing drift in the demo's Task.bugs() / Task.features() helpers (silent because the demo doesn't invoke them at runtime); surfaced when the worker example tried to expose them. Documented in the example's known-limitations and at the issue: https://linear.app/prisma-company/issue/TML-2377.
  • Optional ADR 207 §1 prose tightening. Reviewer noted the pg.Client failure-mode prose ("cannot interleave queries; the ones following the first race for the same wire") is loose at the protocol level — pg.Client queues client-side (FIFO), so the actual hazards under shared-client-across-fetches are head-of-line blocking + cross-fetch transaction-state contamination (BEGIN/COMMIT bracketing across concurrent invocations). Decision and rationale stand; only the prose framing is imprecise. Non-blocking; tighten on next pass.
  • Backporting [Symbol.asyncDispose] to the Node postgres() facade. Out of scope here; useful for Node CLIs that want await using runtime = await db.connect({ url }). Recommend filing a separate ticket.
  • Removing the Node facade's hardcoded cursor: { disabled: true }. Out of scope; the asymmetry is intentional this PR. Out-of-band decision if the team wants to revisit.

Non-goals / intentionally out of scope

  • Tested examples or CI integration on non-Cloudflare per-request runtimes. AWS Lambda / Vercel Edge / Vercel Serverless / Deno Deploy / Bun support follows from the same facade with no further code changes; the deployment guide acknowledges them with a table-of-pointers but ships no worked examples.
  • Convenience surface (orm / runtime() / transaction) on the serverless facade. Deliberately omitted (see ADR 207). Asymmetry is a feature.
  • Dropping the convenience surface from the Node postgres() facade. Long-lived process makes it safe and useful; stays.
  • AsyncLocalStorage-based per-request convenience surface. Considered and rejected — implicit context, makes lifecycle invisible at call sites, load-bearing dependency on node:async_hooks semantics.
  • MySQL via Hyperdrive. Hyperdrive supports it; Prisma Next has no MySQL driver yet.
  • A Hyperdrive control-plane driver for migrations. Migrations run from Node against the origin database connection string, not Hyperdrive. Hyperdrive's caching makes it a poor fit for DDL.
  • Replacing pg with another underlying library (e.g. postgres.js).

Summary by CodeRabbit

  • New Features

    • Added a serverless Postgres facade for per-request/edge runtimes and published it as an export.
  • Documentation

    • New Serverless Deployment Guide, expanded ADR on per-environment facade asymmetry, and updated Postgres README comparing long-lived vs serverless facades.
  • Examples

    • Full Cloudflare Worker example with README, schema, seed/setup scripts, worker endpoints, generated contract, ORM helpers, and runtime/test configs.
  • Tests

    • Integration and type tests for the serverless facade and the Cloudflare example, plus enhanced global test setup.
  • Infrastructure

    • CI/test workflow updated to provision a local Postgres and expose a connection-string env var for local worker tests.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a per-request Postgres facade (postgresServerless) with types, implementation, tests, and package exports; introduces a Cloudflare Workers example (runtime, scripts, tests, generated contract) exercising SQL/ORM/transactions/cursors; updates docs/ADR and CI to support local Hyperdrive/Postgres on port 5433.

Changes

Serverless Postgres Facade

Layer / File(s) Summary
Types / API shape
packages/3-extensions/postgres/src/runtime/postgres-serverless.ts, packages/3-extensions/postgres/src/exports/serverless.ts
Declare PostgresServerlessClient, cursor/options types, and serverless factory overloads.
Core Implementation
packages/3-extensions/postgres/src/runtime/postgres-serverless.ts
Implements postgresServerless(...) returning { sql, context, stack, contract, connect({ url }) }; connect() creates fresh execution stack, pg.Client, driver, runtime, and wires Symbol.asyncDispose.
Exports & Packaging
packages/3-extensions/postgres/package.json, packages/3-extensions/postgres/src/exports/serverless.ts, packages/3-extensions/postgres/tsdown.config.ts
Adds ./serverless subpath export, forwards default export, and includes serverless entry in tsdown entries.
Compiler / Config
packages/3-extensions/postgres/tsconfig.json
Adds lib: ["ES2022","ESNext.Disposable"] to support AsyncDisposable types.
Tests (unit & types)
packages/3-extensions/postgres/test/postgres-serverless.test.ts, packages/3-extensions/postgres/test/postgres-serverless.types.test-d.ts
Adds mocked runtime/unit tests and type-level assertions validating facade surface, per-connect pg.Client creation, cursor defaults/overrides, disposal semantics, option forwarding, and contract validation.

Cloudflare Worker Example, Docs, Tests & CI

Layer / File(s) Summary
Prisma schema & contract emit
examples/prisma-next-cloudflare-worker/prisma/schema.prisma, .../src/prisma/contract.json, .../src/prisma/contract.d.ts
Adds domain schema (User/Post/Task with Bug/Feature discriminator, Address) and generated contract JSON + TypeScript emit.
Module-scoped client
examples/prisma-next-cloudflare-worker/src/prisma/db.ts
Creates db = postgresServerless<Contract>({ contractJson, middleware: [...] }) with telemetry/lints/budgets.
Worker implementation
examples/prisma-next-cloudflare-worker/src/worker.ts
Adds Cloudflare fetch handler with /health, /sql/users, /orm/users, /orm/posts, /tx/commit, /tx/rollback, /cursor/large; uses await using runtime = await db.connect({ url }).
ORM wiring
examples/prisma-next-cloudflare-worker/src/orm-client/client.ts, .../collections.ts
Adds createOrmClient(runtime) and UserCollection/PostCollection helpers (admins, newestFirst, forUser).
Scripts & local DB
examples/prisma-next-cloudflare-worker/scripts/setup-schema.ts, .../seed.ts, docker-compose.yml, .env.example
Add schema init and seed scripts; Docker Compose for ephemeral Postgres exposed on host port 5433; .env.example with WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE.
Example tooling & config
examples/prisma-next-cloudflare-worker/package.json, tsconfig.json, biome.jsonc, wrangler.jsonc
Example package metadata, TS/Biome config, and Wrangler binding placeholder for HYPERDRIVE.
Tests & test harness
examples/prisma-next-cloudflare-worker/test/global-setup.ts, worker.integration.test.ts, cloudflare-test.d.ts, vitest.config.ts
Global setup waits for Postgres, runs schema init, ensures pg_stat_statements, truncates/reseeds (10k posts), provides alice/bob IDs; integration tests exercise endpoints, transaction commit/rollback, and cursor early-break; Vitest config wires Miniflare/Hyperdrive injection and pg SSR optimization.
Docs & ADR
docs/Serverless Deployment Guide.md, docs/architecture docs/adrs/ADR 207 - Per-environment facade asymmetry.md, docs/architecture docs/ADR-INDEX.md, docs/README.md
Adds Serverless Deployment Guide, ADR 207 describing facade asymmetry, updates ADR index and docs README (Deploying link).
CI & root scripts
.github/workflows/ci.yml, package.json
CI test job sets WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE (127.0.0.1:5433) and runs pnpm --filter prisma-next-cloudflare-worker db:up before tests; root fixture scripts updated to include example contract emit/check.
Example docs & misc
examples/prisma-next-cloudflare-worker/README.md, .gitignore
Adds comprehensive example README and .gitignore for local artifacts.

Sequence Diagram(s)

sequenceDiagram
    participant Worker as Cloudflare Worker
    participant Client as postgresServerless Client
    participant Runtime as Per-Request Runtime
    participant Postgres as Hyperdrive/Postgres

    Worker->>Client: db.connect({ url: env.HYPERDRIVE })
    Client->>Runtime: create fresh Runtime & AsyncDisposable
    Runtime->>Postgres: pg.Client.connect()
    Postgres-->>Runtime: connected
    Runtime-->>Client: runtime ready
    Client-->>Worker: return runtime

    Worker->>Runtime: execute SQL/ORM/transaction/cursor
    Runtime->>Postgres: execute queries / stream cursor
    Postgres-->>Runtime: results
    Runtime-->>Worker: data

    Worker->>Runtime: await runtime[Symbol.asyncDispose]()
    Runtime->>Postgres: close connection
    Postgres-->>Runtime: closed
    Runtime-->>Worker: disposed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • jkomyno

"🐰
A fresh runtime hops into each request,
Cursors and transactions tidy as a nest,
Hyperdrive hums, seeds and tests take flight,
Dispose clears the burrow at end of night,
Hops and hops—devs cheer, all is right."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change—adding a per-request Postgres facade for serverless runtimes, which aligns with the substantial implementation in postgres-serverless.ts and the Cloudflare Workers example.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2369-ppg-add-a-hyperdrive-driver

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/3-extensions/postgres/README.md (1)

98-107: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The package-level responsibilities and architecture still describe only the Node facade.

After adding @prisma-next/postgres/serverless, these sections still imply every client has orm, runtime(), memoization, and pg.Pool-based URL binding. That conflicts with the serverless entrypoint you just documented and can push readers toward the exact caching pattern this README warns against. Please split these sections by facade, or add a parallel postgresServerless(...) -> connect({ url }) -> pg.Client path.
As per coding guidelines, "**/README.md: For user-facing packages, keep README.md focused on what the package does, when to use it, and a few concrete examples. Avoid internal implementation detail unless it materially affects usage."

Also applies to: 122-139

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-extensions/postgres/README.md` around lines 98 - 107, The README's
"Responsibilities" and architecture sections currently describe only the Node
facade and thus misrepresent the new serverless entrypoint; update these
sections to explicitly separate the two facades (Node vs serverless) or add a
parallel entry such as "postgresServerless(...) -> connect({ url }) ->
pg.Client" so readers see the different behaviors (no orm, no memoized
db.runtime(), and no pg.Pool-based URL binding) for serverless; mention the
unique symbols/functions to edit: postgresServerless, connect, db.runtime, orm,
pg.Pool and pg.Client, and adjust example usage and responsibilities bullets to
show which apply to each facade.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/architecture` docs/adrs/ADR 207 - Per-environment facade asymmetry.md:
- Around line 50-73: The fenced ASCII diagram block that begins with the box
containing "@prisma-next/postgres" is missing a language hint (triggers MD040);
update the opening fence from ``` to ```text so the block is explicitly marked
as plain text (i.e., replace the triple backticks around the diagram with
```text and keep the closing fence as ```).

In `@docs/Serverless` Deployment Guide.md:
- Line 47: The Markdown code fence used for the ASCII architecture diagram is
missing a language tag which triggers markdownlint MD040; update the opening
triple-backtick fence for that architecture block to include the language tag
"text" (i.e., change ``` to ```text) so the diagram block is explicitly marked
as text and satisfies the linter.

In `@examples/prisma-next-cloudflare-worker/.env.example`:
- Line 8: Remove the surrounding double quotes from the environment variable
value in .env.example for WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE
so the line reads
WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE=postgres://postgres:postgres@127.0.0.1:5433/prisma_next_cloudflare_worker;
this eliminates the dotenv-linter QuoteCharacter warning while preserving the
same connection string.

In `@examples/prisma-next-cloudflare-worker/README.md`:
- Line 31: The README.md has two fenced code blocks missing a language
identifier (triggering markdownlint MD040); update both code fences referenced
in the diff (the example directory listing block and the "Total Upload" block)
to include a language tag such as "text" (i.e., replace the opening triple
backticks with ```text for those two blocks) so the blocks are properly
annotated for markdownlint.
- Around line 74-77: The README example incorrectly states the seed script
inserts 50 posts; update the docs to reflect the actual seeded count from
examples/prisma-next-cloudflare-worker/scripts/seed.ts (which inserts 8 posts
total: 5 for Alice and 3 for Bob) by changing the "pnpm seed" description to
"Insert Alice + Bob + 8 posts" (or equivalent wording listing 5 Alice, 3 Bob) so
the README matches the seed.ts behavior.

In `@examples/prisma-next-cloudflare-worker/scripts/setup-schema.ts`:
- Line 37: Update the stale hint string in the setup failure output: replace the
console.error call that currently prints "Hint: `pnpm db:dev` prints the TCP
URL." with a message pointing to the correct command (e.g., "Hint: `pnpm db:up`
prints the TCP URL.") so the console.error invocation reflects the actual
exposed command.

In `@examples/prisma-next-cloudflare-worker/src/worker.ts`:
- Line 18: The code currently calls db.connect() unconditionally via "await
using runtime = await db.connect(...)" causing every request (including unknown
routes) to open a Postgres runtime; move the db.connect call so it only executes
inside the branch(es) that actually need the database (the DB-backed route
handlers) — i.e., remove the top-level "await using runtime = await
db.connect(...)" and instead call "await using runtime = await db.connect(...)"
inside the specific route handlers that perform queries (refer to the runtime
variable and db.connect call) or guard it behind an explicit allowlist check so
the unknown-route path returns 404 without attempting to connect. Ensure
disposal logic remains colocated with the connect call.
- Around line 76-94: The rollback route currently treats any error from
withTransaction(...) as success; change it so only the intentional rollback
sentinel is considered success. Modify the /tx/rollback handler (the
withTransaction call that throws new Error('intentional rollback')) to either
throw and check a specific sentinel (e.g., a custom RollbackError class) or
check the error message for the exact 'intentional rollback' sentinel, and if
and only if it matches return { ok: true, route: 'tx/rollback', message: ... };
for any other error from withTransaction (SQL errors, connection errors, etc.)
return a failure response like { ok: false, error: <error details> } instead.
Ensure the code references withTransaction, tx.execute and the thrown sentinel
(currently 'intentional rollback') when implementing this conditional handling.

In `@packages/3-extensions/postgres/src/runtime/postgres-serverless.ts`:
- Around line 152-161: The runtime creation may throw after establishing a DB
connection, leaving the Client open; update the sequence around Client,
driver.connect(...) and createRuntime(...) so that if createRuntime(...) fails
you close/dispose the connected client and/or call driver.disconnect/cleanup
before rethrowing; specifically wrap the createRuntime call in a try/catch that
calls client.end() (or the driver's corresponding close method used elsewhere)
and any driver cleanup for the same path, then rethrow the error. Apply the same
pattern to the other connect/createRuntime pair referenced (the similar connect
at the later location) to ensure no leaked connections.

---

Outside diff comments:
In `@packages/3-extensions/postgres/README.md`:
- Around line 98-107: The README's "Responsibilities" and architecture sections
currently describe only the Node facade and thus misrepresent the new serverless
entrypoint; update these sections to explicitly separate the two facades (Node
vs serverless) or add a parallel entry such as "postgresServerless(...) ->
connect({ url }) -> pg.Client" so readers see the different behaviors (no orm,
no memoized db.runtime(), and no pg.Pool-based URL binding) for serverless;
mention the unique symbols/functions to edit: postgresServerless, connect,
db.runtime, orm, pg.Pool and pg.Client, and adjust example usage and
responsibilities bullets to show which apply to each facade.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 754e67c4-20bf-42b1-8cbf-6495c1652e3a

📥 Commits

Reviewing files that changed from the base of the PR and between 9b27e08 and 20502f5.

⛔ Files ignored due to path filters (5)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • projects/cloudflare-hyperdrive-runtime/assets/ac-verification.md is excluded by !projects/**
  • projects/cloudflare-hyperdrive-runtime/assets/workers-compat-audit.md is excluded by !projects/**
  • projects/cloudflare-hyperdrive-runtime/plan.md is excluded by !projects/**
  • projects/cloudflare-hyperdrive-runtime/spec.md is excluded by !projects/**
📒 Files selected for processing (36)
  • .github/workflows/ci.yml
  • docs/README.md
  • docs/Serverless Deployment Guide.md
  • docs/architecture docs/ADR-INDEX.md
  • docs/architecture docs/adrs/ADR 207 - Per-environment facade asymmetry.md
  • examples/prisma-next-cloudflare-worker/.env.example
  • examples/prisma-next-cloudflare-worker/.gitignore
  • examples/prisma-next-cloudflare-worker/README.md
  • examples/prisma-next-cloudflare-worker/biome.jsonc
  • examples/prisma-next-cloudflare-worker/docker-compose.yml
  • examples/prisma-next-cloudflare-worker/package.json
  • examples/prisma-next-cloudflare-worker/prisma-next.config.ts
  • examples/prisma-next-cloudflare-worker/prisma/schema.prisma
  • examples/prisma-next-cloudflare-worker/scripts/seed.ts
  • examples/prisma-next-cloudflare-worker/scripts/setup-schema.ts
  • examples/prisma-next-cloudflare-worker/src/orm-client/client.ts
  • examples/prisma-next-cloudflare-worker/src/orm-client/collections.ts
  • examples/prisma-next-cloudflare-worker/src/prisma/contract.d.ts
  • examples/prisma-next-cloudflare-worker/src/prisma/contract.json
  • examples/prisma-next-cloudflare-worker/src/prisma/db.ts
  • examples/prisma-next-cloudflare-worker/src/worker.ts
  • examples/prisma-next-cloudflare-worker/test/cloudflare-test.d.ts
  • examples/prisma-next-cloudflare-worker/test/global-setup.ts
  • examples/prisma-next-cloudflare-worker/test/worker.integration.test.ts
  • examples/prisma-next-cloudflare-worker/tsconfig.json
  • examples/prisma-next-cloudflare-worker/vitest.config.ts
  • examples/prisma-next-cloudflare-worker/wrangler.jsonc
  • package.json
  • packages/3-extensions/postgres/README.md
  • packages/3-extensions/postgres/package.json
  • packages/3-extensions/postgres/src/exports/serverless.ts
  • packages/3-extensions/postgres/src/runtime/postgres-serverless.ts
  • packages/3-extensions/postgres/test/postgres-serverless.test.ts
  • packages/3-extensions/postgres/test/postgres-serverless.types.test-d.ts
  • packages/3-extensions/postgres/tsconfig.json
  • packages/3-extensions/postgres/tsdown.config.ts

Comment thread docs/architecture docs/adrs/ADR 207 - Per-environment facade asymmetry.md Outdated

### Architecture

```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to the architecture code fence.

Use text for this block to satisfy markdownlint MD040.

🛠️ Proposed fix
-```
+```text
 ┌─────────────────┐      ┌────────────────┐      ┌─────────────────┐
 ...
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>

[warning] 47-47: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/Serverless Deployment Guide.md at line 47, The Markdown code fence used
for the ASCII architecture diagram is missing a language tag which triggers
markdownlint MD040; update the opening triple-backtick fence for that
architecture block to include the language tag "text" (i.e., change ``` to

linter.

# Bring up the local container with `pnpm db:up` (Docker Postgres on port 5433),
# then copy this file to `.env` (gitignored). Schema/seed via:
# pnpm db:init && pnpm seed
WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgres://postgres:postgres@127.0.0.1:5433/prisma_next_cloudflare_worker"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove quote characters in .env.example value

Line 8 includes quotes around the URL, which triggers the reported dotenv-linter QuoteCharacter warning. The value can be unquoted without changing behavior.

Suggested patch
-WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgres://postgres:postgres@127.0.0.1:5433/prisma_next_cloudflare_worker"
+WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE=postgres://postgres:postgres@127.0.0.1:5433/prisma_next_cloudflare_worker
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgres://postgres:postgres@127.0.0.1:5433/prisma_next_cloudflare_worker"
WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE=postgres://postgres:postgres@127.0.0.1:5433/prisma_next_cloudflare_worker
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 8-8: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/prisma-next-cloudflare-worker/.env.example` at line 8, Remove the
surrounding double quotes from the environment variable value in .env.example
for WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE so the line reads
WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE=postgres://postgres:postgres@127.0.0.1:5433/prisma_next_cloudflare_worker;
this eliminates the dotenv-linter QuoteCharacter warning while preserving the
same connection string.


## Layout

```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add language identifiers to fenced code blocks.

Both fences are missing a language tag (text is fine), which triggers markdownlint MD040.

🛠️ Proposed fix
-```
+```text
 examples/prisma-next-cloudflare-worker/
 ...

- +text
Total Upload: 1289.96 KiB / gzip: 254.14 KiB

Also applies to: 103-103

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 31-31: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/prisma-next-cloudflare-worker/README.md` at line 31, The README.md
has two fenced code blocks missing a language identifier (triggering
markdownlint MD040); update both code fences referenced in the diff (the example
directory listing block and the "Total Upload" block) to include a language tag
such as "text" (i.e., replace the opening triple backticks with ```text for
those two blocks) so the blocks are properly annotated for markdownlint.

Comment on lines +74 to +77
pnpm db:up # docker compose up -d --wait (postgres:16 on :5433)
pnpm db:init # prisma-next db init → CREATE TABLE …
pnpm seed # Insert Alice + Bob + 50 posts
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix seeded post-count in setup docs.

Line 76 says seed inserts 50 posts, but examples/prisma-next-cloudflare-worker/scripts/seed.ts currently inserts 8 total (5 Alice, 3 Bob).

🛠️ Proposed fix
-pnpm seed                        # Insert Alice + Bob + 50 posts
+pnpm seed                        # Insert Alice + Bob + 8 posts
As per coding guidelines `**/*.md`: Keep docs current (READMEs, rules, links) as defined in `.cursor/rules/doc-maintenance.mdc`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/prisma-next-cloudflare-worker/README.md` around lines 74 - 77, The
README example incorrectly states the seed script inserts 50 posts; update the
docs to reflect the actual seeded count from
examples/prisma-next-cloudflare-worker/scripts/seed.ts (which inserts 8 posts
total: 5 for Alice and 3 for Bob) by changing the "pnpm seed" description to
"Insert Alice + Bob + 8 posts" (or equivalent wording listing 5 Alice, 3 Bob) so
the README matches the seed.ts behavior.

console.error(
`Set ${HYPERDRIVE_VAR} in .env (or DATABASE_URL in the environment) before running db:init.`,
);
console.error('Hint: `pnpm db:dev` prints the TCP URL.');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix stale command hint in setup failure output

Line 37 points users to pnpm db:dev, but this example exposes db:up. The current hint can send users to a non-existent command.

Suggested patch
-  console.error('Hint: `pnpm db:dev` prints the TCP URL.');
+  console.error('Hint: run `pnpm db:up`, then copy `.env.example` to `.env`.');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.error('Hint: `pnpm db:dev` prints the TCP URL.');
console.error('Hint: run `pnpm db:up`, then copy `.env.example` to `.env`.');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/prisma-next-cloudflare-worker/scripts/setup-schema.ts` at line 37,
Update the stale hint string in the setup failure output: replace the
console.error call that currently prints "Hint: `pnpm db:dev` prints the TCP
URL." with a message pointing to the correct command (e.g., "Hint: `pnpm db:up`
prints the TCP URL.") so the console.error invocation reflects the actual
exposed command.

return Response.json({ ok: true });
}

await using runtime = await db.connect({ url: env.HYPERDRIVE.connectionString });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Defer db.connect() until you know the route needs Postgres.

Line 18 opens a runtime for every non-/health request, so Line 166’s unknown-route path still pays for a database connect/dispose cycle. More importantly, if Postgres is unavailable, an unknown path stops returning the intended 404 and fails on connection instead. Move the connect into the DB-backed branches or behind an explicit route allowlist.

Also applies to: 166-169

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/prisma-next-cloudflare-worker/src/worker.ts` at line 18, The code
currently calls db.connect() unconditionally via "await using runtime = await
db.connect(...)" causing every request (including unknown routes) to open a
Postgres runtime; move the db.connect call so it only executes inside the
branch(es) that actually need the database (the DB-backed route handlers) —
i.e., remove the top-level "await using runtime = await db.connect(...)" and
instead call "await using runtime = await db.connect(...)" inside the specific
route handlers that perform queries (refer to the runtime variable and
db.connect call) or guard it behind an explicit allowlist check so the
unknown-route path returns 404 without attempting to connect. Ensure disposal
logic remains colocated with the connect call.

Comment on lines +76 to +94
if (url.pathname === '/tx/rollback') {
try {
await withTransaction(runtime, async (tx) => {
await tx.execute(
db.sql.user
.update({ displayName: 'rolled-back-write' })
.where((f, fns) => fns.eq(f.email, 'alice@example.com'))
.build(),
);
throw new Error('intentional rollback');
});
return Response.json({ ok: false, error: 'expected rollback but transaction committed' });
} catch (err) {
return Response.json({
ok: true,
route: 'tx/rollback',
message: err instanceof Error ? err.message : String(err),
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only the intentional rollback sentinel should return success.

Lines 88-93 currently convert any error from withTransaction(...) into { ok: true }. That masks real failures—SQL errors, connection loss, driver regressions—as a passing rollback route.

Possible fix
       } catch (err) {
+        const message = err instanceof Error ? err.message : String(err);
+        if (message !== 'intentional rollback') {
+          throw err;
+        }
         return Response.json({
           ok: true,
           route: 'tx/rollback',
-          message: err instanceof Error ? err.message : String(err),
+          message,
         });
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/prisma-next-cloudflare-worker/src/worker.ts` around lines 76 - 94,
The rollback route currently treats any error from withTransaction(...) as
success; change it so only the intentional rollback sentinel is considered
success. Modify the /tx/rollback handler (the withTransaction call that throws
new Error('intentional rollback')) to either throw and check a specific sentinel
(e.g., a custom RollbackError class) or check the error message for the exact
'intentional rollback' sentinel, and if and only if it matches return { ok:
true, route: 'tx/rollback', message: ... }; for any other error from
withTransaction (SQL errors, connection errors, etc.) return a failure response
like { ok: false, error: <error details> } instead. Ensure the code references
withTransaction, tx.execute and the thrown sentinel (currently 'intentional
rollback') when implementing this conditional handling.

Comment thread packages/3-extensions/postgres/src/runtime/postgres-serverless.ts Outdated
wmadden added a commit that referenced this pull request May 5, 2026
…spec and plan

Spec ACs flipped to checked except AC-12 (still NOT VERIFIED, blocked
on real Cloudflare deploy). AC-19/AC-20 carry the recorded
measurements (254 KiB gzip, 35 ms cold / 13 ms warm).

Plan adds a status banner pointing to PR #421 and the latest commit,
marks M2 + M3 tasks complete with commit SHAs, and reshapes M4 into
two streams: Stream A (deployment guide, ADR 207, AC verification)
done; Stream B (wrangler deploy smoke + close-out) is the remaining
handover surface with concrete prerequisites, steps, and risks
documented inline.
wmadden added 27 commits May 6, 2026 15:26
Reframes "build a Hyperdrive driver" as the actual goal: deploy
Prisma Next on Cloudflare Workers, connecting via Hyperdrive to a
Postgres origin. Hyperdrive is just the standard pg wire protocol
terminated at Cloudflare; the real work is Workers-runtime
compatibility for the PN runtime and a per-request lifecycle
that does not double-pool on top of Hyperdrive.

Adds spec.md (FRs, NFRs, non-goals, 18 acceptance criteria, locked
decisions) and plan.md placeholder (to be filled via
drive-create-plan once shaping is approved).
…reaming claim (TML-2369)

Plan: 4 milestones (audit -> driver/wrapper -> example+integration
-> docs+close-out), 22 test cases mapped to acceptance criteria,
shipping-strategy section explaining call-site opt-in as the
implicit gate, per-milestone validation gates with concrete
commands, and 7 open items carried into execution.

Spec correction: cursor support on the Workers/Hyperdrive path is
desired, not pre-emptively dropped. Hyperdrive itself supports
cursors via the standard pg wire protocol; whether pg-cursor works
under nodejs_compat in Workers is an audit deliverable. Only fall
back to buffered if the audit shows pg-cursor is not viable.
M1 (Workers compatibility audit) deliverable. Empirical audit of
@prisma-next/postgres + pg + pg-cursor under Cloudflare Workers
nodejs_compat, performed via a wip/ spike worker against local
Postgres with localConnectionString. Outcome: topology (c)
wrapper-only -- the existing PostgresDirectDriverImpl already
implements the per-request lifecycle Hyperdrive needs, so no
driver-layer changes are required. pg + pg-cursor work in workerd
including cursor open/read/early-break/close. Bundle baseline
(pg-only spike): 53 KiB gzipped, well under spec NFR3 1 MiB target.

Spec/plan amendments to reflect the audit follow in a separate
commit.
…Serverless facade (TML-2369)

Reflects the design conversation that followed the M1 audit. Three
load-bearing reframes:

1. The deliverable is a sibling postgresServerless facade, not a
   driver-layer change. The existing PostgresDirectDriverImpl
   already implements the per-request lifecycle; the gap is at the
   wrapper.

2. The serverless facade intentionally drops db.orm / db.runtime() /
   db.transaction. Closure-cached convenience surfaces are unsafe
   in per-request runtimes (stale connections across isolate idle,
   concurrent-query foot-guns on a shared pg.Client, no clean
   shutdown). Asymmetry with the Node facade is a feature: it forces
   users to acknowledge their environment lifecycle.

3. The facade is runtime-environment-shaped, not Cloudflare-product-
   shaped. db.connect({ url }) accepts any connection string;
   Hyperdrive is one origin among many. Project scope widens from
   "Cloudflare + Hyperdrive only" to "per-request runtimes broadly,
   with Cloudflare + Hyperdrive as the primary tested path."

Spec changes:
- Summary/description rewritten around the lifecycle distinction.
- FR1-FR3 redefine the facade surface, binding input, and lifecycle.
- FR5/FR7 reframe example + docs around the facade-asymmetry rationale.
- Drops "edge runtime portability" non-goal; widens scope.
- ACs renumbered/rewritten to align with the new surface (negative
  type test for absent fields, structural type test for symmetric
  options, no-Pool/single-Client lifecycle tests, etc.).
- Decisions section rewritten to record the locked design.

Plan changes:
- M1 marked complete with link to the audit doc.
- M2 task list rewritten end-to-end: package shape decision, factory
  + connect impl, cursor wiring, unit tests, layering, README.
  No driver-layer tasks.
- Test Design table rewritten to the new ACs.
- Open items pruned to those still pending.
M2 prep — package shape locked: new entrypoint
@prisma-next/postgres/serverless within the existing
@prisma-next/postgres package (Option B). Same package because the
serverless facade shares all runtime deps with the Node factory;
a separate package would add maintenance cost without
architectural benefit.

- Adjust plan task 2.1 to reflect the locked decision.
- Simplify 2.6 (no architecture.config.json change expected).
- Note in Open Items that the package shape is locked.
…s (TML-2369)

Per-request lifecycle is unsafe with the existing closure-cached
postgres() factory: stale connections after isolate idle, concurrent-query
races on a shared pg.Client, no clean shutdown across fetch invocations.

postgresServerless() exposes only the static authoring surface (sql,
context, stack, contract) at module scope. Each db.connect({ url }) call
constructs a fresh pg.Client, routes through the existing pgClient
binding kind on PostgresDirectDriverImpl (no driver-layer changes), and
returns a Runtime augmented with [Symbol.asyncDispose] so per-request
teardown is await-using clean.

Cursors are enabled by default (audit-confirmed working under
nodejs_compat); users can opt out via cursor: { disabled: true }.

Negative type tests confirm orm/runtime/transaction are unreachable on
the serverless surface; mocked-pg lifecycle tests pin construction (one
pg.Client, no pg.Pool, dispose calls client.end exactly once per scope
exit) and the no-closure-cache invariant (two connect() calls -> two
distinct Runtime identities).

Lib esnext.disposable added to the package tsconfig so AsyncDisposable /
Symbol.asyncDispose typecheck under TS 5.9.

Refs: M2 task 2.2/2.3/2.4/2.5 in projects/cloudflare-hyperdrive-runtime/plan.md
Adds package.json exports map entry pointing at dist/serverless.mjs and
the matching tsdown build target so consumers can:

  import postgresServerless from "@prisma-next/postgres/serverless";

Both facades ship from the same package because they share the same
runtime dependency closure (pg, pg-cursor, the existing PN execution
stack); a separate package would add maintenance cost without
architectural benefit.

Refs: M2 task 2.1 in projects/cloudflare-hyperdrive-runtime/plan.md
Adds Quick Start sections for both runtime entrypoints with the
lifecycle rationale for why the serverless surface intentionally omits
orm/runtime/transaction. Existing postgres() runtime section retained.

Refs: M2 task 2.7 in projects/cloudflare-hyperdrive-runtime/plan.md
Two amendments per orchestrator triage of m2 R1 reviewer
escalations:

- Strike TC-25 (telemetry-event-shape test on the serverless
  lifecycle). Selective enforcement otherwise: the Node
  postgres() factory has no telemetry test, and middleware
  pass-through is already structurally covered by
  postgres-serverless.test.ts lines 236-254. Updated both the
  Test Design row and the M2 task 2.5 bullet.

- Add open item #8: re-export PostgresCursorOptions from
  @prisma-next/driver-postgres/runtime as a follow-up ticket.
  Out of scope for this project; the serverless facades
  NonNullable<...> workaround is structurally equivalent.
Per AGENTS.md typesafety rules, every "as unknown as" cast needs an
inline comment explaining why it is necessary. Adds the missing
explanation to the negative-runtime test that probes the keys the
serverless facade intentionally hides from its public type.

Resolves m2 R1 finding F1.
The Node-facade Exports section advertised db.kysely and db.schema on
postgres()'s return; the actual factory exposes only sql, orm, context,
stack, connect(), runtime(), transaction(). Strikes the two stale
bullets, the stranded paragraph that elaborated on db.kysely,
the matching kysely/schema mentions in the Responsibilities section,
and the kysely(build-only)/schema nodes in the architecture mermaid.

Editorial-only: no behavior change. Authorized side-quest from m2 R2.
…drift (TML-2369)

The Dependencies section listed three packages the wrapper does not
import — sql-lane (real import is sql-builder), sql-kysely-lane, and
sql-relational-core (neither imported) — and described pg as
URL-binding-only when both factories also use Client construction
(pgClient binding on Node, every connect() on the serverless facade).

Corrects the sql-builder name, strikes the two unused packages, and
expands the pg description to cover both Pool and Client construction
paths.

Editorial-only: no behavior change. Authorized side-quest follow-up
from m2 R2.
…eading (TML-2369)

Three paragraphs describing Node-only behavior (db.runtime() deferral,
url/pg/binding variants, poolOptions) had ended up under the
@prisma-next/postgres/serverless export subsection — layout drift from
the m2 R1 README rewrite that introduced the Serverless heading. Moves
them up under @prisma-next/postgres/runtime where they describe the
actual surface.

Editorial-only: no behavior change. Authorized side-quest follow-up
from m2 R2.
…369)

Creates the package skeleton for the Cloudflare Worker example that exercises
@prisma-next/postgres/serverless against a Hyperdrive-fronted Postgres.
Schema mirrors prisma-next-demo minus pgvector (PGlite ships no pgvector and
the per-request facade is the focus, not vectors). The Hyperdrive binding shape
is wired in wrangler.jsonc with a placeholder ID for the M4 wrangler hyperdrive
provisioning step; localConnectionString is sourced from .dev.vars (gitignored)
for wrangler dev and vitest-pool-workers.

Plan task: 3.1.
…-2369)

Adds the emitted contract, the module-scope postgresServerless client, ORM
collections, and a fetch handler that routes to:

- /sql/users (SQL DSL select)
- /orm/users, /orm/posts, /orm/tasks (ORM client + variants)
- /tx/commit, /tx/rollback (withTransaction commit / rollback paths)
- /cursor/large (for-await with early break, exercising pg-cursor cancellation)
- /health

The handler acquires a fresh per-request runtime via `await using runtime =
await db.connect({ url: env.HYPERDRIVE.connectionString })`, so disposal
happens automatically on scope exit, including on rejected promises.

Plan tasks: 3.2 + 3.8 (fixtures parity).
…-2369)

Adds three Node-side scripts for the cloudflare-worker example:

- scripts/db-dev.ts boots a PGlite-backed Prisma Postgres via @prisma/dev and
  prints the TCP URL the maker copies into .dev.vars (gitignored) as
  LOCAL_DATABASE_URL. Calls out the prisma+postgres:// HTTP URL so makers do
  not paste the Data-Proxy-style one (postgresServerless speaks pg wire only).
- scripts/setup-schema.ts loads .dev.vars and shells to `prisma-next db init`
  to apply the contract schema.
- scripts/seed.ts mirrors prisma-next-demos seed (minus pgvector / variant
  inserts) using `await using runtime = await db.connect({ url })` so the
  module-scope serverless client is exercised the same way as in the worker.

Also threads the new example into the workspace `fixtures:emit` /
`fixtures:check` pipelines so its emitted contract artifacts stay in sync.

Plan tasks: 3.3 + 3.8.
…ng (TML-2369)

M3.5 ships the example README and corrects the local-dev plumbing wired up in
M3.1/M3.3:

- wrangler.jsonc previously carried a literal `localConnectionString:
  "$LOCAL_DATABASE_URL"`; wrangler does not perform that substitution.
  The Cloudflare-recommended path is the
  WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE env var read from
  `.env`, so the field is removed and the README documents the env var.
- .dev.vars / .dev.vars.example replaced with .env / .env.example. Wrangler
  reads the Hyperdrive env var from `.env`, not `.dev.vars` (the latter is
  for runtime worker secrets).
- scripts/setup-schema.ts and scripts/seed.ts updated to read the same env
  var name from `.env`; scripts/db-dev.ts now prints the correct setup
  instruction.

The README also records the M3.6 bundle-size measurement (gzip 254 KiB,
well under the AC-19 1 MB budget), explains the per-request lifecycle,
and surfaces the M3.4 vitest-pool-workers known-issue with links to the
upstream tracking issue. M3.7 cold-start benchmark is documented as
deferred (see report) — workerd-side connect to prisma dev hangs in both
vitest-pool-workers and wrangler dev.

The deferred M3.4 test infrastructure remains uncommitted on disk for the
orchestrator/reviewer to inspect; the example test script is replaced with
a stub that prints the deferral note and exits 0 so test:examples passes.
…res (TML-2369)

Drops `@prisma/dev` and the `scripts/db-dev.ts` wrapper from the
example: the PGlite TCP shim hangs under workerd 's Hyperdrive emulator
(see plan.md Open items #9 / cloudflare/workers-sdk#12984), so M3 is
moving its local origin to a vanilla `postgres:16` container.

Wires `docker-compose.yml` (port 5433 to avoid clashing with
`prisma-next-demo`'s Postgres.app) plus `pnpm db:up` / `db:down` /
`db:reset` scripts, and points `.env.example` at the docker URL. The
`test` script flips back to a real `vitest run` (the deferral stub
goes away in the next commit, when the integration suite lands).

Lockfile diff is just the `@prisma/dev` removal plus an unrelated
`tinyrainbow` patch bump pulled in by `pnpm install`.
Boots the cloudflare-worker example under workerd via
@cloudflare/vitest-pool-workers, points the Hyperdrive binding at
the local Docker Postgres origin, and exercises the SQL DSL,
ORM, transaction (commit + rollback), and cursor early-break
paths (TC-3 through TC-9 + TC-13/14, M3 task 3.4).

`vitest.config.ts` mirrors `WRANGLER_HYPERDRIVE_*` into
`CLOUDFLARE_HYPERDRIVE_*` before defineConfig runs because
vitest-pool-workers's parseCustomPoolOptions resolves the
Hyperdrive binding at config-parse time (before the cloudflareTest
callback fires). Pre-bundles `pg` and friends to work around
cloudflare/workers-sdk#12984's Vite 8 dual-export resolution.

`test/global-setup.ts` reads .env directly (no dotenv at runtime),
asserts the docker container is reachable, applies the schema
idempotently via `prisma-next db init`, then truncates and
reseeds — long-lived containers stay clean across re-runs.

Two integration-time fixes to the worker:
- `/cursor/large` now sets `.limit(1_000)` so the budgets middleware
  doesn't reject the otherwise-unbounded SELECT; the cursor still
  early-exits at the maker's `break` threshold.
- `/orm/tasks` route + `TaskCollection` are dropped — pre-existing
  framework drift makes class-table-inheritance ORM queries fail
  with `column "bug.id" does not exist` (the demo defines but never
  invokes the same helpers). Schema parity with the demo is kept;
  exercising the variant queries is deferred until the framework
  supports them.

8 tests pass in ~2s.
…art (TML-2369)

Replaces the m3.4/m3.7 deferral block with the actual local-dev
story: `pnpm db:up` + `pnpm db:init` + `pnpm seed`, then
`pnpm test` or `pnpm dev`. Drops the prisma-dev-specific layout
(scripts/db-dev.ts, scripts/start-dev-db-for-tests.mjs) and the
verbose troubleshooting around prisma dev's single-connection
constraint.

Records the cold-start best-effort benchmark from m3.7 (run 0
~35 ms, warm p50 ~13 ms against `wrangler dev` + Docker
Postgres — both well under the 200 ms ceiling in AC-20).

Re-confirmed bundle size at 254.14 KiB gzipped (m3.6
verification — vitest config changes are dev-only and don't
affect the production bundle).

Keeps the cloudflare/workers-sdk#12984 link in a "Why not
prisma dev?" footnote — useful context for anyone re-attempting
the PPg-on-Workers story (M4).

Adds the ORM class-table-inheritance limitation to "Known
limitations" so users hitting `column "bug.id" does not exist`
have a single place to land.
…ents (F2)

The previous /cursor/large test seeded only 50 rows, capped the route SELECT
at 1_000, and hardcoded `cancelled: true` in the response — so the assertion
trivially passed even when the cursor was disabled and the driver buffered
the entire result set. The test failed to exercise TC-9's actual streaming
intent.

Fix:

* Seed 10_000 posts via a single set-based generate_series INSERT, sized to
  the budgets cap in src/prisma/db.ts (`tableRows.post: 10_000`).
* Bump the /cursor/large route LIMIT to 10_000 and derive `cancelled` from
  whether the for-await loop actually broke early (vs. iterator exhaustion).
* Open a side-channel pg.Client inside the route, reset pg_stat_statements,
  run the cursor SELECT, then read SUM(rows) for statements touching post.
  pg_stat_statements is preloaded via docker-compose `shared_preload_libraries`
  + globalSetup `CREATE EXTENSION` so the catalog views are queryable.
* Test asserts rowsTransmitted < 500 — this is the assertion that fails
  decisively when cursor is disabled.

Verified both directions locally:
  cursor enabled (default): rowsTransmitted ≈ 100 (one batch), test passes.
  cursor: { disabled: true }: rowsTransmitted = 10_000, test fails with
  "AssertionError: expected 10000 to be less than 500".
Two coupled issues blocked AC-18 (the cloudflare-worker example test runs
in CI):

1. examples/prisma-next-cloudflare-worker/vitest.config.ts threw at
   config-parse time when the Hyperdrive env var was unset. That broke
   `pnpm test:examples --filter prisma-next-cloudflare-worker` locally
   without a .env, IDE integrations that import the config to list tests,
   and any CI invocation that didn't pre-set the env. Soft-fail when the
   var is missing — globalSetup throws the actionable error instead, with
   the same `pnpm db:up && cp .env.example .env` hint.

2. .github/workflows/ci.yml had no Postgres for the cloudflare-worker
   example. The existing test job has a postgres:15 service on 5432 for
   prisma-next-demo, but the cloudflare-worker example needs postgres:16
   on 5433 with `shared_preload_libraries=pg_stat_statements` preloaded
   (required by the cursor test's observability assertion). GitHub Actions
   service containers can't override the container CMD, so use the example's
   own docker-compose.yml — added a `pnpm --filter prisma-next-cloudflare-worker
   db:up` step before `pnpm test:examples`, plus the WRANGLER_HYPERDRIVE_*
   env var on the job.

Verified locally that the test passes against the env var set via
process.env (no .env file present), matching the CI invocation shape.
Why: the design decision behind `postgresServerless` (drop closure-cached
`orm` / `runtime()` / `transaction()` from the per-request facade) is
recorded today only in `projects/cloudflare-hyperdrive-runtime/spec.md`,
which does not survive close-out. Future contributors asking "why does the
serverless facade not have `db.orm`?" should land on an architecture
artifact, not on a how-to guide or `git blame`.

ADR 207 covers:
- Lifecycle invariants that make the long-lived ergonomic unsafe per-request
  (stale connections after isolate idle; concurrent-fetch races on a shared
  pg.Client).
- The three concrete asymmetries: shared static surface, AsyncDisposable
  runtime returned from `connect()`, no closure-cached convenience members.
  Plus the cursor-default difference (off on Node, on for serverless).
- Rejected alternatives: AsyncLocalStorage-based hidden runtime, single
  facade with per-call disposable runtime everywhere, per-product facades
  (`postgresWorkers`/`postgresLambda`/...).
- Cross-references to the ADRs the facades inherit from (152 execution
  plane, 155 driver/codec boundary, 159 driver lifecycle).

Indexed under Adapters & Targets — closest existing fit, given the
asymmetry is per-target-environment.
User-facing canonical doc for deploying Prisma Next to per-request
runtimes. Cloudflare Workers + Hyperdrive is the worked example;
AWS Lambda / Vercel / Deno / Bun are pointed-at via a "what differs
is only how you source the connection string" table.

Sections per spec FR7:
- Two facades, one driver — table contrasting `postgres()` vs
  `postgresServerless()`, lifecycle rationale, cross-link to ADR 207
  for the architectural deep-dive.
- Cloudflare Workers + Hyperdrive worked example: architecture,
  setup (origin / `wrangler hyperdrive create` / `wrangler.jsonc` /
  `.env`), worker code shape (module-scope db, per-request runtime,
  three surfaces, cursor streaming), ORM-client wiring.
- Generality across other per-request runtimes — short pointers
  table, no worked examples (per spec non-goals).
- Migrations stay on Node against the origin URL, not Hyperdrive
  (FR6); explains Hyperdrive caching / DDL mismatch.
- Known limitations: transaction affinity, isolate memory, cursor
  default asymmetry, static `pg-pool` import, Node-only migrations.
- Validation pointer to the example's vitest-pool-workers suite.

Linked from `docs/README.md` under a new "Deploying" section,
matching the top-level Title-Case-`Guide.md` siblings (`Testing
Guide.md`, `CLI Style Guide.md`).

Closes AC-14. Does not link to any `projects/` artifacts (close-out
rule).
m4 R1 Stream A is SATISFIED per the reviewer (verdict in
projects/cloudflare-hyperdrive-runtime/reviews/code-review.md). Mark
plan tasks 4.1, 4.3, 4.4 done; record the AC verification pull from
the scoreboard under projects/<>/assets/ac-verification.md for the
close-out PR description.

19 PASS / 0 FAIL / 1 NOT VERIFIED. AC-12 (real wrangler deploy smoke)
remains m4 Stream B and is blocked on Cloudflare account access. The
deployment guide (74c8a7c) and ADR 207 (9fec40b) carry the
durable architectural framing; the AC verification doc dies with
projects/ at close-out.

Refs: TML-2369.
wmadden and others added 4 commits May 6, 2026 15:26
Restructure: open with side-by-side call-site code on each client
(grounding example), then state the decision, then build the
lifecycle narrative bit by bit (long-lived = one runtime lifetime;
per-request = many short parallel lifetimes; mismatching the two
breaks in three specific ways), then enumerate the four asymmetries
once the reader has the model. Move alternatives to the end.

Tighten the failure-mode prose: pg.Client queues queries client-side
FIFO, so concurrent fetches sharing a closure-cached client suffer
head-of-line blocking + cross-fetch transaction-state contamination,
not racing for the wire. Drop project-state language ("primary
tested + documented path", "already established in the demo"). Drop
the trailing Decision-record section that duplicated the new lead.
Drop the busy ASCII layering diagram — the prose covers it and the
side-by-side example carries the rest.

Same word count, much stronger narrative.
…spec and plan

Spec ACs flipped to checked except AC-12 (still NOT VERIFIED, blocked
on real Cloudflare deploy). AC-19/AC-20 carry the recorded
measurements (254 KiB gzip, 35 ms cold / 13 ms warm).

Plan adds a status banner pointing to PR #421 and the latest commit,
marks M2 + M3 tasks complete with commit SHAs, and reshapes M4 into
two streams: Stream A (deployment guide, ADR 207, AC verification)
done; Stream B (wrangler deploy smoke + close-out) is the remaining
handover surface with concrete prerequisites, steps, and risks
documented inline.
Captures current state at the m4 R1 boundary (3/4 milestones complete,
Stream A done, Stream B blocked on Cloudflare account + Hyperdrive
entitlement) plus the concrete steps, known-good vs known-broken
context, and follow-up tickets the next maker needs.

Lives under projects/ and dies with the close-out PR.
Production smoke against real CF + Hyperdrive + PPg verified 6/7 routes
end-to-end. AC-12 PASS, AC-20 re-measured (TTFB cold 197.9 ms within
200 ms ceiling).

Surfaced a Hyperdrive bug: the default pg-cursor path (extended-query
named portals) is rejected by Hyperdrive's protocol parser ("Protocol
Error: Unexpected protocol code: C", SQLSTATE 58000) — even inside
withTransaction, verified by wire-level trace. Workaround:
cursor: { disabled: true }. Decision: keep the cursor-on default;
document the Hyperdrive-specific workaround in the deployment guide.
Bug to be filed upstream with Cloudflare.

- Add cursor caveat to docs/Serverless Deployment Guide.md
- Add warning + fix `pnpm deploy` -> `pnpm run deploy` in example README
- Remove projects/cloudflare-hyperdrive-runtime/ per close-out plan
@saevarb saevarb force-pushed the tml-2369-ppg-add-a-hyperdrive-driver branch from 71db735 to 48df22a Compare May 6, 2026 13:26
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 6, 2026

Open in StackBlitz

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-runtime@421

@prisma-next/family-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-mongo@421

@prisma-next/sql-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-runtime@421

@prisma-next/family-sql

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-sql@421

@prisma-next/extension-arktype-json

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-arktype-json@421

@prisma-next/middleware-telemetry

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/middleware-telemetry@421

@prisma-next/mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo@421

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-paradedb@421

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-pgvector@421

@prisma-next/postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/postgres@421

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-orm-client@421

@prisma-next/sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sqlite@421

@prisma-next/target-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-mongo@421

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-mongo@421

@prisma-next/driver-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-mongo@421

@prisma-next/contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract@421

@prisma-next/utils

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/utils@421

@prisma-next/config

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/config@421

@prisma-next/errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/errors@421

@prisma-next/framework-components

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/framework-components@421

@prisma-next/operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/operations@421

@prisma-next/ts-render

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/ts-render@421

@prisma-next/contract-authoring

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract-authoring@421

@prisma-next/ids

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/ids@421

@prisma-next/psl-parser

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-parser@421

@prisma-next/psl-printer

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-printer@421

@prisma-next/cli

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/cli@421

@prisma-next/emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/emitter@421

@prisma-next/migration-tools

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/migration-tools@421

prisma-next

npm i https://pkg.pr.new/prisma/prisma-next@421

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/vite-plugin-contract-emit@421

@prisma-next/mongo-codec

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-codec@421

@prisma-next/mongo-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract@421

@prisma-next/mongo-value

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-value@421

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract-psl@421

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract-ts@421

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-emitter@421

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-schema-ir@421

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-query-ast@421

@prisma-next/mongo-orm

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-orm@421

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-query-builder@421

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-lowering@421

@prisma-next/mongo-wire

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-wire@421

@prisma-next/sql-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract@421

@prisma-next/sql-errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-errors@421

@prisma-next/sql-operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-operations@421

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-schema-ir@421

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-psl@421

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-ts@421

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-emitter@421

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-lane-query-builder@421

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-relational-core@421

@prisma-next/sql-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-builder@421

@prisma-next/target-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-postgres@421

@prisma-next/target-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-sqlite@421

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-postgres@421

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-sqlite@421

@prisma-next/driver-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-postgres@421

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-sqlite@421

commit: 597f1ab

…r connect()

connect() called driver.connect() and then createRuntime() with no
cleanup path between. createRuntime is mostly pure (no I/O), so the
real leak risk today is theoretical — pg.Client opens its TCP socket
lazily on first query, not in driver.connect() — but the absent
cleanup is a defect by inspection and would turn into an actual socket
leak the day pg changes its connect semantics or something in
createRuntime starts allocating resources.

Wrap createRuntime in try/catch; on failure, close the driver (which
delegates into pg.Client.end()) and rethrow the original error.
driver.close() failures during cleanup are swallowed so the caller
sees the actual root cause, not the cleanup-path noise.

Two new unit tests pin the new path: driver.close() is called when
createRuntime throws, and a failing driver.close() does not mask the
original error.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
examples/prisma-next-cloudflare-worker/scripts/setup-schema.ts (1)

37-37: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix stale command hint in setup failure output.

The hint references pnpm db:dev, but this example exposes pnpm db:up (as defined in package.json).

Suggested fix
-  console.error('Hint: `pnpm db:dev` prints the TCP URL.');
+  console.error('Hint: run `pnpm db:up` to start the database, then set the connection string.');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/prisma-next-cloudflare-worker/scripts/setup-schema.ts` at line 37,
The failure message printed by the setup script uses a stale hint "pnpm db:dev";
update the console.error call in setup-schema.ts that currently outputs 'Hint:
`pnpm db:dev` prints the TCP URL.' to instead reference the correct script name
'pnpm db:up' so the hint matches package.json (change the string literal in the
console.error call).
🧹 Nitpick comments (1)
examples/prisma-next-cloudflare-worker/test/global-setup.ts (1)

59-82: ⚡ Quick win

Use the shared Postgres startup timeout here.

The hardcoded 15_000 drifts from the repo’s standard test budgets and makes this example easy to forget when those values are tuned centrally.

Based on learnings, "use prisma-next/test-utils timeout helpers—specifically timeouts.spinUpPpgDev for PostgreSQL server startup and timeouts.databaseOperation for database operation scenarios."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/prisma-next-cloudflare-worker/test/global-setup.ts` around lines 59
- 82, The ensureContainerReady function uses a hardcoded 15_000ms and a 500ms
backoff; replace those with the shared timeouts from prisma-next/test-utils: set
the startup deadline using timeouts.spinUpPpgDev instead of 15_000, and use
timeouts.databaseOperation for any per-attempt DB operation timeouts
(connect/query) or to drive retry/backoff behavior instead of the fixed 500ms
sleep; import timeouts and update ensureContainerReady, Client connect/query
handling, and the thrown error message to reference the timeout values via
timeouts.spinUpPpgDev and timeouts.databaseOperation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/Serverless` Deployment Guide.md:
- Around line 95-96: Update the Serverless Deployment Guide to qualify the
earlier claim that "`pg` + `pg-cursor` work end-to-end" by stating it applies to
local/miniflare and nodejs_compat testing, and add a front-loaded production
caveat that Hyperdrive deployments may hang with cursor mode—advise adding the
`cursor: { disabled: true }` configuration near the setup and code examples;
specifically edit the paragraph mentioning `nodejs_compat` and the sentence
referencing `pg` + `pg-cursor`, and insert a prominent note about Hyperdrive +
cursor behavior and the `cursor: { disabled: true }` workaround so readers see
the production limitation before implementation.

In `@examples/prisma-next-cloudflare-worker/README.md`:
- Around line 99-101: The README blockquote contains an empty line between two
`>` lines causing markdownlint rule MD028 to fail; remove the blank line so the
two quoted lines are contiguous (i.e., make both lines start with `>` with no
blank line between), preserving the existing text about using `pnpm run deploy`
and the note to pass `cursor: { disabled: true }` to `postgresServerless({...})`
in `src/prisma/db.ts`.

In `@examples/prisma-next-cloudflare-worker/scripts/seed.ts`:
- Around line 40-82: The seed is nondeterministic because re-running it inserts
duplicate users and the later select(...).limit(1) can pick any row; fix by
clearing or using fixed IDs: before the inserts call runtime.execute with
db.sql.user.delete().build() (or a truncate-equivalent) to remove existing demo
rows, or instead insert users with deterministic IDs by passing an explicit id
in db.sql.user.insert(...) and then use those IDs directly (avoid re-querying by
email and the select(...).limit(1)). Ensure you update references to alice/bob
to use the inserted IDs or the cleared-table assumption so subsequent
runtime.execute/select calls are deterministic.

In `@examples/prisma-next-cloudflare-worker/test/global-setup.ts`:
- Around line 6-10: The interface GlobalSetupContext currently uses an inline
type import import('vitest').ProvidedContext in the provide method; remove the
inline import and add a top-level type import for ProvidedContext from 'vitest'
(e.g., import type { ProvidedContext } from 'vitest') then update provide<K
extends keyof ProvidedContext & string>(key: K, value: ProvidedContext[K]) to
reference the top-level type; ensure only the type-only import is added at file
top and no inline import remains.

---

Duplicate comments:
In `@examples/prisma-next-cloudflare-worker/scripts/setup-schema.ts`:
- Line 37: The failure message printed by the setup script uses a stale hint
"pnpm db:dev"; update the console.error call in setup-schema.ts that currently
outputs 'Hint: `pnpm db:dev` prints the TCP URL.' to instead reference the
correct script name 'pnpm db:up' so the hint matches package.json (change the
string literal in the console.error call).

---

Nitpick comments:
In `@examples/prisma-next-cloudflare-worker/test/global-setup.ts`:
- Around line 59-82: The ensureContainerReady function uses a hardcoded 15_000ms
and a 500ms backoff; replace those with the shared timeouts from
prisma-next/test-utils: set the startup deadline using timeouts.spinUpPpgDev
instead of 15_000, and use timeouts.databaseOperation for any per-attempt DB
operation timeouts (connect/query) or to drive retry/backoff behavior instead of
the fixed 500ms sleep; import timeouts and update ensureContainerReady, Client
connect/query handling, and the thrown error message to reference the timeout
values via timeouts.spinUpPpgDev and timeouts.databaseOperation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 5e0bea47-69bb-4032-b248-13e2ba8ad0ad

📥 Commits

Reviewing files that changed from the base of the PR and between 5f85a5c and 48df22a.

📒 Files selected for processing (28)
  • .github/workflows/ci.yml
  • docs/README.md
  • docs/Serverless Deployment Guide.md
  • docs/architecture docs/ADR-INDEX.md
  • docs/architecture docs/adrs/ADR 207 - Per-environment facade asymmetry.md
  • examples/prisma-next-cloudflare-worker/.env.example
  • examples/prisma-next-cloudflare-worker/.gitignore
  • examples/prisma-next-cloudflare-worker/README.md
  • examples/prisma-next-cloudflare-worker/biome.jsonc
  • examples/prisma-next-cloudflare-worker/docker-compose.yml
  • examples/prisma-next-cloudflare-worker/package.json
  • examples/prisma-next-cloudflare-worker/prisma-next.config.ts
  • examples/prisma-next-cloudflare-worker/prisma/schema.prisma
  • examples/prisma-next-cloudflare-worker/scripts/seed.ts
  • examples/prisma-next-cloudflare-worker/scripts/setup-schema.ts
  • examples/prisma-next-cloudflare-worker/src/orm-client/client.ts
  • examples/prisma-next-cloudflare-worker/src/orm-client/collections.ts
  • examples/prisma-next-cloudflare-worker/src/prisma/contract.d.ts
  • examples/prisma-next-cloudflare-worker/src/prisma/contract.json
  • examples/prisma-next-cloudflare-worker/src/prisma/db.ts
  • examples/prisma-next-cloudflare-worker/src/worker.ts
  • examples/prisma-next-cloudflare-worker/test/cloudflare-test.d.ts
  • examples/prisma-next-cloudflare-worker/test/global-setup.ts
  • examples/prisma-next-cloudflare-worker/test/worker.integration.test.ts
  • examples/prisma-next-cloudflare-worker/tsconfig.json
  • examples/prisma-next-cloudflare-worker/vitest.config.ts
  • examples/prisma-next-cloudflare-worker/wrangler.jsonc
  • package.json
✅ Files skipped from review due to trivial changes (1)
  • examples/prisma-next-cloudflare-worker/src/prisma/contract.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • examples/prisma-next-cloudflare-worker/src/orm-client/collections.ts
  • docs/README.md
  • examples/prisma-next-cloudflare-worker/tsconfig.json
  • examples/prisma-next-cloudflare-worker/src/prisma/db.ts
  • examples/prisma-next-cloudflare-worker/prisma/schema.prisma

Comment thread docs/Serverless Deployment Guide.md Outdated
Comment thread examples/prisma-next-cloudflare-worker/README.md
Comment thread examples/prisma-next-cloudflare-worker/scripts/seed.ts
Comment thread examples/prisma-next-cloudflare-worker/test/global-setup.ts Outdated
saevarb added 3 commits May 6, 2026 15:44
The example's emitted contract.d.ts had drifted from the current
emitter output: enum 'kind' was typed via the generic
CodecTypes['pg/enum@1']['output'] alias, the emitter now narrows
enums to their literal union ('admin' | 'user'). Pure regen, no
schema change. Restores pnpm fixtures:check to passing.
…orker global-setup

Replace the two inline import('vitest').ProvidedContext references in
the GlobalSetupContext interface body with a top-level
import type { ProvidedContext } from 'vitest'. Pure stylistic cleanup
— both forms are type-only and erased at runtime, but the top-level
import is the standard idiom and the module augmentation below
continues to merge correctly into it.
…uide

Two surgical edits to surface the production-only cursor hang before
a reader copy-pastes the example:

1. Qualify the M1 audit's 'pg + pg-cursor work end-to-end' claim
   to its actual scope (localhost Postgres / miniflare emulator),
   and add a 'Production caveat — read this before deploying' note
   right after, naming the cursor: { disabled: true } workaround
   and linking to the full Known Limitations diagnostic.

2. Update the cursor comment in the Module-scope code-shape block
   from a vague 'optional — defaults to enabled' to an explicit
   'REQUIRED if your origin is behind Cloudflare Hyperdrive' note.
   A reader copy-pasting the example now sees the warning at the
   point of decision rather than 170 lines later.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
docs/Serverless Deployment Guide.md (1)

47-47: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to the architecture code fence (markdownlint MD040).

Line 47 opens a fenced block without a language, which still triggers MD040.

Suggested fix
-```
+```text
 ┌─────────────────┐      ┌────────────────┐      ┌─────────────────┐
 │ Worker isolate  │ ───→ │   Hyperdrive   │ ───→ │ Origin Postgres │
 ...
-```
+```
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/Serverless` Deployment Guide.md at line 47, Replace the opening
architecture fenced block so it includes a language tag to satisfy markdownlint
MD040: change the lone ``` that begins the ASCII diagram in the Serverless
Deployment Guide to ```text (i.e., update the architecture code fence opening
near the ASCII diagram) so the fence becomes ```text and the closing fence
remains ```; no other content changes required.
🧹 Nitpick comments (1)
examples/prisma-next-cloudflare-worker/test/global-setup.ts (1)

58-58: ⚡ Quick win

Use the shared Postgres startup timeout constant here.

Hardcoding 15_000/15s will drift from the repo's test timeout conventions. Pull this from the shared timeout helper so the deadline and error message stay aligned.

Based on learnings, avoid hardcoded timeout numbers in Prisma-next tests and use timeouts.spinUpPpgDev for PostgreSQL server startup.

Also applies to: 78-78

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/prisma-next-cloudflare-worker/test/global-setup.ts` at line 58,
Replace the hardcoded 15000ms deadline with the shared timeout constant: use
timeouts.spinUpPpgDev to compute the deadline (e.g., Date.now() +
timeouts.spinUpPpgDev) wherever the test sets const deadline (refer to the
deadline variable in global-setup.ts) and update any related error messages to
reference the same constant; also change the second occurrence noted around the
other deadline usage so both places use timeouts.spinUpPpgDev for consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/prisma-next-cloudflare-worker/test/global-setup.ts`:
- Around line 77-79: The throw in global-setup.ts exposes full Postgres URL
(databaseUrl) including credentials; change the code that constructs error
messages (the throw new Error at the current diff and the similar occurrence
around line 148) to use a redacted version of databaseUrl instead of the raw
string: create a small helper or inline mask that replaces the user:password@
portion (or everything between "://" and the first "/" or "@" as appropriate)
with "[REDACTED_CREDENTIALS]" and use that maskedDatabaseUrl in the thrown Error
and any logs while still including lastErr (keep lastErr.message or
String(lastErr)); update both the throw new Error(...) sites to reference the
masked value.

---

Duplicate comments:
In `@docs/Serverless` Deployment Guide.md:
- Line 47: Replace the opening architecture fenced block so it includes a
language tag to satisfy markdownlint MD040: change the lone ``` that begins the
ASCII diagram in the Serverless Deployment Guide to ```text (i.e., update the
architecture code fence opening near the ASCII diagram) so the fence becomes
```text and the closing fence remains ```; no other content changes required.

---

Nitpick comments:
In `@examples/prisma-next-cloudflare-worker/test/global-setup.ts`:
- Line 58: Replace the hardcoded 15000ms deadline with the shared timeout
constant: use timeouts.spinUpPpgDev to compute the deadline (e.g., Date.now() +
timeouts.spinUpPpgDev) wherever the test sets const deadline (refer to the
deadline variable in global-setup.ts) and update any related error messages to
reference the same constant; also change the second occurrence noted around the
other deadline usage so both places use timeouts.spinUpPpgDev for consistency.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 7560d23e-101e-4a46-865a-9b87deb6e7cd

📥 Commits

Reviewing files that changed from the base of the PR and between 0ef020a and dfcefd9.

📒 Files selected for processing (2)
  • docs/Serverless Deployment Guide.md
  • examples/prisma-next-cloudflare-worker/test/global-setup.ts

Comment on lines +77 to +79
throw new Error(
`[global-setup] Postgres at ${databaseUrl} unreachable after 15s. Did you run \`pnpm db:up\`? Last error: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Redact the connection string before logging or throwing it.

These messages currently emit the full Postgres URL, which includes credentials. That will leak secrets into CI logs and failure output.

🔒 Minimal fix
+function describeDatabase(connectionString: string): string {
+  const url = new URL(connectionString);
+  return `${url.hostname}:${url.port}${url.pathname}`;
+}
+
   throw new Error(
-    `[global-setup] Postgres at ${databaseUrl} unreachable after 15s. Did you run \`pnpm db:up\`? Last error: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`,
+    `[global-setup] Postgres at ${describeDatabase(databaseUrl)} unreachable after 15s. Did you run \`pnpm db:up\`? Last error: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`,
   );
...
-  console.log(`[global-setup] connecting to Postgres at ${databaseUrl}`);
+  console.log(`[global-setup] connecting to Postgres at ${describeDatabase(databaseUrl)}`);

Also applies to: 148-148

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/prisma-next-cloudflare-worker/test/global-setup.ts` around lines 77
- 79, The throw in global-setup.ts exposes full Postgres URL (databaseUrl)
including credentials; change the code that constructs error messages (the throw
new Error at the current diff and the similar occurrence around line 148) to use
a redacted version of databaseUrl instead of the raw string: create a small
helper or inline mask that replaces the user:password@ portion (or everything
between "://" and the first "/" or "@" as appropriate) with
"[REDACTED_CREDENTIALS]" and use that maskedDatabaseUrl in the thrown Error and
any logs while still including lastErr (keep lastErr.message or
String(lastErr)); update both the throw new Error(...) sites to reference the
masked value.

saevarb added 2 commits May 6, 2026 16:18
The cloudflare-worker example's vitest globalSetup reads
WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE (or its
CLOUDFLARE_* alias) to discover the local Postgres origin. CI
sets that var at the job level, but turbo's test task config only
declared TEST_TIMEOUT_MULTIPLIER in its env list — turbo strips
everything else from the spawned task environment for cache-key
determinism, so vitest never saw the URL. globalSetup threw,
vitest reported 'no test files found, exiting with code 1'.

This was a latent bug from the M3 CI wiring (5f28201); the
sign-off was based on local runs. Add both the WRANGLER_* and
CLOUDFLARE_* var names to passThroughEnv on the test task — they
forward to the task without invalidating the cache (the URL
controls which DB the tests reach, not what the tests test).

Verified locally by running pnpm test:examples with the env var
set; globalSetup now resolves the URL and gets past resolveDatabaseUrl
to ensureContainerReady, where it would have been blocked previously.
@saevarb saevarb merged commit 5914e1b into main May 7, 2026
16 checks passed
@saevarb saevarb deleted the tml-2369-ppg-add-a-hyperdrive-driver branch May 7, 2026 06:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants