# Author in PSL (/docs/orm/contract-authoring/psl-syntax) > For the complete Prisma documentation index, see [llms.txt](https://www.prisma.io/docs/llms.txt). A markdown version of any docs page is available by appending `.md` to its URL. Write the Prisma ORM contract in the Prisma schema language you already know, plus the Prisma ORM 8 additions. Location: ORM > Contract authoring > Author in PSL PSL, the Prisma Schema Language, is the preferred way to author [your contract](https://www.prisma.io/docs/orm/contract-authoring/the-data-contract), the `contract.prisma` file that replaced `schema.prisma`. You usually write one file, `src/prisma/contract.prisma`, or [split it into several](#split-the-contract-across-several-files), and [`npx prisma contract emit`](https://www.prisma.io/docs/cli/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. The five biggest additions are listed here, and the rest of the page is the full reference for every attribute and block: * mixins: write a set of fields, attributes or enum members once and include it in several blocks. * 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](https://www.prisma.io/docs/orm/coming-from-prisma-orm-7#schema) lists every change to the schema file, including what each old `@db.` attribute becomes. ## A complete contract [#a-complete-contract] The first non-blank line of every contract file is `// use prisma-8`, because `contract emit` reads only the `.prisma` files that start with it. When the `contract` setting in `prisma.config.ts` names one file and that file lacks the line, `contract emit` fails with `CONTRACT.SOURCE_LOAD_FAILED`, and the error's details include `PSL_NO_OPTED_IN_SCHEMA_FILES`. When `contract` is a [glob that matches several files](#split-the-contract-across-several-files), a file without the line is left out of the contract without a warning, and `contract emit` fails only when none of the files has it. #### PostgreSQL ```prisma title="src/prisma/contract.prisma" // 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 @onCreate(uuid()) email String createdAt DateTime @default(now()) address Address? posts Post[] @@map("user") } model Post { id Uuid @id @onCreate(uuid()) title ShortName userId Uuid priority Priority @default(Low) createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) @@map("post") } ``` #### MongoDB ```prisma title="src/prisma/contract.prisma" // use prisma-8 type Address { street String city String zip String? country String } enum UserRole { @@type("mongo/string@1") Admin = "admin" Author = "author" Reader = "reader" } model User { id ObjectId @id @map("_id") email String role UserRole address Address? posts Post[] @@map("users") } model Post { id ObjectId @id @map("_id") title String authorId ObjectId author User @relation(fields: [authorId], references: [id]) @@index([authorId]) @@map("posts") } ``` `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`, and then [`npx prisma db init`](https://www.prisma.io/docs/cli/db-init) creates the tables. ## Point the config at the schema [#point-the-config-at-the-schema] The config's `contract` path names the file Prisma ORM reads. If the path ends in `.prisma`, Prisma ORM reads it as PSL, and if it ends in `.ts`, as TypeScript. The `db` key holds the connection URL: ```typescript title="prisma.config.ts" 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](https://www.prisma.io/docs/orm/coming-from-prisma-orm-7#schema) lists the other keys. ### Split the contract across several files [#split-the-contract-across-several-files] Set `contract` to a glob, such as `"./src/prisma/**/*.prisma"`, to write the contract in several files. Every matching file that starts with `// use prisma-8` becomes part of one contract, and a file you add later joins it on the next `contract emit`, with no change to the config. A model in one file can refer to a model in another. `contract emit` writes `contract.json` and `contract.d.ts` into the folder before the first wildcard, here `./src/prisma/`. See [Configuration](https://www.prisma.io/docs/cli/configuration#split-the-schema-across-several-files). ## Models and fields [#models-and-fields] Models declare fields with a type, an optional `?` marker, and attributes. [Scalar fields](https://www.prisma.io/docs/orm/data-modeling#scalar-fields) lists the types a field can hold, among them `String`, `Int`, `Boolean`, `Decimal`, `DateTime`, `Json`, and `Bytes`. On PostgreSQL, plain `String` is a `text` column, `Int` is `int4`, and `DateTime` is `timestamptz`. You can also use one of these PostgreSQL types as the field type: `VarChar`, `Char`, `Numeric`, `Timestamp`, `Timestamptz`, `Time`, `Timetz`, `Date`, `Uuid`, `Inet`, `SmallInt`, `Real`, `Bit`, `VarBit`, or `Interval`. Their arguments are optional: `VarChar(35)`, `Char(2)`, `Bit(8)`, and `VarBit(64)` set a length, `Numeric(10, 2)` sets a precision and scale, and `Timestamp(3)`, `Timestamptz(3)`, `Time(3)`, `Timetz(3)`, and `Interval(3)` set how many digits of a second are kept. Write each argument as a whole number without quotes: `VarChar("35")` and `VarChar(35.0)` are refused with `PSL_VALUE_TYPE_INCOMPATIBLE`. `Timestamptz` without an argument is the same column as `DateTime`, so use it only when you want to set that precision. On PostgreSQL, the date and time types reach your code as `Temporal` values: `DateTime` and `Timestamptz` as a `Temporal.Instant`, `Timestamp` as a `Temporal.PlainDateTime`, `Date` as a `Temporal.PlainDate`, and `Time` as a `Temporal.PlainTime`. Your app needs a global `Temporal` to read and write them, and [Scalar fields](https://www.prisma.io/docs/orm/data-modeling#scalar-fields) says how to check for it. When you want a string instead, use `DateString`, a `date` column your code gets as a string, or `TimestampString`, `TimestamptzString`, and `TimeString`, which do the same for `timestamp`, `timestamptz`, and `time`. `TimestamptzJsDate` is a `timestamptz` column your code gets as a JavaScript `Date`, so it needs no `Temporal`, but a `Date` drops the microseconds PostgreSQL stores. * `@id` marks the primary key. `@@id([a, b])` declares a composite key. * `@unique` adds a unique constraint on one field. `@@unique([userId, title])` adds one across several fields. * `@@index([...])` declares a secondary index. * `@default(...)` sets a database default: the database fills in the column when an insert leaves it out. The default is a literal, `now()`, `autoincrement()`, or any other SQL expression. Write the SQL as a `sql` tagged literal, which is `sql` followed by the SQL in backticks, such as ``@default(sql`gen_random_uuid()`)``. See [Default values](#default-values) for how to write each kind of literal. * `@onCreate(...)` sets a value Prisma ORM generates when it creates the row, and `@onUpdate(...)` one it generates on every update. See [Generated values](#generated-values). * `@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. Without `@@map`, the table or collection has the model's name exactly as written, so `model UserProfile` is the table `"UserProfile"`. * ``@@check(expression: sql`total >= 0`, name: "order_total_positive")`` adds a check constraint of your own to the table. See [Check constraints](#check-constraints). * `@@control(observed)` says how far Prisma ORM manages the table. See [Control policy](#control-policy). A field can be a list on PostgreSQL: `tags String[]` is a `text[]` column, and the same works for `Int[]` and the other scalar types. Prisma ORM adds a check constraint to a list column so it cannot hold a `NULL` element. To allow `NULL` elements, put the `?` on the element type: `String?[]` allows `null` elements, `String[]?` allows a `null` list, and `String?[]?` allows both. In your code, `String?[]` is a `ReadonlyArray`, `String[]?` is a `ReadonlyArray | null`, and `String?[]?` is a `ReadonlyArray | null`. A `String?[]` column has no check constraint, so use it for a list column that already exists without one. A list of an enum or of a named type works the same way, and so does a list of a value object, as [Value objects](#value-objects) shows. How IDs map differs by database: #### PostgreSQL ```prisma model User { id Uuid @id @onCreate(uuid()) } ``` #### MongoDB ```prisma model User { id ObjectId @id @map("_id") } ``` 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`, so type it `ObjectId` and map it to `_id`. Your code still names the field `id`, in rows, `where`, `select`, `orderBy`, and write data: `@map` changes only the name in the stored document. A field of a [value object](#value-objects) cannot take `@map`, because it is always stored under its own name. ### Generated values [#generated-values] `@onCreate(fn())` and `@onUpdate(fn())` name a function that Prisma ORM runs in your application to fill the field: `@onCreate` when it creates the row and you leave the field out, `@onUpdate` on every update. The functions are: * `uuid()`, a version 4 UUID, the same as `uuid(4)`, and `uuid(7)`, a version 7 UUID. * `cuid(2)`, `ulid()`, and `ksuid()`. * `nanoid()`, 21 characters long, and `nanoid(n)`, `n` characters long, from 2 to 255. * `now()`, the current time. ```prisma model Post { id String @id @onCreate(uuid(7)) slug String @onCreate(nanoid(12)) createdAt DateTime @default(now()) updatedAt DateTime @onCreate(now()) @onUpdate(now()) } ``` Each function makes a value for the field's type: `uuid()` works on `String`, `VarChar(n)`, `Char(n)`, and `Uuid`, and `now()` works on every PostgreSQL date and time type, `Date`, `Time`, and `Timetz` included. The database does not fill these columns, so a row written by raw SQL or another application gets no value. A field may have both a database `@default` and an `@onCreate(...)`: `createdAt DateTime @default(now()) @onCreate(now())` takes its time from your application when Prisma ORM creates the row, and from the database for other inserts. On an optional field, `@onCreate(...)` fills the field when you leave it out, and an explicit `null` is stored as `null`. `@default(uuid())` and the other functions that generate a value are refused in `@default` with `PSL_GENERATOR_IN_DEFAULT`, which names the `@onCreate(...)` to write. `@default(now())` and `@default(autoincrement())` stay: they are database defaults. An argument a function does not take, such as `uuid(5)`, `cuid(1)`, or `nanoid(300)`, is refused with `PSL_INVALID_GENERATOR_ARGUMENT`, and `cuid()` needs its version, `cuid(2)`. An argument written with a fraction, such as `nanoid(8.0)`, or in quotes, is refused with `PSL_VALUE_TYPE_INCOMPATIBLE`. The value a function writes must also fit its column: `uuid()` writes 36 characters, `ksuid()` 27, `ulid()` 26, `cuid(2)` 24, `nanoid()` 21, and `nanoid(n)` `n`, so `VarChar(5) @onCreate(uuid())` is refused when you run `contract emit`. A bare `Char` holds one character, and a bare `VarChar` has no limit. Prisma ORM 8 has no `@updatedAt`, so write `@onCreate(now()) @onUpdate(now())`, as `updatedAt` above does, or `temporal.updatedAt()` where the field's type would go: ```prisma model Post { updatedAt temporal.updatedAt() } ``` Prisma ORM sets the field to the current time on every create and update, and you can still set it yourself on a write. `temporal.createdAt()` sets its field once, when the row is created. You do not import or declare `temporal`: it is a group of timestamp field types built into the contract syntax. On PostgreSQL the column is `timestamptz`. Both take the time from your application's clock, not from the database, so the column has no database default, and a row you insert with raw SQL must supply the value. For a value the database generates, use `DateTime @default(now())` instead. `temporal.createdAt()` and `temporal.updatedAt()` need a global `Temporal` for writes as well as reads. When your Node.js version has none, install `temporal-polyfill` and add `import "temporal-polyfill/full/global"` at the top of `src/prisma/db.ts`. `temporal.createdAtJsDate()` and `temporal.updatedAtJsDate()` work the same way, but with a JavaScript `Date` in place of a `Temporal` value. ### Columns of any database type [#columns-of-any-database-type] For a column of a database type no field type covers, write ``Raw(sql`...`)`` with the type written the way the database reports it. To see that text for a column that already exists, run [`prisma db schema`](https://www.prisma.io/docs/cli/db-schema): ```prisma model Category { id Int @id path Raw(sql`ltree`) area Raw(sql`circle`)? @default(sql`'<(0,0),1>'::circle`) } ``` Migrations create the column with that text, and [`db verify`](https://www.prisma.io/docs/cli/db-verify) compares it with the type the database reports, so write `integer`, not `int4`, and quote a name that needs it, as in ``Raw(sql`"MyType"`)``. Your code reads and writes the value as `unknown`, and a default is a `sql` tagged literal. `Raw("ltree")`, without the `sql` tag, is refused. `Raw` works on PostgreSQL and on SQLite, which Prisma ORM 8 supports as an [experimental package](https://www.prisma.io/docs/orm/supported-databases). ### Default values [#default-values] Write each default in a form its column can hold. A quoted string is text, for example, so it cannot be the default of a JSON, decimal, or floating-point column. When a default does not fit its column, `contract emit` fails with `PSL_VALUE_TYPE_INCOMPATIBLE`, pointing at the value you wrote, and the message says what to write instead, such as `Expected a number`. A single value on a list field fails with `PSL_DEFAULT_LIST_EXPECTED`, and text that is not a valid value of its kind, such as a `json` literal that is not a JSON document, fails with `PSL_INVALID_LITERAL`. The examples below show the form for each kind of column: ```prisma title="src/prisma/contract.prisma (excerpt)" model Account { id String @id @default(sql`gen_random_uuid()`) meta Jsonb @default(json`{ "plan": "free" }`) tags Jsonb @default(json`[1, 2]`) price Decimal @default(1.50) ratio Float @default(NaN) role String @default("member") active Boolean @default(true) startsOn Date @default(date`2024-01-01`) validFrom DateTime @default(timestamptz`2024-01-01T00:00:00Z`) payload Bytes @default(bytea`\x010203`) createdAt DateTime @default(now()) expiresAt DateTime @default(sql`(now() + '00:03:00'::interval)`) } ``` * A JSON default is a `json` tagged literal: `json` followed by the JSON document in backticks. Write a JSON array the same way, as `tags` shows. * A decimal default is a bare number. * `NaN`, `Infinity`, and `-Infinity` are written without quotes, and so are `true` and `false`. * A text default is a quoted string. * A fixed date, time, or interval is a tagged literal named after the column's PostgreSQL type: `timestamptz` for `DateTime` and `Timestamptz`, and `timestamp`, `date`, `time`, `timetz`, and `interval` for the types of those names, as in ``@default(interval`P1D`)``. A `timestamptz` default is a point in time with a UTC offset, such as ``timestamptz`2024-01-01T00:00:00Z` ``, and a `timestamp` default has no offset, such as ``timestamp`2024-01-01 00:00:00` ``. A quoted string on one of these fields is refused, and the message shows the tagged literal to write. * A `Bytes` default is a `bytea` tagged literal holding the bytes as hexadecimal after `\x`, as `payload` shows. Base64 text is not accepted. * On SQLite, a `DateTime` default uses the `datetime` tag, as in ``@default(datetime`2024-01-01T00:00:00Z`)``, and a `Bytes` default the `blob` tag, which holds hexadecimal digits, as in ``@default(blob`68656c6c6f`)``. * Each default, and each member of an `enum`, is checked by the column's type, so a value the column cannot hold unchanged is refused: more characters than a `VarChar(n)` or `Char(n)` takes, a number a `Numeric(p, s)` would round, more digits of a second than a `Timestamptz(3)` keeps, an integer outside the range of `Int` or `SmallInt`, or text that is not a UUID on a `Uuid` field. * A date or time default that the field's type does not hold makes `contract emit` fail with `PSL_INVALID_DEFAULT_LITERAL`, and the message shows a text the field takes. That includes an offset on a `Timestamp`, `Date`, or `Time` default, a missing offset on a `DateTime`, `Timestamptz`, or `Timetz` default, more than six digits after the decimal point (three on SQLite), and a date or time that does not exist, such as `2024-02-30`. The CLI checks these defaults on Node.js 24 too, which has no built-in `Temporal`. * On a field typed with an `enum` block, the default is the member's name without quotes, as in `@default(Low)`. On a `pg.enum(...)` field, it is the value the member stores, in quotes, as in `@default("admin")`. [Enums](#enums) covers both. * Any other SQL expression is a `sql` tagged literal, and it goes into the column default exactly as written. Write the current time as `now()` and a sequence as `autoincrement()`, not as `` sql`now()` `` or `` sql`autoincrement()` ``, which are refused. Prisma ORM 7's `dbgenerated("...")` is not accepted, and `contract emit` reports `PSL_UNKNOWN_DEFAULT_FUNCTION`. Write the same SQL as a `sql` tagged literal instead, so `@default(dbgenerated("gen_random_uuid()"))` becomes ``@default(sql`gen_random_uuid()`)``. [`contract infer`](https://www.prisma.io/docs/cli/contract-infer) writes every default in these forms. ### Indexes [#indexes] `@@index([...])` takes a list of fields, and on PostgreSQL the named arguments below. Either the list or `expression:` is required, and an `expression:` index needs a `name:`: ```prisma model User { @@index([name], where: sql`(name IS NOT NULL)`, name: "user_name_active") @@index(expression: sql`lower(handle)`, unique: true, name: "user_handle_lower") @@index([slug], type: "hash", name: "user_slug_hash") } ``` * `expression:` is the whole index expression as SQL, in place of the field list. * `where:` makes a partial index; it is the condition as SQL, without the `WHERE` keyword. * `unique:` makes a unique index. `@@unique([...])` is a unique constraint, which is the usual way to say a value must be unique; use `unique:` on `@@index` when you also need `expression:` or `where:`. * `type:` picks the index method, such as `"hash"` or `"gin"`. * `name:` names the index. The name in the database is not exactly what you typed: it gets an eight-character hash on the end, `user_name_active_000a85d8`, and if you change `name:` later, the next `migration plan` renames the index instead of dropping and recreating it. `map:` instead sets the exact name, for an index that already exists in the database, which is what [`contract infer`](https://www.prisma.io/docs/cli/contract-infer) writes for indexes it finds. Give one or the other, not both. `expression:` and `where:` take a `sql` tagged literal, as `@default` does; a quoted string there is refused with `PSL_VALUE_TYPE_INCOMPATIBLE`. Inside the literal no quote is escaped, and it may span lines. Prisma ORM stores the SQL with its shared indentation and the blank lines at its start and end removed. The SQL goes into the database as written, so it uses column names, not field names (they differ when a field has `@map`), and you quote the names yourself. Prisma ORM does not check the SQL until the migration runs. On MongoDB, `@@index` takes different arguments; see [MongoDB indexes](#mongodb-indexes). ### Full-text indexes [#full-text-indexes] `@@fullTextIndex([...])` creates the GIN index that PostgreSQL [full-text search](https://www.prisma.io/docs/orm/reference/orm-client#full-text-search) uses. It takes one or more text fields, and it needs either `name:` or `map:`, but not both: ```prisma title="src/prisma/contract.prisma (excerpt)" model Post { id Int @id title String subtitle String? body String? @@fullTextIndex([title], name: "post_title_search") @@fullTextIndex([[title, subtitle], body], name: "post_search") } ``` Each top-level item of the list is a weight group, strongest first, and an item can itself be a list of fields that share a weight. In `post_search`, a match in `title` or `subtitle` ranks above a match in `body`. An index takes one to four groups, a group cannot be empty, and a field can appear only once. In an index of several fields, Prisma ORM wraps every column in `coalesce(column, '')`, so the index does not change when a column becomes optional or required. `language:` sets the text-search language, which tells PostgreSQL how to break the text into searchable words. It defaults to `english`, and you set it as in `@@fullTextIndex([title], name: "post_title_search", language: "german")`. `where:` makes it a partial index, as on `@@index`. A field typed `pg.enum(...)`, a PostgreSQL enum type as [Enums](#enums) describes, cannot have a full-text index. PostgreSQL only. To search an index, name it in the query: `fns.fullTextMatches(indexes.post_search, q)` in an ORM `where` callback, as [Full-text search](https://www.prisma.io/docs/orm/reference/orm-client#full-text-search) shows. The query then reads the index's fields, weights and language from the contract, so it searches exactly what the index covers and PostgreSQL can use the index. For a partial index, made with `where:`, PostgreSQL uses it only when the query's own `where` also implies the index's condition, so add that condition to the query and check the plan with `EXPLAIN`. You can also search one text field with its own `fullTextMatches` and `fullTextRank` functions, as in `p.title.fullTextMatches(query, { language: "german" })`; give that search the same language as the index, because PostgreSQL does not use the index for a search in another language. ### Check constraints [#check-constraints] `@@check` adds a check constraint you write yourself, on top of the ones Prisma ORM generates for enum and list columns: ```prisma model Order { id Int @id @default(autoincrement()) total Decimal @@check(expression: sql`total >= 0`, name: "order_total_positive") } ``` `expression:` is the condition as a `sql` literal, using column names, as on `@@index`. `name:` and `map:` work as they do on `@@index`: `name:` for a constraint you are adding, and `map:` for one that already exists in the database under that exact name. A model can carry any number of `@@check` attributes. PostgreSQL only. Prisma ORM also adds two check constraints of its own: one on every enum column, so it accepts only the enum's values, and one on every list column, so it rejects `NULL` elements. A list whose elements may be `NULL` is written `T?[]`, and it gets no check, as [Models and fields](#models-and-fields) describes. ### Control policy [#control-policy] `@@control(...)` says how far Prisma ORM manages the table, with one of four values: | Value | `db verify` | Migrations | | ----------------------- | ----------------------------------------------------------------------- | --------------------------------------------- | | `managed` (the default) | The table must exist and match the model exactly. | Create, alter, and drop it. | | `tolerated` | Declared columns must match; extra columns are accepted. | Create it if missing; never alter or drop it. | | `external` | Declared columns must match; extra columns and constraints are ignored. | Never touch it. | | `observed` | Anything goes; a mismatch is a warning, not a failure. | Never touch it. | Put it on a model whose table something else owns, such as an audit table another system writes: ```prisma model AuditLog { id Int @id @default(autoincrement()) message String @@control(observed) } ``` ## Mixins [#mixins] A mixin is a named set of members you write once and include in several blocks. Declare it with the block's keyword followed by `mixin`, and include it with `+` and its name: ```prisma model mixin Timestamps { createdAt DateTime @default(now()) updatedAt temporal.updatedAt() @@index([createdAt]) } enum mixin BaseRoles { ADMIN = "admin" USER = "user" } model User { id Int @id +Timestamps email String } enum Role { @@type("pg/text@1") +BaseRoles GUEST = "guest" } ``` The contract is the same as if the mixin's members were written where the `+` line is, so `User` has the fields `id`, `createdAt`, `updatedAt` and `email`, in that order, and the index on `createdAt`, and `Role` has the members `ADMIN`, `USER` and `GUEST`. * A `model mixin` goes only in models, a `type mixin` only in composite types, an `enum mixin` only in enums, and a mixin for a block an extension adds, such as `policy_select mixin`, only in blocks of that keyword. * A field or enum member that the block already has, from its own body or from another mixin, is an error, and so is including the same mixin twice in one block. * A mixin's own attributes can name only its own fields, as `@@index([createdAt])` does. A model's attributes can name the fields a mixin gives it, as in `@@id([tenantId, id])`. * A mixin cannot include another mixin, and it is not a type, so it cannot be a field's type. * A mixin declared in `namespace auth { }` is included from another namespace as `+auth.Timestamps`, and the types of its fields are looked up in `auth`. The language server completes mixin names after `+`, and goes to, hovers, renames and colors them. ## Named types [#named-types] The `types` block gives a database column type a name you can reuse on many fields: ```prisma 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`. You do not have to name a type: a field can use the native PostgreSQL type directly, such as `VarChar(35)`, `Numeric(10, 2)`, `Uuid`, or `Timestamptz`. Three types cover big integers, and they differ in what your code receives: | Type | Column | In your code | | -------------- | --------- | --------------------------------------------------------------------------- | | `BigInt` | `bigint` | a JavaScript `bigint` | | `BigIntNumber` | `bigint` | a JavaScript `number`; a read or write outside the safe integer range fails | | `UnboundedInt` | `numeric` | a JavaScript `bigint` of any size | In Prisma ORM 8 the native type is the field's type, so `String @db.VarChar(35)` from Prisma ORM 7 becomes `VarChar(35)`, and the `@db.` attributes are gone. The `types` block is PostgreSQL only. ## Enums [#enums] An enum lists its members. It can also say, with `@@type`, how their values are stored, and what each member stores: ```prisma 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. Each member's value is a value of the enum's type, written as a `@default` of that type is: a quoted string for text, a bare number for a number type, and a tagged literal for a date or time. A quoted member on a number type, such as `Half = "0.5"` under `@@type("pg/numeric@1")`, is refused, and the message shows what to write, here `Half = 0.5`. Two members may not store the same value. Prisma ORM compares the values, not the text you wrote, so on `pg/numeric@1` the members `0.5` and `0.50` store the same value and are refused. Some types cannot hold enum members, so JSON (`pg/json@1` and `pg/jsonb@1`), byte strings (`pg/bytea@1`), text-search queries (`pg/tsquery@1`), time with a time zone (`pg/timetz@1`), vectors (`pg/vector@1`), and geometries (`pg/geometry@1`) are refused in `@@type`: use a text enum instead. For a PostgreSQL `enum` type, use a `native_enum` block, not `@@type("pg/enum@1")`. `@@type` is optional, and when you leave it out 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. At runtime, `db.enums` holds each member as a query returns it, such as a `bigint` for `pg/int8@1`. 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)`: ```prisma native_enum Role { admin = "admin" member = "member" } model User { role pg.enum(Role) } ``` Unlike an `enum` block, a `native_enum` block needs a value for every member. `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`. A field can name a `native_enum` declared in another [namespace](#namespaces) by putting the namespace first, as in `pg.enum(auth.OrderStatus)`. ## Value objects [#value-objects] A `type` block declares a value object: a structured value stored inside its parent row, with no table of its own. ```prisma type Address { street String city String zip String? country String } model User { id Uuid @id @onCreate(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, while 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, and [MongoDB data modeling](https://www.prisma.io/docs/orm/data-modeling/mongodb#embed-or-reference) covers it. ## Relations [#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, and the other side declares a list: ```prisma 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, and each takes `Cascade`, `Restrict`, `NoAction`, `SetNull`, or `SetDefault`: ```prisma model Post { authorId Uuid editorId Uuid? author User @relation("Authored", fields: [authorId], references: [id], onDelete: Cascade) editor User? @relation("Edited", fields: [editorId], references: [id], onDelete: SetNull) } model User { posts Post[] @relation("Authored") edited Post[] @relation("Edited") } ``` On a SQL database, each relation's foreign key gets an index on its columns, so that deleting or updating a referenced row can find the rows that point at it. If the model already has a plain index, a unique constraint or a primary key on exactly those columns, that one serves instead and no second index is created. A partial index, or one with a non-default `type` or `options`, does not count. To use an index whose first columns are the foreign key's, name it on the relation with `index: ""`, using the `name` or `map` you gave it; to have no index at all, write `index: false`. An unnamed `@@index` on exactly the columns of a unique constraint or the primary key is left out of the contract, because the unique one already serves its lookups. When two relations join the same two models, as `Authored` and `Edited` do here, give each pair the same `@relation("Name")` on both ends so Prisma ORM can tell which list belongs to which foreign key; otherwise `npx prisma contract emit` fails with an error whose `code` is `PSL_AMBIGUOUS_BACKRELATION`. 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: ```prisma model Profile { userId Uuid @unique user User @relation(fields: [userId], references: [id]) } ``` Many-to-many relations need a model for the join table, and 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, and its `@@id([...])` must list exactly the foreign-key fields and nothing else. ```prisma 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](https://www.prisma.io/docs/orm/data-modeling/relational-databases) and [MongoDB data modeling](https://www.prisma.io/docs/orm/data-modeling/mongodb). ## Namespaces [#namespaces] A PostgreSQL schema other than `public` is a `namespace` block, and the models inside it get their tables there: ```prisma namespace audit { model AuditLog { id Int @id @default(autoincrement()) message String @@map("audit_log") } } ``` Models outside any block are in `public`, the same namespace as a `namespace public { }` block, so the top level and that block share one set of names: declaring `Account` in both is `PSL_DUPLICATE_DECLARATION`, and so is a top-level declaration named `public`. On SQLite and MongoDB the same holds for the names `unbound` and `__unbound__`. In queries, the block name is the segment after `db.orm`, so this model is `db.orm.audit.AuditLog`. Two `namespace` blocks with the same name in one file are one namespace, so you can open a block again further down the file. A relation can also point at a model that an [extension pack](#extension-types), an npm package you add to your config, declares in a different PostgreSQL schema. Write that model as the relation field's type, in the form `:.`, where `` is the PostgreSQL schema. For example, the Supabase pack declares `AuthUser` in the `auth` schema: ```prisma title="src/prisma/contract.prisma (excerpt)" model Profile { id Uuid @id @onCreate(uuid()) userId Uuid @unique user supabase:auth.AuthUser @relation(fields: [userId], references: [id], onDelete: Cascade) } ``` The [Supabase extension](https://www.prisma.io/docs/orm/extensions) documents that form. ## Row-level security [#row-level-security] Two block kinds and one attribute declare PostgreSQL row-level security, and [`migration plan`](https://www.prisma.io/docs/cli/migration-plan) turns them into `ENABLE ROW LEVEL SECURITY` and `CREATE POLICY` statements. `@@rls` on a model turns row-level security on for its table. A `policy_select`, `policy_insert`, `policy_update`, `policy_delete`, or `policy_all` block declares one policy for one operation, and a `role` block declares a database role a policy names: ```prisma model User { id Uuid @id @onCreate(uuid()) @@map("user") @@rls } namespace unbound { role authenticated {} } policy_select user_self_read { target = User roles = [authenticated] using = sql`id = current_setting('app.user_id')::uuid` } policy_update user_self_write { target = User roles = [authenticated] using = sql`id = current_setting('app.user_id')::uuid` withCheck = sql`id = current_setting('app.user_id')::uuid` permissive = false } ``` Policy and role blocks assign their settings with `=`, unlike model attributes. `target` names the model, which must carry `@@rls`; put the policy blocks inside the same `namespace` block as the model when it has one. `roles` lists bare role names. A role Prisma ORM should create needs a `role` block, whose braces stay empty; a role that already exists in the database, such as `public`, is written bare with no block: `roles = [public]`. `using` and `withCheck` are the two conditions as `sql` literals, using column names: `policy_select` and `policy_delete` take `using`, `policy_insert` takes `withCheck`, and `policy_update` and `policy_all` take either or both. `permissive = false` makes the policy `AS RESTRICTIVE` in PostgreSQL's terms, so a row must pass it as well as the permissive policies. The block's name becomes the policy name with a hash on the end, as index names do. A `role` block must be inside `namespace unbound { }`; `unbound` is a reserved word meaning "not in any schema", which is where a role belongs. ## MongoDB indexes [#mongodb-indexes] On MongoDB, `@@index` and `@@unique` take a list of fields, each with an optional sort direction, plus MongoDB's own index options as named arguments, and `@@textIndex` declares a text index: ```prisma model Post { @@index([authorId]) @@index([sort(createdAt, Desc), authorId]) @@index([expiresAt], expireAfterSeconds: 3600, sparse: true) @@index([title], filter: "{ \"kind\": \"article\" }") @@index([location], type: "2dsphere") @@index([wildcard(meta)], exclude: ["meta.internal"]) @@textIndex([title, body], weights: { "title": 10, "body": 1 }, language: "english") } model User { @@unique([email], collationLocale: "en", collationStrength: 2) } ``` `sort(field, Asc)` or `sort(field, Desc)` in the field list sets a field's direction; this form is MongoDB only. Since `8.0.0-rc.17` the earlier `field(sort: Desc)` form is refused. The keys in `weights` are field names, quoted. The named arguments are `type` (`"text"`, `"2dsphere"`, `"2d"`, or `"hashed"`), `sparse`, `expireAfterSeconds`, `filter` (a partial filter expression, written as a JSON string), `default_language` and `languageOverride` for a `type: "text"` index, `include` or `exclude` for a wildcard index, and the collation options `collationLocale`, `collationStrength`, `collationCaseLevel`, `collationCaseFirst`, `collationNumericOrdering`, `collationAlternate`, `collationMaxVariable`, `collationBackwards`, and `collationNormalization`. `@@textIndex` takes `weights`, `language`, `languageOverride`, `filter`, and the same collation options. There is no `name:` on MongoDB; MongoDB names the index from its keys. A wildcard index covers every field under a path, which is how you index documents whose keys you do not know in advance. Write `wildcard()` in the field list to cover the whole document, or `wildcard(meta)` to cover the fields under `meta`; the key becomes `$**` or `meta.$**`. `include` and `exclude` are lists of field names, quoted, that narrow what the index covers; give one or the other, and only with `wildcard()`. An index can hold one `wildcard()`, and it cannot be an `@@unique`, take `expireAfterSeconds`, or set a `type:` such as `"hashed"`. ## Base models and variants [#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: ```prisma model Task { id Uuid @id @onCreate(uuid()) title String type String @@discriminator(type) @@map("task") } model Bug { severity String stepsToRepro String? @@base(Task, "bug") @@map("bug") } ``` Field and model names are written bare, as `type` and `Task` are, while table names and discriminator values are quoted, as `"task"` and `"bug"` are. 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`. The base model can be declared before or after its variants. A variant does not declare an `@id` of its own: it takes the base model's primary key. On PostgreSQL and SQLite the discriminator field must hold text: `String`, or on PostgreSQL `Char(n)` or `VarChar(n)`, and on SQLite `sql.String(n)`, SQLite's text type with a length, the counterpart of `VarChar(n)` on PostgreSQL. A variant can be in a different `namespace` block from its base, on PostgreSQL a different schema. Name the base with its namespace, as in `@@base(a.Task, "chore")`. In `contract.json`, the base model lists its variants by discriminator value, each naming the variant model and its namespace: `"variants": { "bug": { "model": "Bug", "namespace": "public" } }`. You query a variant through the base model: `db.orm.public.Task.variant('bug')` returns the `Bug` rows only, and `public` here is the PostgreSQL schema. `variant()` takes the discriminator value from `@@base`, not the model name. Writing a row of a variant starts with the same `variant('bug')` call. See [`variant()`](https://www.prisma.io/docs/orm/reference/orm-client#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](https://www.prisma.io/docs/orm/data-modeling/relational-databases#polymorphic-relations) 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 [#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: ```bash npm install @prisma/orm-extension-pgvector ``` ```typescript title="prisma.config.ts" import pgvector from "@prisma/orm-extension-pgvector/control"; // inside ormConfig({ ... }), beside contract and db: extensions: [pgvector], ``` ```prisma title="src/prisma/contract.prisma" // use prisma-8 types { Embedding1536 = pgvector.Vector(1536) } model Post { id Uuid @id @onCreate(uuid()) embedding Embedding1536? } ``` A field can also use the pack's type directly, with the argument named: `embedding pgvector.Vector(length: 1536)?`. 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, and run `npx prisma contract emit` again after changing the extension list. [Using extensions](https://www.prisma.io/docs/orm/extensions/using-extensions) covers installing a pack and names the packs you can add. ## Starting from an existing database [#starting-from-an-existing-database] If the database already exists, don't write the contract by hand: [`contract infer`](https://www.prisma.io/docs/cli/contract-infer) reads the live schema and writes a starter `contract.prisma` for you to review and edit. ## Prompt your coding agent [#prompt-your-coding-agent] Projects created with `npm create prisma@latest -- my-app` include the [Prisma ORM skills](https://www.prisma.io/docs/ai/tools/skills#available-skills-for-prisma-8) for your coding agent: skills are instruction files the agent reads. In an existing project, run `npx prisma skills sync` to add them. The `prisma-8` skill covers PSL authoring, so 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 [#next-steps] * Run `npx prisma contract emit` and inspect [`contract.json` and `contract.d.ts`](https://www.prisma.io/docs/orm/contract-authoring/the-contract-artifact). You do not import them yourself. `db`, the client that reads both, is in `src/prisma/db.ts`, and `prisma orm init` writes that file. See [transactions and runtime](https://www.prisma.io/docs/orm/reference/transactions-and-runtime). * If you prefer defining models in code, see [authoring in TypeScript](https://www.prisma.io/docs/orm/contract-authoring/typescript-schema-builder). * Plan changes to a database you have already created with [`migration plan`](https://www.prisma.io/docs/cli/migration-plan). ## Related pages - [`Author in TypeScript`](https://www.prisma.io/docs/orm/contract-authoring/typescript-schema-builder): 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. - [`contract.json and contract.d.ts`](https://www.prisma.io/docs/orm/contract-authoring/the-contract-artifact): contract.json and contract.d.ts are the two files every other part of Prisma ORM reads. Here is what is inside them. - [`Editor support`](https://www.prisma.io/docs/orm/contract-authoring/editor-support): What the Prisma VS Code extension does for a Prisma ORM contract, and what to do when it stops accepting the file. - [`Supported database features`](https://www.prisma.io/docs/orm/contract-authoring/capabilities): The contract records which database features your packages support, so Prisma ORM can reject an unsupported one early with a clear error. - [`The data contract`](https://www.prisma.io/docs/orm/contract-authoring/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.