feat(postgres): per-request facade for serverless runtimes#421
feat(postgres): per-request facade for serverless runtimes#421
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a per-request Postgres facade ( ChangesServerless Postgres Facade
Cloudflare Worker Example, Docs, Tests & CI
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Comment |
There was a problem hiding this comment.
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 winThe package-level responsibilities and architecture still describe only the Node facade.
After adding
@prisma-next/postgres/serverless, these sections still imply every client hasorm,runtime(), memoization, andpg.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 parallelpostgresServerless(...) -> connect({ url }) -> pg.Clientpath.
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
⛔ Files ignored due to path filters (5)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlprojects/cloudflare-hyperdrive-runtime/assets/ac-verification.mdis excluded by!projects/**projects/cloudflare-hyperdrive-runtime/assets/workers-compat-audit.mdis excluded by!projects/**projects/cloudflare-hyperdrive-runtime/plan.mdis excluded by!projects/**projects/cloudflare-hyperdrive-runtime/spec.mdis excluded by!projects/**
📒 Files selected for processing (36)
.github/workflows/ci.ymldocs/README.mddocs/Serverless Deployment Guide.mddocs/architecture docs/ADR-INDEX.mddocs/architecture docs/adrs/ADR 207 - Per-environment facade asymmetry.mdexamples/prisma-next-cloudflare-worker/.env.exampleexamples/prisma-next-cloudflare-worker/.gitignoreexamples/prisma-next-cloudflare-worker/README.mdexamples/prisma-next-cloudflare-worker/biome.jsoncexamples/prisma-next-cloudflare-worker/docker-compose.ymlexamples/prisma-next-cloudflare-worker/package.jsonexamples/prisma-next-cloudflare-worker/prisma-next.config.tsexamples/prisma-next-cloudflare-worker/prisma/schema.prismaexamples/prisma-next-cloudflare-worker/scripts/seed.tsexamples/prisma-next-cloudflare-worker/scripts/setup-schema.tsexamples/prisma-next-cloudflare-worker/src/orm-client/client.tsexamples/prisma-next-cloudflare-worker/src/orm-client/collections.tsexamples/prisma-next-cloudflare-worker/src/prisma/contract.d.tsexamples/prisma-next-cloudflare-worker/src/prisma/contract.jsonexamples/prisma-next-cloudflare-worker/src/prisma/db.tsexamples/prisma-next-cloudflare-worker/src/worker.tsexamples/prisma-next-cloudflare-worker/test/cloudflare-test.d.tsexamples/prisma-next-cloudflare-worker/test/global-setup.tsexamples/prisma-next-cloudflare-worker/test/worker.integration.test.tsexamples/prisma-next-cloudflare-worker/tsconfig.jsonexamples/prisma-next-cloudflare-worker/vitest.config.tsexamples/prisma-next-cloudflare-worker/wrangler.jsoncpackage.jsonpackages/3-extensions/postgres/README.mdpackages/3-extensions/postgres/package.jsonpackages/3-extensions/postgres/src/exports/serverless.tspackages/3-extensions/postgres/src/runtime/postgres-serverless.tspackages/3-extensions/postgres/test/postgres-serverless.test.tspackages/3-extensions/postgres/test/postgres-serverless.types.test-d.tspackages/3-extensions/postgres/tsconfig.jsonpackages/3-extensions/postgres/tsdown.config.ts
|
|
||
| ### Architecture | ||
|
|
||
| ``` |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
| 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 | ||
|
|
||
| ``` |
There was a problem hiding this comment.
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.
| 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 | ||
| ``` |
There was a problem hiding this comment.
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🤖 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.'); |
There was a problem hiding this comment.
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.
| 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 }); |
There was a problem hiding this comment.
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.
| 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), | ||
| }); | ||
| } |
There was a problem hiding this comment.
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.
…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.
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.
…itest-pool-workers locked) (TML-2369)
…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.
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
71db735 to
48df22a
Compare
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-telemetry
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/emitter
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
…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.
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
examples/prisma-next-cloudflare-worker/scripts/setup-schema.ts (1)
37-37:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix stale command hint in setup failure output.
The hint references
pnpm db:dev, but this example exposespnpm 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 winUse the shared Postgres startup timeout here.
The hardcoded
15_000drifts 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-utilstimeout helpers—specificallytimeouts.spinUpPpgDevfor PostgreSQL server startup andtimeouts.databaseOperationfor 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
📒 Files selected for processing (28)
.github/workflows/ci.ymldocs/README.mddocs/Serverless Deployment Guide.mddocs/architecture docs/ADR-INDEX.mddocs/architecture docs/adrs/ADR 207 - Per-environment facade asymmetry.mdexamples/prisma-next-cloudflare-worker/.env.exampleexamples/prisma-next-cloudflare-worker/.gitignoreexamples/prisma-next-cloudflare-worker/README.mdexamples/prisma-next-cloudflare-worker/biome.jsoncexamples/prisma-next-cloudflare-worker/docker-compose.ymlexamples/prisma-next-cloudflare-worker/package.jsonexamples/prisma-next-cloudflare-worker/prisma-next.config.tsexamples/prisma-next-cloudflare-worker/prisma/schema.prismaexamples/prisma-next-cloudflare-worker/scripts/seed.tsexamples/prisma-next-cloudflare-worker/scripts/setup-schema.tsexamples/prisma-next-cloudflare-worker/src/orm-client/client.tsexamples/prisma-next-cloudflare-worker/src/orm-client/collections.tsexamples/prisma-next-cloudflare-worker/src/prisma/contract.d.tsexamples/prisma-next-cloudflare-worker/src/prisma/contract.jsonexamples/prisma-next-cloudflare-worker/src/prisma/db.tsexamples/prisma-next-cloudflare-worker/src/worker.tsexamples/prisma-next-cloudflare-worker/test/cloudflare-test.d.tsexamples/prisma-next-cloudflare-worker/test/global-setup.tsexamples/prisma-next-cloudflare-worker/test/worker.integration.test.tsexamples/prisma-next-cloudflare-worker/tsconfig.jsonexamples/prisma-next-cloudflare-worker/vitest.config.tsexamples/prisma-next-cloudflare-worker/wrangler.jsoncpackage.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
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.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
docs/Serverless Deployment Guide.md (1)
47-47:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd 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 winUse the shared Postgres startup timeout constant here.
Hardcoding
15_000/15swill 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.spinUpPpgDevfor 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
📒 Files selected for processing (2)
docs/Serverless Deployment Guide.mdexamples/prisma-next-cloudflare-worker/test/global-setup.ts
| 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)}`, | ||
| ); |
There was a problem hiding this comment.
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.
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.
closes TML-2369
Intent
Make Prisma Next deployable on serverless / per-request Postgres runtimes by shipping a sibling
postgresServerlessfacade alongside the existingpostgres()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
./serverlessentrypointnodejs_compat+ Hyperdrive binding declarationpg)vitest-pool-workersintegration suite underworkerdThe story
Audit first; pick the wrapper topology before writing code. A throwaway spike confirmed
pg+pg-cursorwork inworkerdundernodejs_compat(cursor open / read-batches / early-break/ close all clean). The existingPostgresDirectDriverImpl(pgClientPostgresBindingkind) already implements the per-request lifecycle Hyperdrive needs — lazyclient.connect(), nopg.Pool, explicitclient.end(), mutex-serializedacquireConnectionfor transaction affinity. Conclusion: the gap is at the wrapper level, not the driver level. No driver-layer changes shipped in this PR.Ship the wrapper as a sibling facade, not a new package. A new entrypoint
@prisma-next/postgres/serverlessexportspostgresServerless<Contract>({ contractJson, extensions, middleware }). Construction shape mirrors the existingpostgres()factory; the runtime surface is intentionally narrower —sql,context,stack,contract, andconnect(), withorm,runtime(), andtransaction()deliberately omitted.db.connect({ url })returns a freshRuntime & AsyncDisposableper call (no closure cache); each connection routes through the existingpgClientdriver path with a freshly constructedpg.Client. The runtime carries[Symbol.asyncDispose]so per-request teardown isawait using-clean.Build the example as the dogfood.
examples/prisma-next-cloudflare-workerdeploys viawrangler.jsoncwithnodejs_compat, points at a Hyperdrive binding whoselocalConnectionStringresolves to a local Docker Postgres, and exercises all three query surfaces (SQL DSL, ORM,withTransaction) plus a cursor-streaming + early-breakpath. The integration test runs undervitest-pool-workers(workerd in-process) and is wired into CI with a Docker Postgres bring-up step.Settle the architecture decision in a durable place. The asymmetry between
postgres()(long-lived) andpostgresServerless()(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(underprojects/) 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
postgresServerlessfacade for per-request Postgres runtimes. Construction shape mirrors the existingpostgres()factory; the returned client exposessql,context,stack,contract, andconnect(), and intentionally omitsorm,runtime(), andtransaction()— those convenience surfaces depend on a closure-cached runtime that is unsafe in per-request lifecycles.fetchtransaction-state contamination on a sharedpg.Client, and no clean shutdown idiom — a long-lived ergonomic reused under a per-request lifecycle. Forcing the user throughawait using runtime = await db.connect(...)makes the lifetime explicit at every call site and makes[Symbol.asyncDispose]carry teardown.pg, prove noPoolis constructed, prove distinct runtimes perconnect()call, prove negative runtime probe of the absent convenience members), and packages/3-extensions/postgres/test/postgres-serverless.types.test-d.ts — lines 15–84 (negative type tests + structural mirror withpostgres()'s option keys).Adds a deployable Cloudflare Worker example exercising the facade end-to-end.
examples/prisma-next-cloudflare-worker/mirrorsexamples/prisma-next-demo(minus pgvector) and runs against a Hyperdrive-fronted local Postgres. Module-scopedb = 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.vitest-pool-workers:/healthboot, SQL DSL, ORMfindMany, ORM relation traversal, transaction commit, transaction rollback, cursor early-break)Cursor streaming is enabled by default on the per-request facade and proven against materialization. The serverless facade leaves
cursorunset (defaulting to enabled in the driver); the long-livedpostgres()facade keeps its existingcursor: { disabled: true }default. The cursor integration test seeds 10 000 posts and usespg_stat_statementsas 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).pg-cursoris built for, and exactly what isolate memory pressure makes a buffered fetch dangerous for. The default reflects the dominant shape; the option is exposed for parity.rowsTransmittedfrom apg_stat_statementsreset → SELECT → SUM(rows) probe), packages/3-extensions/postgres/src/runtime/postgres-serverless.ts — lines 148–150 (cursor passthrough only when supplied; default leaves the driver'scursor enabledpath)expect(body.rowsTransmitted).toBeLessThan(500)); examples/prisma-next-cloudflare-worker/test/global-setup.ts — lines 131–142 (10 000-rowgenerate_seriesseed)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;
AsyncDisposableruntime; 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.spec.mdunderprojects/is transient. ADR 207 is the durable home for the architectural rationale; it'll be the reference when MySQL or MongoDB serverless support eventually lands and needs to make the same per-environment shape decision. The deployment guide is what users find when they ask "how do I deploy Prisma Next on Workers / Lambda / Vercel / Deno / Bun?".pnpm lint:deps/pnpm lint:docsclean.Compatibility / migration / risk
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.tsis empty; the existing 27-casepostgres.test.tssuite passes unchanged.PostgresDirectDriverImpl(pgClientbinding) already implements the lifecycle the serverless facade needs.pg,pg-protocol,pg-types,pg-cursor,pg-pool(statically imported by@prisma-next/driver-postgreseven thoughpostgresServerlessdoes not construct aPoolat runtime),pg-cloudflare(auto-pulled bypgwhen running underworkerd), and@cloudflare/unenv-presetpolyfills.wrangler dev+ local Docker Postgres. Production cold-start over real Hyperdrive will be slower; re-measure duringwrangler deploysmoke verification.Follow-ups / open questions
wrangler deploysmoke) — 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.@@base + @@mapdiscriminator schemas reference non-existent columns. Pre-existing drift in the demo'sTask.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.pg.Clientfailure-mode prose ("cannot interleave queries; the ones following the first race for the same wire") is loose at the protocol level —pg.Clientqueues client-side (FIFO), so the actual hazards under shared-client-across-fetches are head-of-line blocking + cross-fetchtransaction-state contamination (BEGIN/COMMIT bracketing across concurrent invocations). Decision and rationale stand; only the prose framing is imprecise. Non-blocking; tighten on next pass.[Symbol.asyncDispose]to the Nodepostgres()facade. Out of scope here; useful for Node CLIs that wantawait using runtime = await db.connect({ url }). Recommend filing a separate ticket.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
orm/runtime()/transaction) on the serverless facade. Deliberately omitted (see ADR 207). Asymmetry is a feature.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 onnode:async_hookssemantics.pgwith another underlying library (e.g.postgres.js).Summary by CodeRabbit
New Features
Documentation
Examples
Tests
Infrastructure