Prisma Next is in early access.Read the docs

Author in PSL

Write the Prisma Next contract as a Prisma schema file, using the schema language you already know plus the Prisma Next additions.

PSL is the preferred way to author the Prisma Next data contract. You write a Prisma schema file, usually prisma/contract.prisma, and prisma-next contract emit turns it into contract.json and contract.d.ts.

If you know the Prisma schema language, most of a contract file reads exactly as you expect. This page covers the shared basics briefly and the Prisma Next additions in detail: named types, typed enums, value objects, base models with variants, and extension types.

A complete contract

prisma/contract.prisma
// use prisma-next

types {
  Uuid = String @db.Uuid
}

type Address {
  street  String
  city    String
  zip     String?
  country String
}

enum Priority {
  @@type("pg/text@1")
  Low    = "low"
  High   = "high"
  Urgent = "urgent"
}

model User {
  id        Uuid     @id @default(uuid())
  email     String
  createdAt DateTime @default(now())
  address   Address?
  posts     Post[]

  @@map("user")
}

model Post {
  id        Uuid     @id @default(uuid())
  title     String
  userId    Uuid
  priority  Priority @default(Low)
  createdAt DateTime @default(now())

  user User @relation(fields: [userId], references: [id])

  @@map("post")
}

Run npx prisma-next contract emit after any change to refresh the artifacts.

Point the config at the schema

The config's contract path names the source of truth. A .prisma extension selects PSL authoring:

prisma-next.config.ts
import { defineConfig } from "@prisma-next/postgres/config";

export default defineConfig({
  contract: "./prisma/contract.prisma",
});

Scaffolded projects (npx create-prisma@next) already contain this wiring and a starter schema.

Models and fields

Models declare fields with a type, an optional ? marker, and attributes:

  • @id marks the primary key; @@id([a, b]) declares a composite key.
  • @unique adds a unique constraint; @@index([...]) declares a secondary index.
  • @default(...) sets a default. Database function defaults such as @default(now()) become column defaults in the database. Generated defaults such as @default(uuid()) are applied by Prisma Next before each write instead, so they work the same on every database and appear in the contract's execution section rather than as DDL.
  • @map("column_name") sets a field's physical name; @@map("table_name") sets the table or collection name when it differs from the model name.

How IDs map differs by database:

model User {
  id Uuid @id @default(uuid())
}

The primary key is an ordinary column; pick its type and default yourself.

model User {
  id ObjectId @id @map("_id")
}

The primary key is MongoDB's _id: type it ObjectId and map it to the physical _id key.

Named types

The types block declares reusable type aliases. An alias binds a base type and its storage details under one name:

types {
  Uuid = String @db.Uuid
}

Fields then use Uuid like any built-in type. The alias keeps the storage decision (uuid columns rather than text) in one place.

Enums

An enum in Prisma Next declares its storage codec with @@type and, optionally, the stored value for each member:

enum Priority {
  @@type("pg/text@1")
  Low    = "low"
  High   = "high"
  Urgent = "urgent"
}

@@type("pg/text@1") stores the values as text on PostgreSQL. When a member has no explicit value, the member name itself is stored. In the database, the emitted contract enforces the allowed values with a CHECK constraint on each column that uses the enum.

Value objects

A type block declares a value object: a structured value that belongs to its parent record and has no identity or table of its own.

type Address {
  street  String
  city    String
  zip     String?
  country String
}

model User {
  id      Uuid     @id @default(uuid())
  address Address?
}

Storage follows the database's nature: on PostgreSQL a value object field lives in a single jsonb column, and on MongoDB it is an embedded document. Either way, contract.d.ts types it as a structured object rather than untyped JSON. On MongoDB, whether to embed or reference is the central modeling decision; MongoDB data modeling covers it.

Relations

Relations use the @relation syntax you know from Prisma ORM. The side that holds the foreign key declares the scalar field and the mapping; the other side declares a list:

model Post {
  userId Uuid
  user   User @relation(fields: [userId], references: [id])
}

model User {
  posts Post[]
}

Many-to-many relations go through an explicit join model with a composite primary key. The list fields on both sides then resolve through it:

model Post {
  tags Tag[]
}

model Tag {
  posts Post[]
}

model PostTag {
  postId Uuid
  tagId  Uuid

  post Post @relation(fields: [postId], references: [id])
  tag  Tag  @relation(fields: [tagId], references: [id])

  @@id([postId, tagId])
  @@map("post_tag")
}

The emitted contract records the relation as N:M with the join table's columns, so queries can traverse post.tags directly.

Base models and variants

A base model declares a discriminator field, and each variant names its base and its discriminator value:

model Task {
  id     Uuid   @id @default(uuid())
  title  String
  type   String

  @@discriminator(type)
  @@map("task")
}

model Bug {
  severity     String
  stepsToRepro String?

  @@base(Task, "bug")
  @@map("bug")
}

Rows with type = "bug" are Bug records. On PostgreSQL, the variant's @@map picks its storage layout: with its own @@map, as here, the variant's fields live in their own table sharing the base model's primary key; without one, they live in the base table as nullable columns. Relational data modeling covers choosing between the two. On MongoDB, variants add their fields to documents in the base model's collection, so a variant declares @@base but no @@map of its own.

Extension types

Extension packs contribute types through constructor expressions in the types block. Compose the pack in the config, then use its types:

prisma-next.config.ts
import pgvector from "@prisma-next/extension-pgvector/control";
import { defineConfig } from "@prisma-next/postgres/config";

export default defineConfig({
  contract: "./prisma/contract.prisma",
  extensions: [pgvector],
});
prisma/contract.prisma
types {
  Embedding1536 = pgvector.Vector(1536)
}

model Post {
  id        Uuid           @id @default(uuid())
  embedding Embedding1536?
}

Emission validates extension types against the composed packs: a type from a pack that is not listed in the config fails the emit with a diagnostic. Re-run contract emit after changing the extension list.

Starting from an existing database

If the database already exists, don't write the schema by hand. prisma-next contract infer reads the live schema and writes a starter contract.prisma for you to review and edit.

The syntax above declares relations; for which shape to choose and which side owns the foreign key, see relational data modeling and MongoDB data modeling.

Prompt your coding agent

Projects scaffolded with create-prisma@next install Prisma Next skills for your coding agent; the prisma-next-contract skill covers this page. Ask your agent to:

  • "Using the prisma-next-contract skill, add a Status enum stored as text and use it on the Order model."
  • "Add a one-to-many between User and Post with the foreign key on Post."
  • "Give the Post model a composite unique constraint on userId and title."

Next steps

On this page