prisma-next-contract
This skill edits the Prisma Next data contract. Developers can add models, fields, relations, indexes, enums, namespaces, cross-contract foreign keys, and extension types via PSL or the TypeScript builder, then run `prisma-next contract emit` to generate the contract IR and type definitions. It keeps the data layer and types in sync automatically and reduces migration planning effort. Ideal for complex backend projects using multiple data sources and extensions such as pgvector and cipherstash.
npx skills add https://github.com/prisma/prisma-next --skill prisma-next-contractBefore / After Comparison
1 组Without this skill, developers had to manually edit the Prisma schema, keep TypeScript types in sync, and plan migrations by hand. A typical model change took about 60 minutes and often led to type mismatches or missed migrations.
With this skill, developers only need to edit the contract source and run contract emit; it generates the contract IR and precise types automatically and helps plan migrations. A model change can be done in 10 minutes, with far fewer type errors.
Prisma Next — Contract Authoring
Edit your data contract. Prisma handles the rest.
The data contract is the single source of truth for your data layer. You edit a contract source — contract.prisma (PSL, the canonical surface) or contract.ts (TypeScript builder) — and the framework derives types, migrations, and runtime configuration from it. The three-step user model:
- You edit your data contract.
- The system plans the migrations for you. (
prisma-next-migrations) - If you need data migrations, you edit
migration.tsand execute it. (prisma-next-migrations)
Behind step 1 the agent runs prisma-next contract emit after every contract edit (or installs the Vite plugin so the bundler runs it on save — see prisma-next-build). Emit reads the contract source through the provider the façade picks based on the file extension of contract: in prisma-next.config.ts, then writes two artefacts colocated with the source:
contract.json— the canonical, content-hashed Contract IR. Read by the planner, the runtime, anddb verify.contract.d.ts— the precise TypeScript types the runtime + lanes propagate when you importContractfrom it.
Both files are emitted artefacts. Edit the source; never the JSON or .d.ts.
When to Use
- User wants to add, change, or remove a model / field / relation.
- User wants to add an index, unique constraint, enum, or value object (composite type).
- User wants to add a namespace block (Postgres schema) or a cross-contract foreign key.
- User wants to set
@@controlon a model or configuredefaultControlPolicy. - User wants to use a custom type from an extension (
pgvector.Vector(length: 1536),cipherstash.EncryptedString({...})). - User wants to install or configure an extension via
extensions: [...]inprisma-next.config.ts, including@prisma-next/extension-supabase. - User is migrating between authoring sources (PSL ↔ TypeScript builder).
- User received
PN-CLI-4002,PN-CLI-4003, orPN-CLI-4011fromcontract emit. - User mentions: schema, fields, models, attributes, prisma schema, PSL, contract.prisma, contract.ts, contract.json, contract.d.ts, contract emit, façade imports,
@prisma-next/postgres/config,@prisma-next/postgres/contract-builder, extensions, pgvector, cipherstash, postgis, paradedb, supabase, namespaces, cross-space FK,@@control, enums, value objects, validations, callbacks, soft delete, paranoid, scopes. (The last cluster routes to What Prisma Next doesn't do yet below.)
When Not to Use
- User wants to apply a contract change to the DB →
prisma-next-migrations. - User wants to write a query against the contract →
prisma-next-queries. - User wants to wire
db.ts(runtime entry point, middleware, env config) →prisma-next-runtime. - User wants the Vite / bundler integration →
prisma-next-build. - User wants to set up Prisma Next for the first time →
prisma-next-quickstart. - User wants a deeper read of a single structured error envelope →
prisma-next-debug. - User wants to file a missing-feature request →
prisma-next-feedback.
Key Concepts
-
The
@prisma-next/<target>façade is the only surface user-authored code imports from. For a Postgres app:@prisma-next/postgres/config,@prisma-next/postgres/contract-builder,@prisma-next/postgres/control,@prisma-next/postgres/runtime. Mongo has the same layout (@prisma-next/mongo/config,@prisma-next/mongo/contract-builder,@prisma-next/mongo/runtime). Each extension publishes its own façade —@prisma-next/extension-pgvector/control,@prisma-next/extension-postgis/control,@prisma-next/extension-paradedb/control. Never reach into@prisma-next/cli/*,@prisma-next/family-*,@prisma-next/target-*,@prisma-next/adapter-*,@prisma-next/driver-*, or@prisma-next/sql-contract-*from user code. The façade bakes the family / target / adapter / driver wiring in. See Common Pitfalls #4. -
Contract source. A file the framework reads and lowers to the canonical Contract IR. Two flavours, both first-class:
contract.prisma(PSL) — schema-flavoured DSL. Canonical for typical apps and brownfield Prisma users. Wired bycontract: './<path>/contract.prisma'— thedefineConfigfaçade detects the.prismaextension and routes through the PSL provider.contract.ts(TypeScript builder) — programmatic authoring withdefineContract({...}, ({ field, model, rel, type }) => ({...}))from@prisma-next/postgres/contract-builder(or@prisma-next/mongo/contract-builder). Wired bycontract: './<path>/contract.ts'— the façade detects the.tsextension and routes through the TS provider. Use when you need programmatic composition (per-tenant variants, generated fields) or constructs PSL doesn't yet express (e.g. registering a parameterised extension type — see pgvector's contract).
-
prisma-next.config.ts. Wires the contract source, the database connection, the migrations directory, and any installed extensions. UsedefineConfig({...})from@prisma-next/postgres/config(or@prisma-next/mongo/config). The four fields the façade accepts:contract(path string —.prismaor.ts),db({ connection?: string }),extensions(array of control descriptors),migrations({ dir?: string }). The output path forcontract.jsonis auto-derived fromcontract(e.g../src/prisma/contract.prisma→./src/prisma/contract.json). -
Emit pipeline.
prisma-next contract emit --config <path>?readsprisma-next.config.ts, calls the provider the façade picked, validates the resulting Contract, then atomically writescontract.json+contract.d.tscolocated with the source. -
Extension namespaces. Extensions contribute namespaced constructors (
pgvector.Vector(length: 1536),cipherstash.EncryptedString({equality: true})) and helper presets. Install them by adding the descriptor to two places — both fields are namedextensions, but the two surfaces consume two different descriptor types and shapes:- In the config (façade and core):
extensions: [pgvector]— array of control descriptors imported from@prisma-next/extension-<name>/control. - In the TS builder's
defineContract(only when authoringcontract.ts):extensions: { pgvector }— record of pack descriptors imported from@prisma-next/extension-<name>/pack.
- In the config (façade and core):
-
Contract space. Every package that emits a contract owns its own contract space — a
prisma-next.config.tsat package root, a contract source, the colocated emitted artefacts, and amigrations/directory. There are two intentional on-disk layouts, picked by whether the contract space is the consuming application or a contract-space package (an extension, an internal aggregate-root package, etc.):- Application layout (what you use when building an app).
prisma-next.config.tsat repo root;src/prisma/contract.{prisma,ts};src/prisma/contract.{json,d.ts}colocated;src/prisma/db.tscolocated; migrations undermigrations/app/<timestamp>_<slug>/. Theapp/segment is the consuming application's space-id; extension space-ids land in siblingmigrations/<extension-space-id>/directories that the extension packages manage. This is whatexamples/prisma-next-demouses.prisma-next initcurrently scaffolds something different (prisma/...at repo root) — that's a defect (TML-2532); the canonical layout is what every command actually expects to see. - Contract-space-package layout (what you use when publishing a contract-space package — extensions, internal monorepo packages).
prisma-next.config.tsat package root;src/contract.{prisma,ts}directly (noprisma/subdir);src/contract.{json,d.ts}colocated;migrations/<timestamp>_<slug>/directly undermigrations/(no<space-id>segment — the package is a single space). Documented in.cursor/rules/contract-space-package-layout.mdcand ADR 212.
Both layouts let
defineConfig'scontract:path point at the source; the framework derives everything else (emit output, migration root) from there. Pick the layout that matches what you're building and stick with it — don't mix. - Application layout (what you use when building an app).
Diagnostic codes you route on
prisma-next contract emit surfaces structured errors with stable codes; branch on code rather than message text.
| Code | Meaning | Next move |
|---|---|---|
PN-CLI-4002 Contract configuration missing | contract not set in prisma-next.config.ts. | Add contract: './src/prisma/contract.prisma' (app layout) or './src/contract.prisma' (contract-space-package layout) — likewise for .ts sources — to defineConfig({...}) from @prisma-next/postgres/config. |
PN-CLI-4003 Contract validation failed | Source loaded but the Contract IR failed structural validation. | Read meta.diagnostics / meta.issues for the offending model/field, fix the source, re-emit. |
PN-CLI-4011 Missing extension packs in config | The contract uses a namespaced constructor (e.g. pgvector.Vector(...)) but extensions in the config does not list a matching descriptor. meta.missingExtensions names them. | Install the package, import its control descriptor (import pgvector from '@prisma-next/extension-pgvector/control'), add it to extensions: [...] in prisma-next.config.ts. |
Workflow — Read the contract source of truth
The concept: every contract change starts by locating the source file. The config is authoritative — read prisma-next.config.ts, find the contract: field (a path string under the façade), and open the file it points at. The same field tells you the installed extensions: [...].
cat prisma-next.config.ts
If contract: ends in .prisma, the source is PSL; if it ends in .ts, the source is the TS builder. If prisma-next.config.ts is missing, route to prisma-next-quickstart.
Workflow — Edit a model / field / relation (PSL)
The concept: PSL models lower to tables (or collections, on Mongo); fields lower to columns; @relation(...) declares the FK side. Add the relation only on the owning side — the framework derives the back-reference automatically.
model User {
id Int @id @default(autoincrement())
email String @unique
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
@@unique([title, authorId])
@@index([authorId])
}
Then run pnpm prisma-next contract emit (or rely on the Vite plugin — see prisma-next-build). Specify cascade behaviour explicitly with onDelete / onUpdate; the default is Restrict.
@@index also accepts expression: (instead of a fields list), where: (partial-index predicate), unique:, type:/options: (target-registered access method), and name: xor map::
@@index(expression: "lower(email)", name: "users_email_lower")
@@index([authorId], where: "(archived_at IS NULL)", name: "posts_author_active")
name: declares a managed index (physical name <name>_<8-hex hash>, renames plan as ALTER INDEX … RENAME); map: adopts an exact physical name verbatim (for infer-captured objects — combining it with a SQL body warns, because drift detection byte-compares the authored text against Postgres's reprint). An expression: requires name: or map:. The TS builder mirrors this via constraints.index([cols.x], {...}) / constraints.index({ expression, ... }) — see packages/2-sql/2-authoring/contract-ts/README.md.
PSL alias surface for repeated types lives in a top-level types {} block:
types {
Email = String
}
model User {
id Int @id @default(autoincrement())
email Email @unique
}
Note: scalar lists (e.g. String[]) and implicit Prisma-ORM many-to-many (list nav on both sides without a join model) are rejected by the SQL interpreter — use a join model. Composite/embeddable types (type Address { ... } with address Address on a model) are supported: the interpreter lowers them to valueObjects in the domain and stores them as jsonb columns. See Workflow — Value objects below.
Workflow — Edit a model / field / relation (TS builder)
The concept: same model, different authoring surface. The façade re-exports defineContract, field, model, rel, plus the family/target packs as default exports of @prisma-next/postgres/family and @prisma-next/postgres/target. Use the callback overload (defineContract({...}, ({ field, model, rel, type }) => ({...}))) to get the higher-level helpers (field.text(), field.id.uuidv7String(), field.temporal.createdAt(), type.sql.String(35)).
import sqlFamily from '@prisma-next/postgres/family';
import { defineContract } from '@prisma-next/postgres/contract-builder';
import postgresPack from '@prisma-next/postgres/target';
export const contract = defineContract(
{
family: sqlFamily,
target: postgresPack,
},
({ field, model }) => ({
models: {
User: model('User', {
fields: {
id: field.id.uuidv7String(),
email: field.text().unique(),
createdAt: field.temporal.createdAt(),
},
}).sql({ table: 'app_user' }),
},
}),
);
Then pnpm prisma-next contract emit. The field.<scalar>() helpers are only available inside the callback overload; outside the callback only field.column(...), field.generated(...), field.namedType(...) exist.
For Mongo, swap every @prisma-next/postgres/* import for @prisma-next/mongo/*. The Mongo builder also exposes index and valueObject.
Workflow — Add an extension-typed scalar (pgvector)
The concept: an extension contributes a namespace (pgvector.*) plus two descriptor flavours — a control descriptor for the config façade and a pack descriptor for the TS builder. Register the control descriptor in defineConfig.extensions (array form). If you're authoring with the TS builder, also register the pack descriptor in defineContract.extensions (record form). Then reference the namespaced constructor from the contract.
prisma-next.config.ts:
import pgvector from '@prisma-next/extension-pgvector/control';
import { defineConfig } from '@prisma-next/postgres/config';
export default defineConfig({
contract: './src/prisma/contract.prisma',
extensions: [pgvector],
});
src/prisma/contract.prisma:
model Document {
id Int @id @default(autoincrement())
content String
embedding pgvector.Vector(length: 1536)
}
Emit. The named-type lowering puts vector(1536) on the column and the type map in contract.d.ts carries the right TS type.
If you reference pgvector.* without registering the pack in the config, emit fails with PN-CLI-4011 and meta.missingExtensions: ['pgvector']. The envelope's fix text says *"Add the missing extension des
...
User Reviews (0)
Write a Review
No reviews yet
Statistics
User Rating
Rate this Skill