Core concepts
Prisma ORM 8 is the current major version. To meet the evolving needs of developers, it has been rebuilt in TypeScript according to one idea: your application and database follow an explicit, checkable agreement of how all data is structured.
This idea relies upon a small vocabulary which repeats throughout: in the CLI, in the query APIs, and in error messages. The sections below define each term in plain language; each also links to relevant in-depth information.
The contract is your description of the data your application needs: the models, their fields, how they relate, and how they map to database tables or collections. You author it in PSL (the Prisma schema language) in a .prisma file, or in TypeScript:
// use prisma-8
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
published Boolean @default(false)
userId Int
user User @relation(fields: [userId], references: [id])
}The schema differs in that it is the actual structure of the database: the tables or collections, plus their indexes, that exist right now. The contract lives in your repository; the schema lives in the database. Everything Prisma ORM does is a relationship between the two: queries are typed against the contract, migrations move the schema toward the contract, and verification checks that the schema still satisfies it.
Read more in The data contract, and author it in PSL or in TypeScript.
Emitting is the build step that compiles your contract source into two plain files:
bunx prisma contract emitpnpm prisma contract emityarn prisma contract emitnpx prisma contract emitcontract.json: a canonical JSON description of your models, storage layout, and required capabilities.contract.d.ts: the TypeScript types derived from it, which is what makes your queries type-safe.
Every other part of the toolchain reads these artifacts, not your source file: the query APIs read contract.d.ts for types, the migration planner diffs two contract.json files, and the runtime verifies contract.json against the database. That is why contract emit comes first in almost every workflow: after any contract change, emit before you plan, migrate, or run.
Emission is deterministic: the same source always produces byte-identical artifacts, so both files are committed to version control and diff cleanly in code review. Think of the pair like package.json and package-lock.json: the source is what you ask for, the artifacts are the exact resolved result.
See contract.json and contract.d.ts for what is inside each file.
contract.json: a canonical JSON description of your models, storage layout, and required capabilities.contract.d.ts: the TypeScript types derived from it, which is what makes your queries type-safe.
Every other part of the toolchain reads these artifacts, not your source file: the query APIs read contract.d.ts for types, the migration planner diffs two contract.json files, and the runtime verifies contract.json against the database. That is why contract emit comes first in almost every workflow: after any contract change, emit before you plan, migrate, or run.
Emission is deterministic: the same source always produces byte-identical artifacts, so both files are committed to version control and diff cleanly in code review. Think of the pair like package.json and package-lock.json: the source is what you ask for, the artifacts are the exact resolved result.
See contract.json and contract.d.ts for what is inside each file.
A hash is a short fingerprint computed from a file's content: the same content always produces the same hash, while any change will produce a different one. Because emission is deterministic, hashing contract.json gives an identifier for that exact contract state, the way a Git commit hash identifies an exact state of your code. Contract hashes appear throughout the CLI; a migration, for example, records the hash it starts from and the hash it produces.
The database carries the other half of the agreement: a signature, a small marker record stored in the database itself that names the contract hash the database currently satisfies. db sign writes it, and db migrate updates it each time it applies a migration.
The two halves make the agreement checkable from either side:
- Before executing queries, the runtime can compare the contract that your application was built with against the database's signature and stop when it encounters a mismatch, for example a deploy against an unmigrated database, before it produces incorrect results.
- Before applying a migration, the runner checks that the database's signature matches the contract hash the migration starts from.
When the contract and the database disagree, the resulting state is called drift. db verify is the read-only command that reports it.
A plan is the compiled form of a query: a plain data object holding the statement to run, its parameters, and metadata about what the query touches. Every query, whichever API produced it, becomes a plan before it executes; running the plan is a separate step.
With the SQL query builder, the two steps are visible in your code:
import { db } from "./prisma/db";
const plan = db.sql.public.Post
.select("id", "title", "userId")
.where((f, fns) => fns.eq(f.published, true))
.limit(10)
.build();
const publishedPosts = await db.runtime().query(plan);Plans matter for two reasons:
- Every query goes through the same pipeline. However a query is written (the ORM client, a query builder, a raw fragment, or an API that an extension added), it reaches the database as a plan. Middleware sees every query in the same shape, execution works the same way for all of them, and the query APIs can be mixed freely. In addition, one policy (an authorization check, for instance) can sit in one place and see everything.
- A plan is data. The statement and its parameters exist as an object before anything touches the database: middleware can check them, telemetry can record them, and a failed query can report exactly what it ran.
All query APIs are typed against the contract and all produce plans. They differ in how much of the statement you write yourself.
The ORM client is where you start on both databases: model-based queries such as db.orm.public.User.where(...). It is more than a query builder; for example, the .include() operation coordinates several queries on your behalf to serve higher-order needs, relation traversal above all, and hands back one typed result. For more information, start with Reading data.
Beneath it, each database family has a typed builder for the queries that the ORM client cannot express, and a raw escape hatch below that. A builder plan compiles to exactly one statement, so what you build is what runs:
| PostgreSQL | MongoDB | |
|---|---|---|
| Typed builder | The SQL query builder: composable joins, grouping, projections | The pipeline builder: typed aggregation pipelines |
| Raw escape hatch | Raw SQL: fns.raw fragments spliced into builder queries, or whole statements written with db.raw.sql |
Raw commands sent to the driver |
Raw queries are still plans, so middleware and telemetry see them as any other query. A whole db.raw.sql statement declares its row shape with .returnsRow(spec), so its rows come back decoded. fns.raw fragments and MongoDB raw commands carry no result shape, so you must handle those values yourself; the raw queries reference explains what that means for different databases.
One facade package connects your code to your database. A PostgreSQL project installs @prisma/orm-postgres, and its config helper wires up everything underneath: the database family (SQL), the target dialect (PostgreSQL), the adapter that translates plans into that dialect, and the driver that holds the network connection. These four names recur in error messages and extension docs; day to day, you configure the one facade package and move on.
Prisma ORM's layering exists for extensibility. Our core remains small, because it is focused; everything around it, including PostgreSQL support, plugs in through the same public interfaces. A new database is supported by writing a new target, adapter and driver, without any need to change the core.
A capability is a specific feature that a database may or may not support, such as RETURNING clauses or vector indexes. Your contract declares the capabilities it needs; the adapter reports what the connected database provides. Prisma ORM compares the two at startup, so a missing feature surfaces as one clear error when the app boots, instead of as a failed query later. See Supported database features for more information.
A codec converts values between JavaScript and the database's wire format, in both directions. Every column type in your contract is backed by one: a PostgreSQL timestamptz column has a codec that produces a JavaScript Date when you read and encodes it back when you write. This means that when you pick a column type in PSL, you are also picking the codec that will handle every value that column carries.
Extensions bring codecs for the types they add: with pgvector installed, a Vector(1536) column comes back as a typed vector rather than a string. Raw fragments and raw commands skip this conversion step, so with those you convert the values yourself.
An extension is an installable package that plugs new pieces into the toolchain: column types and their codecs, query operations, index kinds, capabilities, and, at the widest, support for an entire database. One package extends the contract language, the emitted types, the query builders, and migrations together.
Extensions are declared in prisma.config.ts and registered on the client:
import { definePrismaConfig } from 'prisma/config';
import pgvector from '@prisma/orm-extension-pgvector/control';
import { defineConfig as ormConfig } from '@prisma/orm-postgres/config';
export default definePrismaConfig({
orm: ormConfig({
contract: './src/prisma/contract.prisma',
extensions: [pgvector],
db: {
connection: process.env['DATABASE_URL']!,
},
}),
});Subsequently, pgvector.Vector(1536) is a column type in your contract, vector operators appear in the query builder, and migration plan knows how to create vector indexes. See Using extensions.
A middleware is a plain object with a name and one or more hooks that run around every query, following the same principles as in Express or Koa. You need only register it once, in the middleware option of your client setup. Because every query is a plan, middleware gets a structured object to inspect: it can log it, enforce limits on it, or reject it, without changing how queries are written. That makes middleware the place for one policy that must cover the whole app, such as an authorization rule that examines every plan before it runs.
Three middleware ship with Prisma ORM at present, and they are in the early stages: treat them as working demonstrations of the pattern rather than finished products. Budgets caps row counts and surfaces slow queries, lints blocks risky query shapes, and cache serves repeated reads from memory. For policy that your app depends on, write your own; the middleware API is the durable surface. Start by exploring How middleware works.
A migration is a recorded change to your database. Most migrations change the database schema from what one contract describes to what another describes, so each one records which contract hash it starts from and which it ends at, to. A migration that only changes rows, such as a backfill, starts and ends at the same contract state. On disk, a migration is a directory in your repository that holds the change as editable TypeScript (migration.ts), the compiled operations that Prisma ORM runs (ops.json), and that from and to metadata (migration.json).
Of the commands that work with migrations, only db migrate changes a database: it applies the recorded migrations to it. The migration ... commands create and inspect the migration files in your repository. Of those, only migration status and migration log connect to a database, and they only read it.
Because every migration records its from and to hashes, the migrations in a repository form a graph: contracts are the nodes, migrations are the edges. When two branches each add a migration and both merge, the graph has a fork and a join, and db migrate works out which migrations to run, given the contract state the database matches and the one you name. No renumbering, no rebasing migration files.
A ref is a named pointer at a contract, such as production or staging, managed with migration ref. Refs let deployment commands target an environment by name: db migrate --to production.
If you know Git, the whole vocabulary maps across:
| Git | Prisma ORM |
|---|---|
| A commit | A contract, identified by its hash |
| A patch between commits | A migration |
| A branch or tag | A ref |
HEAD |
The database signature |
git checkout <commit> |
db migrate --to <contract> |
Start with How migrations work, then The migration graph for the branching story.
One rule divides the whole CLI: db ... commands connect to a live database and can change it, while contract ... and migration ... commands work on the files in your repository. The one exception is contract infer, which reads a live database, changing nothing in it, to write a starter contract file. If you are unsure what a command might touch, its first word provides the answer: only db can change a database.
The commands compose into four everyday workflows.
The development loop. Edit your contract, emit it, turn the change into a reviewable migration, apply it:
bunx prisma contract emit
bunx prisma migration plan --name add_user_phone
bunx prisma db migrate --advance-ref dbpnpm prisma contract emit
pnpm prisma migration plan --name add_user_phone
pnpm prisma db migrate --advance-ref dbyarn prisma contract emit
yarn prisma migration plan --name add_user_phone
yarn prisma db migrate --advance-ref dbnpx prisma contract emit
npx prisma migration plan --name add_user_phone
npx prisma db migrate --advance-ref db--advance-ref db records what you just applied, so the next migration plan starts from there. The db ref explains it.
Prototyping without migration files. While a schema is still in flux, skip the migration directory and reconcile the database directly. db update diffs the live schema against the emitted contract and applies the difference; --dry-run previews it first:
--advance-ref db records what you just applied, so the next migration plan starts from there. The db ref explains it.
Prototyping without migration files. While a schema is still in flux, skip the migration directory and reconcile the database directly. db update diffs the live schema against the emitted contract and applies the difference; --dry-run previews it first:
bunx prisma contract emit
bunx prisma db update --db "$DATABASE_URL" --dry-run
bunx prisma db update --db "$DATABASE_URL"pnpm prisma contract emit
pnpm prisma db update --db "$DATABASE_URL" --dry-run
pnpm prisma db update --db "$DATABASE_URL"yarn prisma contract emit
yarn prisma db update --db "$DATABASE_URL" --dry-run
yarn prisma db update --db "$DATABASE_URL"npx prisma contract emit
npx prisma db update --db "$DATABASE_URL" --dry-run
npx prisma db update --db "$DATABASE_URL"When the shape settles, switch to migration plan so changes become reviewable files.
Adopting an existing database. contract infer writes a starter contract from a live schema. Review and edit it, emit, then bring the database under contract management: db init applies only additive changes and writes the first signature. If the database already matches the contract exactly, db sign records the signature without changing anything:
bunx prisma contract infer --db "$DATABASE_URL"
bunx prisma contract emit
bunx prisma db init --db "$DATABASE_URL"pnpm prisma contract infer --db "$DATABASE_URL"
pnpm prisma contract emit
pnpm prisma db init --db "$DATABASE_URL"yarn prisma contract infer --db "$DATABASE_URL"
yarn prisma contract emit
yarn prisma db init --db "$DATABASE_URL"npx prisma contract infer --db "$DATABASE_URL"
npx prisma contract emit
npx prisma db init --db "$DATABASE_URL"Deploying. Give each environment a ref, such as production, that names the contract state the environment should reach. When a change is ready to ship, point the ref at the new state with migration ref set, such as npx prisma migration ref set production <hash>, and commit the ref file it writes under migrations/app/refs/. The deploy pipeline then migrates the environment to its ref by name, and that one command is all it needs. db migrate refuses to run a migration whose files no longer match its hash, so the pipeline needs no separate check:
Deploying. Give each environment a ref, such as production, that names the contract state the environment should reach. When a change is ready to ship, point the ref at the new state with migration ref set, such as npx prisma migration ref set production <hash>, and commit the ref file it writes under migrations/app/refs/. The deploy pipeline then migrates the environment to its ref by name, and that one command is all it needs. db migrate refuses to run a migration whose files no longer match its hash, so the pipeline needs no separate check:
bunx prisma db migrate --db "$DATABASE_URL" --to productionpnpm prisma db migrate --db "$DATABASE_URL" --to productionyarn prisma db migrate --db "$DATABASE_URL" --to productionnpx prisma db migrate --db "$DATABASE_URL" --to productionmigration check runs the same file checks offline, which is useful before you commit a migration you edited by hand, and db verify checks, without changing anything, whether a database satisfies the contract.
migration check runs the same file checks offline, which is useful before you commit a migration you edited by hand, and db verify checks, without changing anything, whether a database satisfies the contract.
Projects scaffolded with create-prisma@latest install Prisma ORM skills for your coding agent. Ask your agent to:
- "Using the prisma-8 skill, explain the difference between our contract and the database schema."
- "Show me the plan the SQL query builder produces for this query."
- "Which of our CLI scripts touch the live database, and which are offline?"
- The data contract: the concept that this whole page hangs off, in depth.
- Reading data: put the ORM client to work against your contract.
- How migrations work: change your contract, then plan, review, and apply the migration, hands-on.