Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
154 changes: 154 additions & 0 deletions .drive/projects/prisma-next-data-contract/design-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Design notes — Prisma Next data contract

Settled in an operator design session (2026-07-11). The ADR draft
(`docs/design/90-decisions/ADR-0022-*.md`) is the durable record; these notes
carry the working detail and the deferred extension design.

## Principles

- **Opt-out stays real.** Bare `postgres()` is untouched — the `any` of data
deps, same role `http()` plays for communication. Prisma Next is a second,
parallel primitive; a project or service that doesn't import it never loads
`@prisma-next/*` or `pg` at runtime.
- **Schema checking is build/deploy-time, not runtime.** The deploy is the
authoritative check (migrate to the contract hash or fail). The runtime
binding does no schema verification — it just builds the client — so a
running service can never be crashed by a marker check. Runtime marker
verification is explicitly disabled, not warn-only.
- **Deploys never run synthesized plans.** The deploy step migrates along the
authored migration graph only, and fails if the graph has no path to the
target hash. `dbUpdate`-style diff-and-apply is not used against deployed
databases.

## The model

Two ends, one contract:

```ts
// resource end — the owning system
import contractJson from './contract.json' // consume: data
import type { Contract } from './contract.d' // consume: types
const contract = pnContract<Contract>(contractJson)
const db = provision('database', pnPostgres({
name: 'database',
contract,
config: './prisma-next.config.ts', // locate: a PATH string, deploy-only
}))

// dep end — each consumer
deps: { db: pnPostgres(contract) } // binding: PostgresClient<Contract>
```

- **Consume the contract; locate the config.** The resource carries two
different things by two different doors. The **contract** enters as its
emitted artifact (`contract.json` + `contract.d.ts`) — consumed by the
runtime and the type system, lightweight, safe to bundle. The
**`prisma-next.config.ts`** enters as a **path string** — the deploy
migration step reads it to find the migrations directory. The app build
**never imports** the config; importing it pulls PN's CLI / migration engine
/ source-providers into the bundle. `connection` is gone entirely — there is
no config object to carry a connection on.
- **Single contract per database (v1).** The user authors one contract that
serves all consuming systems. Every consumer sees the full contract type —
no least-privilege slices yet.
- **Compile-time check:** the dep's `required` contract and the resource's
provided contract are the same emitted `Contract` type; the branded
`storageHash` literal makes assignability exact-version equality.
`satisfies()` mirrors it at Load as a hash-equality check.
- **Binding is the typed client**, constructed in hydrate:
`postgres<Contract>({ contractJson, url })` from
`@prisma-next/postgres/runtime` — no `verifyMarker` (no runtime schema
check). Lazy pool, rides node-postgres. This amends ADR-0015's "pack ships
no driver" for this dep kind: Prisma Next is framework-blessed like rpc, so
the contract alone *can* construct the client.
- **Type flow by authoring mode:** TS-authored contracts carry their own types
through the config import; PSL-first passes the emitted `contract.d.ts` type
explicitly as the type parameter. Support and document both.

## Deploy lowering

Per PN-postgres resource, after DB provisioning, an Alchemy resource:

1. `readMarker()` on the live DB → compare marker `storageHash` to the
contract's.
2. Equal → no-op (idempotent redeploy).
3. Different → PN `migrate`: walk the authored migration graph from the
marker's hash to the target hash. Resume-safe; marker writes are atomic
with apply.
4. Fail the deploy on: no path through the graph, destructive step without
explicit opt-in (`acceptDataLoss` stays off), or runner failure. A failed
apply leaves marker and diff unchanged.

The plan-mode operation list is the Alchemy diff preview. Migration files are
read from disk relative to the config — deploys run from a machine/CI that has
the workspace, so the files are present where the lowering runs.

## Packaging

In `@prisma/app-cloud` behind a dedicated subpath entry
(`@prisma/app-cloud/prisma-next`), not re-exported from the index — so the
`@prisma-next/postgres` + `pg` dependency tree loads only when imported.
Install weight is shared; runtime weight isn't. (Operator: no new npm
packages. Folding `@prisma/app-rpc` into app-cloud is a separate move, out of
scope here.)

## Deferred: the multi-contract / contract-space model

Worked out in the design session; deferred because PN's multi-peer-app-space
support (ADR 212's "monorepo aggregator" case) has unproven edges. When picked
up:

- Resource declares the set of contracts it hosts:
`pnPostgres({ name, contracts: [sales, auth] })` — the aggregate in code;
the owner consents to each slice; wiring typechecks membership.
- Each consumer's slice maps to a PN **contract space**; disjointness is
PN-verified (`STORAGE_ELEMENT_CONFLICT`); apply ordering is PN's.
- **Space id must live on the contract declaration, never derived from
topology names** — the id is what links versions of a contract over time
(marker at hash X is this contract's predecessor, not a stranger claiming
overlapping tables). Deriving from a system/dep name makes renames read as
conflicts.
- Prerequisite spike: confirm PN handles multiple peer app-authored spaces in
one database end-to-end.

## Deferred: dev-time

Parked. Intended shape: `prisma dev` serves identical copies of the management
API locally; Alchemy then treats the local machine as one more deploy target
and the lowering doesn't change at all. Nothing to design in this project.

## Alternatives considered

- **Binding = `{ url, contractJson }`, app constructs the client** — keeps
packs driver-free (ADR-0015 as written) at the cost of one line of app code
and the framework not constructing the typed client. Rejected: "data
contracts are Prisma Next" is a README-level framework decision; PN is
blessed the way rpc is.
- **Sibling npm package for the primitive** — cleanest dependency isolation,
rejected by operator (package proliferation); subpath entry achieves the
runtime isolation that matters.
- **Deriving the aggregate from wiring** (no contracts on the resource
declaration) — superseded by single-contract v1; in the multi-contract
extension the explicit declaration wins anyway (owner consent, visible
edits, lowering reads the node not the graph).
- **`dbUpdate` at deploy** — synthesized plans against production databases;
rejected outright. Authored `migrate` only.

## Open questions

- Factory name. `pnPostgres` is a placeholder; PN becomes **Prisma Data** at
GA, so the name will churn. Decide before slice 1 merges.
- `@prisma-next/*` is 0.x (0.14.0 on npm) and fast-moving — pinning strategy
and breakage tolerance.
- Exact `verifyMarker` warn-only semantics — confirm PN's option supports
warn-don't-throw, or wrap it.

## References

- ADR draft: `docs/design/90-decisions/ADR-0022-*.md`
- PN surfaces: `@prisma-next/postgres` `/contract-builder`, `/runtime`,
`/control`, `/config`; contract spaces ADR 212; `migrate` /
`readMarker` / `readLedger` in the control API.
- Framework surfaces: `packages/app-cloud/src/postgres.ts` (bare primitive),
`packages/app-cloud/src/control.ts` (lowering), ADR-0013/0015 (dep model,
bindings).
64 changes: 64 additions & 0 deletions .drive/projects/prisma-next-data-contract/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Project plan — Prisma Next data contract

## Summary

Two slices in a stack. Slice 1 lands the ADR and the typed primitive with its
runtime behavior proven against a real local Postgres; slice 2 lands the
deploy-time migration step and proves the whole path live on Prisma Cloud via
an example conversion.

**Spec:** `.drive/projects/prisma-next-data-contract/spec.md`

## Slices

### Slice 1: typed primitive (`typed-primitive`)

**Outcome:** `pnPostgres` (name settled by then) exists in
`@prisma/app-cloud/prisma-next`: resource end takes `{ name, config }` from a
`prisma-next.config.ts` import; dep end resolves to a
`PostgresClient<Contract>` built in hydrate with warn-only marker
verification; compile-time assignability is exact on `storageHash` and
`satisfies()` mirrors it. ADR-0022 ships in this PR.

**Slice DoD:** unit tests for contract assignability/`satisfies`; integration
test against a real local Postgres (existing harness) proving the typed
client round-trips and a mismatched marker warns without throwing; bare
`postgres()` suite untouched; subpath entry verified not to load
`@prisma-next/*` from the index import.

**Builds on:** —
**Hands to:** stable primitive + `Contract` type flow + ADR merged, for the
lowering to target.

**Linear:** [TML-3009](https://linear.app/prisma-company/issue/TML-3009/slice-1-typed-pnpostgres-primitive-adr-0021)

### Slice 2: deploy migrate lowering (`deploy-migrate-lowering`)

**Outcome:** the app-cloud control extension lowers a PN-postgres resource to
DB provisioning plus a migration step: read marker → authored `migrate` to
the contract hash → hard fail on no-path/destructive/runner error, no-op when
hashes match. An example app (storefront-auth converted, or a sibling
example) authors a contract + migration and deploys live; CI E2E covers
deploy, migrated redeploy, and no-op redeploy.

**Slice DoD:** live Prisma Cloud round trip through the typed client;
no-path deploy failure test leaves DB untouched; at least one CI example
still exercises bare `postgres()`.

**Builds on:** Slice 1.
**Hands to:** project DoD; the datahub port consumes the shipped primitive.

**Linear:** _pending green light_

## Sequencing

Stack: 1 → 2. No parallel groups — slice 2 consumes slice 1's primitive.

## Close-out (required)

- [ ] Verify all acceptance criteria in `spec.md`
- [ ] Migrate long-lived docs into `docs/` (ADR-0022 already lands in slice 1;
migrate the deferred multi-contract design from design-notes into
docs/design if not already captured in the ADR)
- [ ] Strip repo-wide references to `.drive/projects/prisma-next-data-contract/**`
- [ ] Delete `.drive/projects/prisma-next-data-contract/`
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Slice 1 spec — typed pnPostgres primitive

**Linear:** TML-3009 · **Project:** prisma-next-data-contract

## Outcome

A Prisma Next-typed postgres primitive exists in `@prisma/app-cloud` behind a
dedicated subpath entry, with its static surface and its runtime hydrate both
proven — unit/type tests plus a live local-Postgres round trip. ADR-0022 ships
in the same PR.

## Scope

**In:**
- `packages/app-cloud/src/prisma-next.ts` — the new module: a
`Contract<'prisma-next', …>` kind, the `pnPostgres` factory (resource +
dependency overloads, mirroring `rpc.ts`), hydrate constructing the Prisma
Next client with no runtime schema verification (checks are deploy-time).
- `packages/app-cloud/package.json` — add `@prisma-next/postgres` dependency
and a `./prisma-next` subpath export; `tsdown.config.ts` — add the entry.
- Test fixtures: a small Prisma Next contract + its `prisma-next.config.ts`
and emitted artifacts (or TS no-emit contract), under the package's test
tree.
- Unit + type tests; one integration test against the repo's local-Postgres
harness.

**Out:**
- The deploy lowering in `control.ts` (slice 2).
- Any example-app conversion (slice 2).
- Bare `postgres()` — untouched.
- Multi-contract / contract-space support.

## Design decisions locked (operator, post-D1r1)

- **Resource shape: `pnPostgres({ name, contract })`.** The contract is the
*consumed* emitted artifact. The `prisma-next.config.ts` **path** (which the
deploy migration step needs) is **not** part of D1 — it rides on the resource
in slice 2, alongside the lowering that reads it (the `ResourceNode` has no
metadata slot today; that mechanism is a slice-2 decision). Never import the
config into the app build.
- **No runtime schema verification.** Hydrate builds the client with no
`verifyMarker` — schema checking is build/deploy-time only. A running
service can't be crashed by a marker check because there is no marker check.

## Slice DoD

- [ ] `pnPostgres` resource (`{ name, contract }`) + dependency overloads
typecheck; the dependency's binding is the Prisma Next client typed by
the contract.
- [ ] Type test: a consumer requiring contract vX is assignable to a resource
providing vX, and a different `storageHash` is a type error.
- [ ] Unit test: `satisfies()` returns true for equal `storageHash`, false
otherwise **including the missing/malformed-hash paths** (reviewer
finding — these branches are correct but untested); factory returns
correct `ResourceNode` / `DependencyEnd` shapes.
- [ ] Integration test (local Postgres): the hydrated client round-trips a
query against a DB migrated to the contract. (No mismatched-marker case —
there is no runtime check.)
- [ ] `@prisma/app-cloud` index import does not pull in `@prisma-next/*`/`pg`
(verified: the symbol lives only behind `./prisma-next`).
- [ ] Bare `postgres()` unit/type tests unchanged and green.
- [ ] Validation gate green (below).

## Validation gate

- `pnpm --filter @prisma/app-cloud typecheck`
- `pnpm --filter @prisma/app-cloud test` (bun test)
- `pnpm --filter @prisma/app-cloud test:types` (vitest --typecheck)
- Integration test command for the local-Postgres test (implementer confirms
the harness invocation; the state-store harness self-spawns `postgresql@15`
or reads `STATE_TEST_DATABASE_URL`).

## Open questions carried from the project spec

- Factory name (`pnPostgres` is the working name; do not block on it — the
operator settles it before merge).
- ~~Runtime `verifyMarker` semantics~~ — **resolved / moot.** The operator
ruled out runtime schema verification entirely (checks are build/deploy-time
only). Hydrate carries no `verifyMarker`.

## Dispatch plan

### D1 — primitive + unit/type proof

**Outcome:** `pnPostgres` compiles and is unit/type-proven without a live DB —
package wired, contract kind + factory + hydrate implemented, subpath export
in place, bare-postgres path untouched.

**Builds on:** — · **Hands to:** a compiling, unit-proven primitive whose
hydrate constructs the PN client (lazy — no connection yet).

**Focus:** structure mirrors `packages/app-rpc/src/rpc.ts`. The PN client is
lazy (pool on first query), so hydrate is fully implementable and unit-testable
without Postgres. Resolve the `verifyMarker` open question here.

**Completed when:** typecheck + `bun test` + `vitest --typecheck` green for
`@prisma/app-cloud`; type test proves storageHash-exact assignability; unit
test proves `satisfies` + node shapes; index import free of `@prisma-next/*`.

### D1r2 — reshape + coverage (batched: operator steer + reviewer finding)

**Outcome:** the primitive matches the locked design — resource is
`{ name, contract }`, hydrate has no `verifyMarker`, and the `satisfies`
missing/malformed-hash branches are tested.

**Builds on:** D1r1. · **Hands to:** D2.

**Completed when:** resource overload is `{ name, contract }` (no
`PnPostgresConfig`/`connection`); hydrate builds the client with no
`verifyMarker`; new unit cases cover `satisfies` on missing-hash /
malformed-`__cmp` both directions; module doc updated; full gate green.

### D2 — live integration proof

**Outcome:** the hydrated client is proven against a real local Postgres —
round-trips a query on a DB migrated to the contract.

**Builds on:** D1r2's primitive. · **Hands to:** slice DoD; slice 2's lowering.

**Focus:** use the repo's local-Postgres harness. Needs the fixture contract
migrated into the DB (apply it via PN's control client in test setup, or seed
the schema directly). Prove the round-trip only — there is no runtime marker
check to exercise.

**Completed when:** integration test green against a real DB (round-trip);
full validation gate green.
Loading
Loading