feat(app-cloud): pnPostgres — Prisma Next-typed postgres primitive (slice 1) - #44
Merged
Merged
Conversation
…h entry Adds @prisma-next/postgres (plus its @prisma-next/contract and @prisma-next/sql-contract type-only deps) to @prisma/app-cloud, and gives prisma-next.ts its own tsdown entry so the ./prisma-next subpath export is generated automatically — the index entry never pulls in @prisma-next/* or pg unless a caller explicitly imports the subpath (ADR-0021). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
A second, parallel data primitive alongside bare postgres() (untouched):
pnPostgres(), overloaded like rpc()/postgres() into a resource end
({ name, config }, providing config.contract) and a dependency end
(pnPostgres(contract), binding to the typed Prisma Next client).
The prisma-next Contract kind's __cmp carries the deserialized contract data
plus a type-only _contract anchor pinning the emitted contract's branded
storageHash literal, so plain TypeScript assignability between two wrapped
contracts is exact-version equality; satisfies() mirrors it at Load with a
real storageHash comparison. Hydrate constructs the client via
@prisma-next/postgres/runtime with verifyMarker: 'onFirstUse', which is
natively warn-only (confirmed by reading sql-runtime's verifyMarker()
implementation, not just its .d.ts) — a marker mismatch logs and returns, it
never throws, so no wrapper is needed to satisfy "the app always attempts to
run" (ADR-0021).
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…t emit Two minimal, one-model contracts (Widget, Gadget), each with its TS-authored source (defineContract() + prisma-next.config.ts) and real emitted artifacts (contract.json + contract.d.ts) produced by running the installed @prisma-next/cli's `contract emit` offline — no live database needed. Source and emitted artifacts live in separate directories per fixture so a same-name .ts/.d.ts pair in one directory can't shadow each other during module resolution (this bit an earlier layout: an import of contract.d.ts silently resolved to the sibling contract.ts's inferred, non-branded type instead). Named widget-contract/gadget-contract rather than prisma-next(-other): a bare `prisma-next/` name is caught by the repo's .gitignore (an external-checkout ignore rule, not meant for this). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Type test (vitest --typecheck): a consumer requiring the emitted Widget contract is assignable where the resource provides that same contract type (including a second, independently-built wrap of it — the lever is the type, not object identity); a different emitted contract (Gadget, a different storageHash) is a type error, and so is a resource of a wholly different protocol kind (bare postgresContract). Unit test (bun test): satisfies() is true for equal storageHash and false otherwise (including across kinds); the factory returns correctly branded ResourceNode/DependencyEnd shapes; hydrate constructs a real Prisma Next client from a fake url without connecting, and never throws regardless of what a live marker would say. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Updates the entry-map invariant to expect the new subpath export, and adds invariant 7: no module reachable from src/index.ts imports prisma-next.ts, @prisma-next/*, or pg, and the built dist/index.mjs contains none of those tokens — the DoD's "index import stays free of @prisma-next/*/pg" checked directly against the built output, not just reasoned about. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…rop runtime marker check
Two locked design changes (ADR-0021, rewritten):
- The resource overload is now `pnPostgres({ name, contract })` — the
consumed emitted contract, nothing else. Deletes `PnPostgresConfig` and its
`connection?` field: the `prisma-next.config.ts` path the deploy migration
step needs is slice-2 metadata that rides on the resource there, not a
field the app build declares (the frozen ResourceNode has no metadata slot
today; that mechanism is slice 2's call).
- Hydrate builds the client with no `verifyMarker`. Schema correctness is a
build/deploy-time job: the deploy migrates the DB to the contract hash and
keeps it there, so the runtime binding just builds the client. A running
service can't be crashed by a marker check because there is none.
The dependency end (`pnPostgres(contract)`) and the `satisfies` storageHash
comparison are unchanged.
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…-hash satisfies
- Type + unit tests construct the resource via `pnPostgres({ name, contract })`;
the removed `config.connection`-is-ignored case goes with the removed field.
- The hydrate test now asserts the design directly: hydrate does no schema
verification and reads no database, so it can't throw regardless of any
live marker (there is no `verifyMarker`).
- Adds the reviewer's missing-hash coverage for `satisfies`: a required
contract with a malformed `__cmp` (no `contractJson`) is not satisfied, and
a wrapper whose `contractJson` lacks `storage.storageHash` is unsatisfiable
in both directions (and does not even satisfy itself) — exercising the
`storageHashOf` undefined branches that were correct but untested.
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Mirrors packages/alchemy/src/state/__tests__/harness.ts: honors STATE_TEST_DATABASE_URL (the env var the CI test job already wires), else spins an ephemeral initdb/pg_ctl cluster, else returns undefined so the caller skips loudly on a dev machine without Postgres — and throws on CI so the suite can never silently go unexecuted. Kept as a local copy rather than a cross-package import: that harness is another package's private test file with no export, and the workspace keeps a package's tests self-contained. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…al Postgres
The slice's live proof (D2): stand up a real Postgres, bring its schema to the
widget-contract fixture via Prisma Next's own control client
(createPostgresControlClient().dbInit({ mode: 'apply' }) — the machinery slice
2's deploy lowering will drive, so this exercises the real apply-and-sign path,
not hand-written SQL), construct the client exactly as a service would through
pnPostgres(contract)'s hydrate given only the DB url, and round-trip a real
query: ORM create() then where().first()/all(), asserting the row.
The result is contract-typed end to end — Widget.id is a char(36) column, so
its typed value is the branded Char<36>; a plain string does not compile as the
id, which is what makes the round-trip's type-check the typed-result proof (the
typecheck gate enforces it). Adds @prisma-next/target-postgres as a devDep for
that Char type (a direct import in the test).
Environment-gated via the harness: skips cleanly with no Postgres locally,
runs on CI against the wired postgres service. Verified live here against a
spawned ephemeral cluster.
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Records the design for the pnPostgres primitive: a Prisma Next-typed postgres resource/dependency whose binding is the contract-typed client, alongside untouched bare postgres(). Consume the contract (emitted artifact), locate the config by path (deploy-only); schema checks are build/deploy-time, so the runtime binding does no verification. Amends ADR-0015 (Prisma Next is framework-blessed like rpc). Includes the Drive project artifacts for the two-slice delivery. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
wmadden
approved these changes
Jul 11, 2026
Contributor
Author
There was a problem hiding this comment.
Done in 8738f19 — gadget is now authored in PSL (contract.prisma, same schema). It re-emits byte-for-byte identical contract.json/contract.d.ts and the same storageHash (the hash fingerprints the schema, not the authoring form), so no test changes were needed. I kept widget as TS-authored on purpose, so the two fixtures now cover both authoring modes ADR-0021 supports (PSL-first and TS no-emit). If you actually meant all fixtures should be PSL, say so and I will convert widget too.
Review ask on PR #44 ("use PSL"): the gadget contract is now authored as a .prisma file instead of TS defineContract(), and its config points at the .prisma source (same defineConfig({ contract: <path> }) shape — the provider picks PSL from the extension). Widget stays TS-authored on purpose: widget covers the TS defineContract() authoring mode, gadget covers PSL, so the two fixtures exercise both modes ADR-0021 says to support. A short mode note is added to each source. The PSL re-expresses the identical schema (id char(36) uuid(4) default @id, label text), with @@Map("Gadget") to keep the physical table capitalized — PSL lowercases table names by default, and that was the only thing that differed. Because storageHash is a deterministic fingerprint of the schema, not the authoring form, the re-emitted emitted/ artifacts are byte-for-byte identical (storageHash sha256:35caf6…) — so the widget-vs-gadget type test and everything consuming emitted/ stay green with no test edits. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ias as
Two fixes so the CI Lint + cast-ratchet jobs pass on this branch:
- biome.jsonc: ignore `**/fixtures/**/emitted` — that dir holds only generated
`prisma-next contract emit` output (contract.json plus the already-ignored
contract.d.ts), which must stay byte-exact and must not be reformatted. Uses
the folder form without a trailing `/**` (biome's own useBiomeIgnoreFolder
rule rejects the `/**` suffix since 2.2.0).
- prisma-next.ts: the `no-bare-cast` grit rule counts every `as` token, so the
`Contract as PnRawContract` import alias (a rename, not a cast) tripped the
cast-ratchet as one net-new cast vs merge-base. Replaced the aliased import
with an inline `import('@prisma-next/contract/types').Contract<…>` type
reference — no `as`, no logic change. Ratchet is back to delta=0.
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Brings in the schema-typed config-params breaking change (f69cb1f), config()/load() split, target-owned serialization, the reusable-auth H3 work, and the hex-composition closeout. Slice-1 adaptations: - Params API: pnPostgres's dependency now declares its url param via the string() schema-builder (`string({ secret: true })`) instead of the removed `{ type: 'string', secret: true }` shape, matching the merged postgres.ts; dropped the now-stale explicit dependency<> generics. Unit assertion updated to `toEqual(string({ secret: true }))`. - Conflicts: package.json (kept our ./prisma-next export + their ./testing), tsdown.config.ts (union of entries), invariants.test.ts (entry-map union of all three runtime entries; kept invariant 7), pnpm-lock.yaml regenerated. - ADR renumber: main shipped its own ADR-0021 (params read through config), so our data-deps ADR renumbers to ADR-0022 (repo convention: the later branch takes the next free number). File renamed, header and all our-side references (code + tracking docs) updated; main's ADR-0021 references left untouched. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
wmadden-electric
added a commit
that referenced
this pull request
Jul 11, 2026
Integrates #44 (pnPostgres) / #43 (hex-composition close-out). Regenerates pnpm-lock.yaml from the merged manifests — the auto-merge left it internally inconsistent (a dangling snapshot ref) because my branch predated main's large dependency change, which is what failed CI's frozen install on the PR merge. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
This was referenced Jul 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Introduces
pnPostgres, a Prisma Next-typed postgres primitive for@prisma/app-cloud— the first half of delivering the "data contracts are Prisma Next" promise the README/glossary already make but the code never implemented (today's barepostgres()binding is{ url }andsatisfiescompares kind only).A service that depends on
pnPostgres(contract)gets a contract-typed Prisma Next client injected atload()— typed to the schema, built by the framework in hydrate. Barepostgres()is untouched: it stays the untyped escape hatch (theanyof data deps).This is slice 1 of 2. It lands the primitive + ADR-0021, proven live against real Postgres. Slice 2 adds the deploy-time migration lowering (
control.ts).Design record: ADR-0021. Tracking: TML-3009.
Design (settled with the operator)
contract.jsondata +contract.d.tstypes — lightweight, safe to bundle). It never importsprisma-next.config.ts— that would pull Prisma Next's CLI/migration machinery into the user's bundle. The config reaches the deploy lowering as a path (slice 2).postgres<Contract>({ contractJson, url })from@prisma-next/postgres/runtime, lazy pool. This amends ADR-0015: Prisma Next is framework-blessed like rpc, so the contract alone constructs the client.storageHashor fails). The runtime binding just builds the client — so a running service can never be crashed by a marker check.storageHashliteral makes wiring assignability version-exact;satisfies()mirrors it at Load with a hash comparison../prisma-nextsubpath entry, never re-exported from the index — a service that doesn't import it never loads@prisma-next/*orpg.What's in this PR
packages/app-cloud/src/prisma-next.ts— theprisma-nextContract kind,pnContract(), and thepnPostgres()resource ({ name, contract }) / dependency (pnPostgres(contract)) overloads, mirroringrpc.ts.@prisma-next/postgresdep + the./prisma-nextsubpath entry (tsdown).widget/gadget) built from a realprisma-next contract emit.satisfiesincl. missing/malformed-hash paths, node shapes, lazy hydrate) and a live local-Postgres integration test — applies the fixture contract via Prisma Next's real control client (dbInitapply) and round-trips a query through the hydrated client; a brandedChar<36>column makes the typed-result assertion enforced by the typecheck gate.Proof
pnpm --filter @prisma/app-cloud typecheck/test(60 pass) /test:types(9 pass) — green.STATE_TEST_DATABASE_URL, elseinitdb/pg_ctl); it throws on CI if no Postgres is available, so it can't silently pass.dist/index.mjscarries zero@prisma-next/pgtokens.Deferred to slice 2
control.tsdeploy lowering: read the DB marker →migratealong the authored graph to the contract's hash → hard-fail on no-path / destructive-without-opt-in.prisma-next.config.tspath rides on the (frozen)ResourceNodeso the lowering can find the migrations dir.Notes
pnPostgresis a working name — Prisma Next → Prisma Data at GA will churn it.🤖 Generated with Claude Code