Author in PSL
Write the Prisma ORM contract in the Prisma schema language you already know, plus the Prisma ORM 8 additions.
PSL, the Prisma Schema Language, is the preferred way to author your contract, the contract.prisma file that replaced schema.prisma. You write one file, usually src/prisma/contract.prisma, and npx prisma contract emit writes contract.json and contract.d.ts beside it. If you know the Prisma schema language, most of a contract file reads exactly as you expect. Prisma ORM 8 differs in five places:
- named types: give a database column type a name you can reuse on many fields.
- enums: an enum can now say how its values are stored, and what each member stores.
- value objects: a structured value stored inside its parent row, with no table of its own.
- base models and variants: one table can hold more than one kind of record. Put the shared fields in a base model, put the differences in each variant, and Prisma ORM uses one column to tell them apart.
- extension types: field types that come from an npm package, such as vectors.
The datasource and generator blocks are gone. The connection URL and the file paths are set in prisma.config.ts instead. Coming from Prisma ORM 7 lists every change to the schema file, including what each old @db. attribute becomes.
A complete contract
Every contract starts with // use prisma-8. Keep that line at the top.
// use prisma-8
types {
ShortName = VarChar(35)
}
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 ShortName
userId Uuid
priority Priority @default(Low)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
@@map("post")
}Uuid is PostgreSQL's uuid type written as a field type. Run npx prisma contract emit after any change to refresh contract.json and contract.d.ts. Then npx prisma db init creates the tables.
Point the config at the schema
The config's contract path names the one file Prisma ORM reads. It takes a single path, so there is no folder of contract files. If the path ends in .prisma, Prisma ORM reads it as PSL. If it ends in .ts, as TypeScript. The db key holds the connection URL:
import 'dotenv/config';
import { definePrismaConfig } from "prisma/config";
import { defineConfig as ormConfig } from "@prisma/orm-postgres/config";
export default definePrismaConfig({
orm: ormConfig({
contract: "./src/prisma/contract.prisma",
db: {
connection: process.env['DATABASE_URL']!,
},
}),
});npx prisma orm init writes a config like this, DATABASE_URL included. The import chooses the database: @prisma/orm-postgres/config makes it a PostgreSQL project, and @prisma/orm-mongo/config a MongoDB one. Coming from Prisma ORM 7 lists the other keys.
Models and fields
Models declare fields with a type, an optional ? marker, and attributes. Scalar fields lists the types a field can hold, among them String, Int, Boolean, Decimal, DateTime, Json, and Bytes. You can write a PostgreSQL type wherever you would write String or DateTime. The PostgreSQL types you can write are VarChar, Char, Numeric, Timestamp, Timestamptz, Time, Timetz, Date, Uuid, Inet, SmallInt, and Real, plus DateString, TimestampString, TimestamptzString, and TimeString, which store the same columns but read back as text. On PostgreSQL, plain String is a text column, Int is int4, and DateTime is timestamptz.
@idmarks the primary key.@@id([a, b])declares a composite key.@uniqueadds a unique constraint on one field.@@unique([userId, title])adds one across several fields.@@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())come from Prisma ORM, and the database will not fill them in for you, so a row written by raw SQL or another application gets no value.@map("column_name")sets a field's column name in the database.@@map("table_name")sets the table or collection name when it differs from the model name.
@updatedAt is gone. Write temporal.updatedAt() where the field's type would go:
model Post {
updatedAt temporal.updatedAt()
}Prisma ORM sets the field to the current time on every create and update. temporal is built in. You do not import it or declare it. On PostgreSQL the column is timestamptz, and you can still set the field yourself on a write. temporal.createdAt() sets the field once, when the row is created.
How IDs map differs by database:
model User {
id Uuid @id @default(uuid())
}On PostgreSQL the primary key is an ordinary column, so pick its type and default yourself. On MongoDB the primary key is the document's _id. Type it ObjectId and map it to _id.
Named types
The types block gives a database column type a name you can reuse on many fields:
types {
ShortName = VarChar(35)
}Fields then use ShortName like any built-in type. The name keeps the column decision in one place: a varchar(35) column rather than text. A name is optional, and a field can use the native PostgreSQL type directly, such as VarChar(35), Uuid, or Timestamptz. In Prisma ORM 8 the native type is the field's type, so String @db.VarChar(35) from Prisma ORM 7 becomes VarChar(35). The @db. attributes are gone. The types block is PostgreSQL only.
Enums
An enum lists its members. It can also say how their values are stored, with @@type, and what each member stores:
enum Priority {
@@type("pg/text@1")
Low = "low"
High = "high"
Urgent = "urgent"
}In pg/text@1, pg is PostgreSQL, text is the column type, and @1 is the version of how the value is stored and read. Write @@type("pg/int4@1") to store the values as integers instead. When a member has no explicit value, the member name itself is stored.
@@type is optional. Leave it out and Prisma ORM picks the type from the member values: bare member names and string values give the database's text type, and integer values give its integer type. Give every member the same kind of value, because a mix of string and integer values throws an error whose code is PSL_ENUM_CANNOT_INFER_TYPE. A @default names the member, as in priority Priority @default(Low), even where the member stores a different value.
An enum block is not a PostgreSQL enum type: the column is text or an integer. For a PostgreSQL enum type, declare it in a native_enum block and type the field pg.enum(Role):
native_enum Role {
admin = "admin"
member = "member"
}
model User {
role pg.enum(Role)
}Each member needs a value. pg comes with @prisma/orm-postgres, so there is nothing to import. You do not create the PostgreSQL type yourself: npx prisma migration plan includes the CREATE TYPE.
Value objects
A type block declares a value object: a structured value stored inside its parent row, with no table of its own.
type Address {
street String
city String
zip String?
country String
}
model User {
id Uuid @id @default(uuid())
address Address?
addresses Address[]
}A value object field can be optional or a list, and a type block can hold a field of another type. Watch the two spellings: types { ... } declares named types, and type X { ... } declares a value object. Storage differs by database. On PostgreSQL a value object field is stored in a single jsonb column. 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[]
}Add onDelete and onUpdate to the same @relation. They belong on the side that holds the foreign key, not on the list side. For a one-to-one, make the other side singular instead of a list, so User declares profile Profile?, and put @unique on the foreign-key field:
model Profile {
userId Uuid @unique
user User @relation(fields: [userId], references: [id])
}Many-to-many relations need a model for the join table. You write that model yourself: there is no implicit many-to-many. That model must follow two rules. If a side has a composite primary key, that model needs one foreign-key field for each part of it. Its @@id([...]) must list exactly the foreign-key fields, and nothing else.
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")
}You then read post.tags as a list of Tag, without mentioning PostTag in the query. Break either rule and npx prisma contract emit reports which list field it could not match to a model. If two models qualify, npx prisma contract emit throws an error whose code is PSL_AMBIGUOUS_BACKRELATION. Put the same @relation("name") on both ends of one pair, so on PostTag.post for Post.tags.
For which shape to choose and which side owns the foreign key, see relational data modeling and MongoDB data modeling.
Base models and variants
A base model declares a discriminator field, the field whose value says which variant a row is. 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")
}A field name is written bare, as type is in @@discriminator(type), and a database name is quoted, as in @@map("task"). Rows whose type column holds "bug" are Bug records. A variant reuses its base model's fields, so a Bug has id, title, and type as well as severity and stepsToRepro. A variant does not declare an @id of its own: it takes the base model's primary key.
You query a variant through the base model: db.orm.public.Task.variant('Bug') returns the Bug rows only. Here public is the PostgreSQL schema. Writing a row of a variant starts with the same variant('Bug') call. See variant().
On a variant, @@map does more than rename. On PostgreSQL it chooses between two storage layouts. Give the variant its own @@map, as Bug has here, and its fields are in a table of their own that shares the base model's primary key. Leave @@map out and they are nullable columns in the base table. Relational data modeling covers choosing between the two.
On MongoDB, a variant adds its fields to documents in the base model's collection, so it declares @@base but no @@map of its own.
Extension types
An extension pack is an npm package that adds field types Prisma ORM does not ship, such as vectors. Install the pack, add the import and the extensions line to the config shown above, then call its types in the types block:
npm install @prisma/orm-extension-pgvectorimport pgvector from "@prisma/orm-extension-pgvector/control";
// inside ormConfig({ ... }), beside contract and db:
extensions: [pgvector],types {
Embedding1536 = pgvector.Vector(1536)
}
model Post {
id Uuid @id @default(uuid())
embedding Embedding1536?
}The pgvector part of pgvector.Vector(1536) is a fixed name the pack declares, not the name you gave the import. List the pack before using its types. Run npx prisma contract emit again after changing the extension list. Using extensions covers installing a pack and names the packs you can add.
Starting from an existing database
If the database already exists, don't write the contract by hand. contract infer reads the live schema and writes a starter contract.prisma for you to review and edit.
Prompt your coding agent
Projects created with npm create prisma@latest -- my-app include the Prisma ORM skills for your coding agent. Skills are instruction files the agent reads. In an existing project, run npx prisma skills sync. The prisma-8 skill covers PSL authoring. Ask your agent to:
- "Using the prisma-8 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
- Run
npx prisma contract emitand inspectcontract.jsonandcontract.d.ts. You do not import them yourself.db, the client that reads both, is insrc/prisma/db.ts, andprisma orm initwrites that file. See transactions and runtime. - If you prefer defining models in code, see authoring in TypeScript.
- Plan changes to a database you have already created with
migration plan.
The data contract
The data contract is the one description of your data model and how it is stored. Prisma ORM types your queries, plans your migrations, and checks your database against it.
Author in TypeScript
Define the Prisma ORM contract with a typed builder in TypeScript instead of a schema file. Same models, same `contract.json` and `contract.d.ts`, no separate language.
