Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6f933c0
chore(workspace): pin @prisma/ppg 1.0.1 in pnpm catalog
SevInf May 29, 2026
5361d9e
feat(driver-ppg-serverless): scaffold package with placeholder runtime
SevInf May 29, 2026
af4eef1
docs(driver-ppg-serverless): add Slice-2-aware README shell
SevInf May 29, 2026
4da8421
fix(driver-ppg-serverless): replace transient slice IDs in runtime + …
SevInf May 29, 2026
25edfb1
chore(projects): scaffold ppg-serverless project artifacts
SevInf May 29, 2026
8278a81
chore(driver-ppg-serverless): align biome.jsonc schema with repo 2.4.15
SevInf Jun 2, 2026
6e2341d
feat(driver-ppg-serverless): implement one-shot session driver + erro…
SevInf Jun 2, 2026
924f6a7
fix(driver-ppg-serverless): replace transient project IDs in JSDoc + …
SevInf Jun 2, 2026
bcf3083
refactor(driver-ppg-serverless): extract PpgServerlessQueryable abstr…
SevInf Jun 2, 2026
635b270
test(driver-ppg-serverless): exercise long-lived connection + transac…
SevInf Jun 2, 2026
ba3383a
fix(driver-ppg-serverless): describe current acquireConnection wrappe…
SevInf Jun 2, 2026
871a186
feat(prisma-postgres-serverless): scaffold facade package with placeh…
SevInf Jun 2, 2026
c82bc6f
feat(prisma-postgres-serverless): wire runtime factory through driver…
SevInf Jun 2, 2026
1fa7b16
test(prisma-postgres-serverless): exercise facade wiring + end-to-end…
SevInf Jun 2, 2026
dac0382
feat(test-utils): surface server.ppg.url as DevDatabase.ppgUrl
SevInf Jun 2, 2026
848ac2b
chore(projects): accrete ppg-serverless slice 2-6 artifacts + halt notes
SevInf Jun 2, 2026
382002e
fix(driver-ppg-serverless): align manifest + lift coverage to thresholds
SevInf Jun 2, 2026
1cf3d84
feat(ppg-serverless): re-export TCP control surface through driver + …
SevInf Jun 2, 2026
0734f89
test(prisma-postgres-serverless): cloud ORM round-trip via Management…
SevInf Jun 2, 2026
c97ba9c
feat(ppg-serverless): hydrate array columns + cloud ORM end-to-end ve…
SevInf Jun 3, 2026
2f9d97a
chore(projects): accrete ppg-serverless slice 6 artifacts after D3 Ph…
SevInf Jun 3, 2026
49eb83d
docs(ppg-serverless): substantive READMEs + Package-Layering registra…
SevInf Jun 3, 2026
0c350e8
docs(drive,test): record wire-compat overlay + prep ppg-serverless cl…
SevInf Jun 3, 2026
64affef
chore: close out ppg-serverless project; delete transient artefacts
SevInf Jun 3, 2026
0f8d8b5
refactor(prisma-postgres-serverless): drop facade-level binding valid…
SevInf Jun 3, 2026
2b88c4a
refactor(driver-ppg-serverless): drop unused ownsClient field on boun…
SevInf Jun 4, 2026
e4febd4
refactor(prisma-postgres-serverless): accept PpgBinding directly; dro…
SevInf Jun 4, 2026
c011409
docs(ppg-serverless): trim comments that describe implementation
SevInf Jun 4, 2026
8b050f9
style(prisma-postgres-serverless): use named sql/orm imports
SevInf Jun 4, 2026
abe9a1d
refactor(sql-orm-client,ppg-serverless): export OrmClient type, drop …
SevInf Jun 4, 2026
39ff543
fix(prisma-postgres-serverless): drop import-rename `as` to satisfy l…
SevInf Jun 4, 2026
cf5a2e6
fix(ppg-serverless): address CodeRabbit review comments
SevInf Jun 4, 2026
e8a853e
fix(ppg-serverless): lift driver branch coverage back over 95% threshold
SevInf Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@ jobs:
runs-on: ubuntu-latest
env:
TEST_TIMEOUT_MULTIPLIER: 2
# Exposed only to runs from prisma/prisma-next (not fork PRs). The
# `Require PPG service token` step below hard-fails own-repo runs that
# are missing the secret; the cloud-PPG integration test itself
# `describe.skipIf`s when the var is empty (fork PRs, local).
PRISMA_POSTGRES_SERVICE_TOKEN: ${{ secrets.PRISMA_POSTGRES_SERVICE_TOKEN }}
services:
postgres:
image: postgres:15
Expand All @@ -263,6 +268,23 @@ jobs:
- name: Build (restored from Turbo cache)
if: needs.changes.outputs.inert != 'true'
run: pnpm build
- name: Require PPG service token on own-repo PR runs
# Fork PRs can't access repo secrets — skip this gate there and let
# the integration test `describe.skipIf` handle it. On own-repo PR
# runs the secret MUST be configured, otherwise the cloud-PPG
# integration test would silently skip and AC-4 would go uncovered.
if: |
needs.changes.outputs.inert != 'true' &&
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
env:
TOKEN: ${{ secrets.PRISMA_POSTGRES_SERVICE_TOKEN }}
run: |
if [ -z "$TOKEN" ]; then
echo "::error::PRISMA_POSTGRES_SERVICE_TOKEN is not configured in repo secrets — required for the cloud-PPG integration test on prisma/prisma-next PRs."
exit 1
fi
echo "PPG service token is configured."
- name: Run Integration tests
if: needs.changes.outputs.inert != 'true'
run: pnpm test:integration
Expand Down
66 changes: 66 additions & 0 deletions architecture.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,30 @@
"layer": "drivers",
"plane": "runtime"
},
{
"glob": "packages/3-targets/7-drivers/ppg-serverless/src/core/**",
"domain": "targets",
"layer": "drivers",
"plane": "shared"
},
{
"glob": "packages/3-targets/7-drivers/ppg-serverless/src/ppg-driver.ts",
"domain": "targets",
"layer": "drivers",
"plane": "shared"
},
{
"glob": "packages/3-targets/7-drivers/ppg-serverless/src/normalize-error.ts",
"domain": "targets",
"layer": "drivers",
"plane": "shared"
},
{
"glob": "packages/3-targets/7-drivers/ppg-serverless/src/exports/runtime.ts",
"domain": "targets",
"layer": "drivers",
"plane": "runtime"
},
{
"glob": "packages/3-extensions/postgres/src/config/**",
"domain": "extensions",
Expand Down Expand Up @@ -312,6 +336,48 @@
"layer": "adapters",
"plane": "shared"
},
{
"glob": "packages/3-extensions/prisma-postgres-serverless/src/exports/config.ts",
"domain": "extensions",
"layer": "adapters",
"plane": "shared"
},
{
"glob": "packages/3-extensions/prisma-postgres-serverless/src/exports/contract-builder.ts",
"domain": "extensions",
"layer": "adapters",
"plane": "shared"
},
{
"glob": "packages/3-extensions/prisma-postgres-serverless/src/exports/family.ts",
"domain": "extensions",
"layer": "adapters",
"plane": "shared"
},
{
"glob": "packages/3-extensions/prisma-postgres-serverless/src/exports/migration.ts",
"domain": "extensions",
"layer": "adapters",
"plane": "migration"
},
{
"glob": "packages/3-extensions/prisma-postgres-serverless/src/exports/runtime.ts",
"domain": "extensions",
"layer": "adapters",
"plane": "runtime"
},
{
"glob": "packages/3-extensions/prisma-postgres-serverless/src/runtime/**",
"domain": "extensions",
"layer": "adapters",
"plane": "runtime"
},
{
"glob": "packages/3-extensions/prisma-postgres-serverless/src/exports/target.ts",
"domain": "extensions",
"layer": "adapters",
"plane": "shared"
},
{
"glob": "packages/3-extensions/mongo/src/config/**",
"domain": "extensions",
Expand Down
16 changes: 13 additions & 3 deletions docs/architecture docs/Package-Layering.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ The targets domain (`packages/3-targets/`) contains concrete target extension pa
|-- 6-adapters/postgres (multi-plane: shared, migration, runtime)
| |-- → @prisma-next/adapter-postgres (adapter with control/runtime entrypoints)
|-- 7-drivers/postgres (runtime plane)
|-- → @prisma-next/driver-postgres (driver implementation)
| |-- → @prisma-next/driver-postgres (TCP/pg driver implementation)
|-- 7-drivers/ppg-serverless (multi-plane: runtime, migration)
|-- → @prisma-next/driver-ppg-serverless (PPG WebSocket driver; runtime entry, migration entry re-exports driver-postgres/control)
```

### Mongo Targets Domain
Expand All @@ -158,7 +160,11 @@ The extensions domain (`packages/3-extensions/`) contains ecosystem extensions a
|-- sql-orm-client/ (runtime plane)
| |-- → @prisma-next/sql-orm-client
|-- pgvector/ (multi-plane)
|-- → @prisma-next/extension-pgvector
| |-- → @prisma-next/extension-pgvector
|-- postgres/ (multi-plane: shared, runtime, migration)
| |-- → @prisma-next/postgres (long-lived Node-process facade; TCP via @prisma-next/driver-postgres)
|-- prisma-postgres-serverless/ (multi-plane: shared, runtime, migration)
|-- → @prisma-next/prisma-postgres-serverless (edge/serverless facade; WebSocket via @prisma-next/driver-ppg-serverless)
```

### Layer Structure
Expand Down Expand Up @@ -303,7 +309,8 @@ Database adapters, drivers, and targets (dialects) live in the Targets domain as
- `src/exports/runtime.ts` → runtime plane (runtime factory)

**Drivers (Runtime Plane):**
- `packages/3-targets/7-drivers/postgres/` → `@prisma-next/driver-postgres` - Postgres driver
- `packages/3-targets/7-drivers/postgres/` → `@prisma-next/driver-postgres` - Postgres TCP driver via `pg`
- `packages/3-targets/7-drivers/ppg-serverless/` → `@prisma-next/driver-ppg-serverless` - Prisma Postgres WebSocket driver via `@prisma/ppg`; ships `./runtime` (substantive) + `./control` (re-export of `@prisma-next/driver-postgres/control`)

## Naming Conventions

Expand Down Expand Up @@ -358,8 +365,11 @@ Database adapters, drivers, and targets (dialects) live in the Targets domain as
| `packages/3-targets/3-targets/postgres/` | `@prisma-next/target-postgres` |
| `packages/3-targets/6-adapters/postgres/` | `@prisma-next/adapter-postgres` |
| `packages/3-targets/7-drivers/postgres/` | `@prisma-next/driver-postgres` |
| `packages/3-targets/7-drivers/ppg-serverless/` | `@prisma-next/driver-ppg-serverless` |
| `packages/3-extensions/sql-orm-client/` | `@prisma-next/sql-orm-client` |
| `packages/3-extensions/pgvector/` | `@prisma-next/extension-pgvector` |
| `packages/3-extensions/postgres/` | `@prisma-next/postgres` |
| `packages/3-extensions/prisma-postgres-serverless/` | `@prisma-next/prisma-postgres-serverless` |

## Dependency Rules

Expand Down
4 changes: 4 additions & 0 deletions drive/calibration/dod.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,8 @@ Beyond the canonical project DoD items:

Walk `design-decisions.md` for any decision that hasn't migrated to an ADR. If unmigrated decisions exist that are architecturally durable (cross-cutting, hard to reverse, affect future work), block close-out until they have ADRs — closing with un-ADR'd architectural decisions is a known close-out failure mode.

### Substrate-substitution wire-compat coverage (added 2026-06-03 retro)

When a project introduces a new driver / adapter / runtime substrate that claims wire-compat parity with an existing one (e.g. a serverless driver substituting for the TCP driver against the same database family), **live-wire integration coverage against the substituted backend is a slice-DoD prerequisite, not a project-DoD nice-to-have**. Mocked-driver tests can verify the new substrate's own logic but cannot see column-hydration, error-shape, or protocol-framing gaps at the wire boundary by construction — they shape the row themselves before it crosses the seam. The framework adapter layer often banks on per-column behaviour (e.g. `pg`'s native `text[]` -> JS-array hydration) that mocked tests trivially satisfy but the new substrate may not. Land the live-wire integration test in the slice that introduces the substrate, not in a later validation slice; otherwise wire-compat regressions surface only at project close-out (or first real-user contact) when the cost of pivoting design is highest.

_(Living; add overlays as the team discovers them.)_
208 changes: 208 additions & 0 deletions packages/3-extensions/prisma-postgres-serverless/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# @prisma-next/prisma-postgres-serverless

Edge/serverless-friendly Prisma Postgres facade for Prisma Next. Install this single package to get config, runtime, and the transitive type dependencies needed to author and run a Prisma Postgres app against the `@prisma/ppg` WebSocket client — no `pg` and no TCP transport on the data plane, so the runtime entry is portable to edge runtimes that do not expose raw TCP sockets.

The facade composes the existing Postgres execution stack with a different driver:

- the existing `postgres` target (`@prisma-next/target-postgres`) — same dialect, same migration ops.
- the existing `postgres` adapter (`@prisma-next/adapter-postgres`) — shared SQL lowering.
- the new `@prisma-next/driver-ppg-serverless` driver — WebSocket transport via `@prisma/ppg`.

It is the serverless sibling of [`@prisma-next/postgres`](../postgres/README.md) (the long-lived Node-process facade backed by TCP `pg`). Pick the facade that matches your deployment lifecycle; both expose the same authoring + ORM surface.

## Package Classification

- **Domain**: extensions
- **Layer**: adapters
- **Planes**: shared (`config`, `contract-builder`, `control`, `family`, `target`), runtime (`runtime`), migration (`migration`)

## Quick Start

```typescript
// prisma-next.config.ts
import { defineConfig } from '@prisma-next/prisma-postgres-serverless/config';

export default defineConfig({
contract: './prisma/contract.prisma',
db: { connection: process.env['PPG_URL']! },
});
```

```typescript
// db.ts
import prismaPostgresServerless from '@prisma-next/prisma-postgres-serverless/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };

export const db = prismaPostgresServerless<Contract>({
contractJson,
binding: { kind: 'url', url: process.env['PPG_URL']! },
});
```

### Cloudflare Workers

```typescript
// worker.ts
import prismaPostgresServerless from '@prisma-next/prisma-postgres-serverless/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };

interface Env {
PPG_URL: string;
}

export default {
async fetch(_req: Request, env: Env): Promise<Response> {
const db = prismaPostgresServerless<Contract>({
contractJson,
binding: { kind: 'url', url: env.PPG_URL },
});
try {
const rows = await db.orm.User.findMany();
return Response.json(rows);
} finally {
await db.close();
}
},
};
```

The PPG-compatible URL form is `postgres://identifier:key@db.prisma.io:5432/postgres?sslmode=require`. The `prisma+postgres://accelerate.prisma-data.net/?api_key=…` form returned by Prisma Accelerate / data-proxy is **not** a PPG URL — it carries a different wire protocol (GraphQL over HTTPS) and is rejected by `@prisma/ppg` upstream of the facade. If you provision via the Prisma Data Platform Management API, take the URL from `endpoints.pooled.connectionString`.

## Runtime environments

The runtime entry uses only `fetch` and `WebSocket` at runtime (transitively, through `@prisma/ppg`). Tested under:

- Node.js 20+
- Cloudflare Workers
- Vercel Edge Functions
- Deno / Deno Deploy
- Bun (Node + edge)

## Exports

| Subpath | Status | Notes |
|---|---|---|
| `./runtime` | Substantive | `prismaPostgresServerless<Contract>(options)` factory. Returns a client with `sql` / `orm` / `context` / `runtime()` / `connect()` / `transaction()` / `prepare()` / `close()` / `[Symbol.asyncDispose]`. |
| `./config` | Re-export | `@prisma-next/postgres/config` (`defineConfig`). |
| `./contract-builder` | Re-export | `@prisma-next/postgres/contract-builder` (`defineContract`, `field`, `model`, `rel`, …). |
| `./control` | Re-export | `@prisma-next/postgres/control` (control-plane descriptor + `createPostgresControlClient` for migration tooling). Pulls `pg` into the install graph; never into the runtime bundle. |
| `./family` | Re-export | `@prisma-next/family-sql/pack` (the value passed as `family:` to `defineContract`). |
| `./migration` | Re-export | `@prisma-next/target-postgres/migration` — `Migration` base class, CLI runner, op helpers. |
| `./target` | Re-export | `@prisma-next/target-postgres/pack` (the value passed as `target:` to `defineContract`). |

Compared to `@prisma-next/postgres`, two exports are deliberately absent:

- **No `./serverless`.** This package _is_ the serverless surface; there is no second facade hiding behind a subpath.
- No separate Node / Pool factory — the runtime is always per-call session-based (one `@prisma/ppg` session per top-level call; one long-lived session per `acquireConnection()`), so there is no `pg.Pool` to surface.

## Authoring + ORM

The contract-builder, family, and target re-exports point at the same packages `@prisma-next/postgres` uses, so contracts authored against either facade are interchangeable:

```typescript
import { defineContract, field, model } from '@prisma-next/prisma-postgres-serverless/contract-builder';

export const contract = defineContract(
{ extensionPacks: {} },
({ field: f, model: m }) => ({
models: {
Item: m('Item', {
fields: {
id: f.id.uuidv7(),
name: f.text(),
},
}),
},
}),
);
```

The migration plane runs over a direct TCP connection (re-exported `./control` from `@prisma-next/postgres/control`). Running migrations in CI / locally typically uses the same `prisma-next` CLI tooling against a TCP URL; runtime queries from Workers / Edge use the WebSocket data plane. Both planes target the same Prisma Postgres database.

## Binding variants

The `runtime()` factory takes a `binding` of one of two kinds:

```typescript
// (a) Connection-string URL — the facade constructs and owns the PPG client.
// Array-OID parsers are registered automatically.
const db = prismaPostgresServerless({
contractJson,
binding: { kind: 'url', url: env.PPG_URL },
});

// (b) Pre-built @prisma/ppg Client — the caller owns the lifecycle.
// Wire array parsers in yourself if you read array-typed columns
// (text[], uuid[], int4[], jsonb[], …).
import { client as createPpgClient, defaultClientConfig } from '@prisma/ppg';
import { withArrayParsers } from '@prisma-next/driver-ppg-serverless/runtime';

const config = defaultClientConfig(env.PPG_URL);
const ppgClient = createPpgClient({
...config,
parsers: withArrayParsers(config.parsers ?? []),
});
const db = prismaPostgresServerless({
contractJson,
binding: { kind: 'ppgClient', client: ppgClient },
});
```

## Transactions

`db.transaction(fn)` opens a long-lived session, issues `BEGIN`, runs the callback, then `COMMIT`s on return or `ROLLBACK`s on throw. The callback receives a transaction-scoped `tx` whose `orm` / `sql` / `context` mirror `db`'s top-level surface:

```typescript
await db.transaction(async (tx) => {
await tx.orm.Item.create({ id: crypto.randomUUID(), name: 'alice' });
await tx.orm.Item.create({ id: crypto.randomUUID(), name: 'bob' });
});
// Both rows committed atomically. Throw inside the callback to roll back.
```

## Responsibilities

- Build a static Prisma Postgres execution stack from target, adapter, and driver descriptors.
- Build a typed SQL authoring surface and ORM root from the execution context.
- Forward the caller's `PpgBinding` through to the driver.
- Lazily instantiate runtime resources on first `db.runtime()` or `db.connect(...)` call; memoise so repeated calls return one instance.
- Forward the control / config / contract-builder surfaces from `@prisma-next/postgres` so consumers get a single-import experience.

## Dependencies

- `@prisma/ppg` (via `@prisma-next/driver-ppg-serverless`) — Prisma Postgres WebSocket client.
- `@prisma-next/sql-runtime` — stack / context / runtime primitives.
- `@prisma-next/framework-components/execution` — stack instantiation.
- `@prisma-next/target-postgres` — target descriptor (shared with the long-lived facade).
- `@prisma-next/adapter-postgres` — adapter descriptor (shared with the long-lived facade).
- `@prisma-next/driver-ppg-serverless` — driver descriptor (this facade's defining choice).
- `@prisma-next/postgres` — re-exported for the `./config`, `./contract-builder`, and `./control` surfaces. Pulls `pg` into the install graph through the control re-export, but the runtime bundle stays edge-clean (bundlers tree-shake the unimported `./control` re-export from the `./runtime` entry).
- `@prisma-next/sql-builder`, `@prisma-next/sql-orm-client`, `@prisma-next/sql-contract` — authoring + ORM surfaces.

## Architecture

```mermaid
flowchart TD
App[App Code] --> Client[prisma-postgres-serverless runtime]
Client --> Static[Roots: sql, orm, context, contract]
Client --> Lazy[runtime / connect]

Lazy --> Bind[Resolve binding: url, ppgClient, or binding]
Bind --> NewSession[ppg Client.newSession per call or per connection]
Lazy --> Runtime[createRuntime]

Runtime --> Target[@prisma-next/target-postgres]
Runtime --> Adapter[@prisma-next/adapter-postgres]
Runtime --> Driver[@prisma-next/driver-ppg-serverless]
Runtime --> SqlRuntime[@prisma-next/sql-runtime]
Runtime --> ExecPlane[@prisma-next/framework-components/execution]
```

## Related Docs

- Architecture: [`docs/Architecture Overview.md`](../../../docs/Architecture%20Overview.md)
- Subsystem: [`docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md`](../../../docs/architecture%20docs/subsystems/4.%20Runtime%20%26%20Middleware%20Framework.md)
- Subsystem: [`docs/architecture docs/subsystems/5. Adapters & Targets.md`](../../../docs/architecture%20docs/subsystems/5.%20Adapters%20%26%20Targets.md)
- ADR: [`docs/architecture docs/adrs/ADR 207 - Per-environment facade asymmetry.md`](../../../docs/architecture%20docs/adrs/ADR%20207%20-%20Per-environment%20facade%20asymmetry.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
"extends": "//"
}
Loading
Loading