From scratch
By the end of this page you have a project with a prisma.config.ts, a prisma/contract.prisma holding one User model, and a single index.ts that writes, updates, and reads rows in PostgreSQL through Prisma ORM 8, plus a migration you planned and applied. No template generates anything: you create every file yourself and see what each one is for.
Use this path when you want to see every file Prisma ORM needs, or when you are adding Prisma ORM to a repository that already has its own layout. If you would rather have the files written for you, run npm create prisma@latest and follow the PostgreSQL quickstart.
Two commands have new names in Prisma ORM 8. contract emit does what prisma generate did, and migration plan followed by db migrate does what migrate dev did. There is no @prisma/client package and no db push.
- Node.js 22.18 or newer (on the 24 line, 24.11 or newer); Node.js 24 is recommended.
- A connection string for an empty PostgreSQL database. Step 4 creates the tables and step 5 clears the
Usertable on every run, so do not point this page at a database that holds data you need. Any PostgreSQL URL works. If you don't have one,npx create-db@latestcreates a temporary Prisma Postgres database and prints its connection string, plus a claim URL if you want to keep it.
Create an empty directory with a package.json, and switch it to ES modules, because the Prisma ORM packages and the index.ts below use import syntax. npm init -y writes "type": "commonjs", and the second npm command changes it:
mkdir hello-prisma
cd hello-prisma
npm init -y
npm pkg set type=moduleInstall the runtime library, the command-line tool, and the tooling to run a TypeScript file:
bun add @prisma/orm-postgres dotenv
bun add --dev prisma tsx typescriptpnpm add @prisma/orm-postgres dotenv
pnpm add --save-dev prisma tsx typescriptyarn add @prisma/orm-postgres dotenv
yarn add --dev prisma tsx typescriptnpm install @prisma/orm-postgres dotenv
npm install --save-dev prisma tsx typescript@prisma/orm-postgres is the library your code imports, and prisma is the command-line tool. dotenv loads .env, tsx runs index.ts without a build step, and typescript 5.9 or newer is a peer dependency of @prisma/orm-postgres. The two Prisma packages have different version numbers (for example, prisma 8.0.0-rc.17 and @prisma/orm-postgres 8.0.0-rc.13); that is normal, and any two latest versions work together.
@prisma/orm-postgres is the library your code imports, and prisma is the command-line tool. dotenv loads .env, tsx runs index.ts without a build step, and typescript 5.9 or newer is a peer dependency of @prisma/orm-postgres. The two Prisma packages have different version numbers (for example, prisma 8.0.0-rc.17 and @prisma/orm-postgres 8.0.0-rc.13); that is normal, and any two latest versions work together.
The command-line tool reads prisma.config.ts from the project root to find your contract and your database:
import "dotenv/config";
import { definePrismaConfig } from "prisma/config";
import { defineConfig as ormConfig } from "@prisma/orm-postgres/config";
export default definePrismaConfig({
orm: ormConfig({
contract: "./prisma/contract.prisma",
db: {
connection: process.env["DATABASE_URL"]!,
},
}),
skills: {
check: false,
},
});contract is the path to the model file you write in the next step, and db.connection is the connection string the database commands use. The skills block turns off the notice that every command otherwise prints about installing agent skill files; remove it if you want those files. Every setting is documented on Configuration.
Put the connection string in .env, which the first line of the config file loads:
DATABASE_URL="postgresql://username:password@host:5432/database?sslmode=require"Add .env to .gitignore so the credentials stay out of version control.
The contract is Prisma ORM 8's name for the schema file. It replaces schema.prisma, and it has no datasource or generator blocks because the connection string lives in prisma.config.ts. Declare one model:
// use prisma-8
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
}Data modeling covers field types, primary keys, and relations.
4. Generate the contract and create the table
Section titled “4. Generate the contract and create the table”contract emit reads contract.prisma and writes prisma/contract.json (read at run time) and prisma/contract.d.ts (read by TypeScript) next to it. This is the step that was prisma generate; run it again after every change to the contract.
bunx prisma contract emitpnpm prisma contract emityarn prisma contract emitnpx prisma contract emit✔ Resolving contract source...
✔ Emitting contract...
│ contract: prisma/contract.json
│ types: prisma/contract.d.ts
✔ Emitted contract.json and contract.d.tsdb init creates the tables the contract declares in your database, then signs the database, which records which version of the contract it matches so later migrations know where to start from:
✔ Resolving contract source...
✔ Emitting contract...
│ contract: prisma/contract.json
│ types: prisma/contract.d.ts
✔ Emitted contract.json and contract.d.tsdb init creates the tables the contract declares in your database, then signs the database, which records which version of the contract it matches so later migrations know where to start from:
bunx prisma db initpnpm prisma db inityarn prisma db initnpx prisma db init✔ Introspecting database schema
✔ Planning migration
✔ Initialising database across spaces
│ contract: prisma/contract.json
│ database: postgres://****:****@db.prisma.io:5432/postgres?sslmode=require
✔ Applied 2 operation(s) across 1 contract space
App space
├─ Create table "User"
├─ Add unique constraint on "User" (email)
└─ marker b5ae4e66fad1f0b7dd5590568f75a12a6f4576e9f367a787a69a4e729abac1fb
✔ Advanced ref "db" → b5ae4e66fad1f0b7dd5590568f75a12a6f4576e9f367a787a69a4e729abac1fbThe marker line in the output is the record that signing stores in the database: the hash that identifies the version of the contract the database now matches. db init also writes migrations/app/refs/db.json, called the db ref: a file that records which contract version your development database is at, so that migration plan in step 6 knows where to start. The Advanced ref "db" line in the output is db init writing it. Commit it, together with the snapshot of the contract that db init writes under migrations/snapshots/. If the command fails with DRIVER.CONNECTION_FAILED, DATABASE_URL in .env is wrong or the database is not reachable. A SECURITY WARNING about SSL modes comes from the pg driver and does not stop the command; change sslmode=require to sslmode=verify-full in .env to silence it.
✔ Introspecting database schema
✔ Planning migration
✔ Initialising database across spaces
│ contract: prisma/contract.json
│ database: postgres://****:****@db.prisma.io:5432/postgres?sslmode=require
✔ Applied 2 operation(s) across 1 contract space
App space
├─ Create table "User"
├─ Add unique constraint on "User" (email)
└─ marker b5ae4e66fad1f0b7dd5590568f75a12a6f4576e9f367a787a69a4e729abac1fb
✔ Advanced ref "db" → b5ae4e66fad1f0b7dd5590568f75a12a6f4576e9f367a787a69a4e729abac1fbThe marker line in the output is the record that signing stores in the database: the hash that identifies the version of the contract the database now matches. db init also writes migrations/app/refs/db.json, called the db ref: a file that records which contract version your development database is at, so that migration plan in step 6 knows where to start. The Advanced ref "db" line in the output is db init writing it. Commit it, together with the snapshot of the contract that db init writes under migrations/snapshots/. If the command fails with DRIVER.CONNECTION_FAILED, DATABASE_URL in .env is wrong or the database is not reachable. A SECURITY WARNING about SSL modes comes from the pg driver and does not stop the command; change sslmode=require to sslmode=verify-full in .env to silence it.
Create index.ts, which builds the client from the two emitted files and then deletes, creates, updates, and reads rows on the User model:
import "dotenv/config";
import postgres from "@prisma/orm-postgres/runtime";
import type { Contract } from "./prisma/contract.d";
import contractJson from "./prisma/contract.json" with { type: "json" };
const db = postgres<Contract>({
contractJson,
url: process.env["DATABASE_URL"]!,
});
async function main() {
// Start from an empty table so you can run this file more than once.
await db.orm.public.User.where({}).deleteAll();
// Write: insert one row.
const alice = await db.orm.public.User.create({
email: "alice@prisma.io",
name: "Alice",
});
console.log("Created:", alice);
// Update: change one row, picked by its primary key.
const renamed = await db.orm.public.User
.where({ id: alice.id })
.update({ name: "Alice Smith" });
console.log("Updated:", renamed);
// Read: fetch every row.
const users = await db.orm.public.User.all();
console.log("All users:", users);
await db.close();
}
main().catch((error) => {
console.error(error);
process.exit(1);
});db.orm.public.User is the model: public is the PostgreSQL schema, and every model in your contract appears under it. .update() and .delete() need a .where() first, and .where({}) matches every row. Reading data and Writing data show the rest of the query API.
Run it:
bunx tsx index.tspnpm dlx tsx index.tsyarn dlx tsx index.tsnpx tsx index.tsCreated: { email: 'alice@prisma.io', id: 1, name: 'Alice' }
Updated: { email: 'alice@prisma.io', id: 1, name: 'Alice Smith' }
All users: [ { email: 'alice@prisma.io', id: 1, name: 'Alice Smith' } ]The id goes up by one each time you run the file, because the table is cleared but the sequence is not. If the run fails with Cannot find module '.../prisma/contract.json', you skipped contract emit in step 4.
Created: { email: 'alice@prisma.io', id: 1, name: 'Alice' }
Updated: { email: 'alice@prisma.io', id: 1, name: 'Alice Smith' }
All users: [ { email: 'alice@prisma.io', id: 1, name: 'Alice Smith' } ]The id goes up by one each time you run the file, because the table is cleared but the sequence is not. If the run fails with Cannot find module '.../prisma/contract.json', you skipped contract emit in step 4.
Add an optional field to the model:
// use prisma-8
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
phone String?
}Emit the contract again, then plan a migration. migration plan compares the emitted contract with the version the database was signed with and writes the difference as a migration directory, without connecting to the database:
bunx prisma contract emit
bunx prisma migration plan --name add_user_phonepnpm prisma contract emit
pnpm prisma migration plan --name add_user_phoneyarn prisma contract emit
yarn prisma migration plan --name add_user_phonenpx prisma contract emit
npx prisma migration plan --name add_user_phone✔ Planned baseline (3 operation(s)) + 1 operation(s)
migrations/app/20260917T1457_add_user_phone
└─ Add column "phone" to "User"
from: b5ae4e66fad1f0b7dd5590568f75a12a6f4576e9f367a787a69a4e729abac1fb
to: 300832575ace8feebb3d3442d1bca52150c46628de3e4c6c32a61cb0ab21100f
baseline: migrations/app/20260917T1456_baseline
app space: migrations/app/20260917T1457_add_user_phone
ℹ DDL preview
ALTER TABLE "public"."User" ADD COLUMN "phone" text;Two directories appear under migrations/app/: a baseline that records the table db init created, and your change. The baseline is never applied to this database, because the database is already signed at that state. Each migration directory holds a migration.ts you can read and edit; Generating a migration explains the files.
Apply the migration with --advance-ref db, which records that your development database is now at the new contract so the next migration plan starts from there:
✔ Planned baseline (3 operation(s)) + 1 operation(s)
migrations/app/20260917T1457_add_user_phone
└─ Add column "phone" to "User"
from: b5ae4e66fad1f0b7dd5590568f75a12a6f4576e9f367a787a69a4e729abac1fb
to: 300832575ace8feebb3d3442d1bca52150c46628de3e4c6c32a61cb0ab21100f
baseline: migrations/app/20260917T1456_baseline
app space: migrations/app/20260917T1457_add_user_phone
ℹ DDL preview
ALTER TABLE "public"."User" ADD COLUMN "phone" text;Two directories appear under migrations/app/: a baseline that records the table db init created, and your change. The baseline is never applied to this database, because the database is already signed at that state. Each migration directory holds a migration.ts you can read and edit; Generating a migration explains the files.
Apply the migration with --advance-ref db, which records that your development database is now at the new contract so the next migration plan starts from there:
bunx prisma db migrate --advance-ref dbpnpm prisma db migrate --advance-ref dbyarn prisma db migrate --advance-ref dbnpx prisma db migrate --advance-ref db✔ Applied 1 migration(s) (1 operation(s)) across 1 contract space(s)
App space
├─ Add column "phone" to "User"
└─ marker 300832575ace8feebb3d3442d1bca52150c46628de3e4c6c32a61cb0ab21100f
✔ Advanced ref "db" → 300832575ace8feebb3d3442d1bca52150c46628de3e4c6c32a61cb0ab21100fIf you do not want migration files for a throwaway database, db update applies the contract change directly instead of migration plan and db migrate.
Run index.ts again without changing it. The new column shows up in every row:
✔ Applied 1 migration(s) (1 operation(s)) across 1 contract space(s)
App space
├─ Add column "phone" to "User"
└─ marker 300832575ace8feebb3d3442d1bca52150c46628de3e4c6c32a61cb0ab21100f
✔ Advanced ref "db" → 300832575ace8feebb3d3442d1bca52150c46628de3e4c6c32a61cb0ab21100fIf you do not want migration files for a throwaway database, db update applies the contract change directly instead of migration plan and db migrate.
Run index.ts again without changing it. The new column shows up in every row:
bunx tsx index.tspnpm dlx tsx index.tsyarn dlx tsx index.tsnpx tsx index.tsCreated: { email: 'alice@prisma.io', id: 2, name: 'Alice', phone: null }
Updated: { email: 'alice@prisma.io', id: 2, name: 'Alice Smith', phone: null }
All users: [
{ email: 'alice@prisma.io', id: 2, name: 'Alice Smith', phone: null }
]Created: { email: 'alice@prisma.io', id: 2, name: 'Alice', phone: null }
Updated: { email: 'alice@prisma.io', id: 2, name: 'Alice Smith', phone: null }
All users: [
{ email: 'alice@prisma.io', id: 2, name: 'Alice Smith', phone: null }
]Check that the migration files on disk and the database agree:
bunx prisma migration statuspnpm prisma migration statusyarn prisma migration statusnpx prisma migration status○ 3008325 @contract @db (db)
│↑ 20260917T1457_add_user_phone b5ae4e6 → 3008325 1 ops ✓ applied
○ b5ae4e6
│↑ 20260917T1456_baseline ∅ → b5ae4e6 3 ops
○ ∅
✔ Up to dateFrom here, every schema change follows the same routine: edit prisma/contract.prisma, run npx prisma contract emit, run npx prisma migration plan --name <name>, and apply with npx prisma db migrate --advance-ref db.
○ 3008325 @contract @db (db)
│↑ 20260917T1457_add_user_phone b5ae4e6 → 3008325 1 ops ✓ applied
○ b5ae4e6
│↑ 20260917T1456_baseline ∅ → b5ae4e6 3 ops
○ ∅
✔ Up to dateFrom here, every schema change follows the same routine: edit prisma/contract.prisma, run npx prisma contract emit, run npx prisma migration plan --name <name>, and apply with npx prisma db migrate --advance-ref db.
- Data modeling for relations, more field types, and indexes.
- Generating a migration for what is in a migration directory and how to edit one.
- PostgreSQL quickstart if you want the same setup generated for you with a starter app.