Prisma ORM 8 is here.Read the docs
Quickstart

Add Prisma ORM and PostgreSQL to an existing app

Add Prisma ORM to an app you already have, create the tables in an empty PostgreSQL database, run a first query, and apply a first migration.

This page uses PostgreSQL. Use MongoDB instead.

Use this page when you already have an app and its database has no tables yet. By the end, your app has one model, the database has a table for it, and a script writes and reads a row. You also change the model once and apply the change to the database with a migration. If your database already has tables, follow Add Prisma ORM to an existing PostgreSQL database instead. If your app uses Prisma ORM 7 today, follow Prisma ORM 7 to 8 (PostgreSQL).

Prerequisites

  • A project directory with a package.json, on Node.js 22.18 or newer on the 22 line, 24.11 or newer on the 24 line, or 26 or newer (24 recommended).
  • A way to run a TypeScript file. This page uses tsx. If your project does not have it, run npm install --save-dev tsx typescript.
  • An empty PostgreSQL database, version 15 or newer. Step 2 shows how to get one if you have none.

1. Add Prisma ORM to the project

The orm init command below changes your tsconfig.json and your package.json. In tsconfig.json, it sets module to preserve and moduleResolution to bundler. If your package.json has no "type" field, it adds "type": "module". If it has "type": "commonjs", the command keeps it and prints a warning. If your app runs as CommonJS, for example because it loads files with require or because tsc compiles it to require calls, follow In a CommonJS project before you start your app again. tsx runs the scripts on this page in both kinds of app, so you can finish this page first.

From the root of your project, run:

bunx prisma@latest orm init --yes --target postgres --authoring psl --write-env

prisma@latest runs Prisma ORM 8, and --target postgres picks PostgreSQL. --authoring psl picks PSL, the Prisma Schema Language, which is the .prisma file format you know from Prisma ORM 7. --write-env writes a .env file, unless you already have one. --yes accepts the default answer to every question, so the command asks nothing.

The command installs the packages, writes the files, and ends with the summary below. In package.json, it adds the packages with their current version numbers, and a contract:emit script that runs prisma contract emit. If your project already has a tsconfig.json or a .gitignore, the command edits that file and keeps the rest of it.

│  target:     postgres
│  authoring:  psl
│  schema:     src/prisma/contract.prisma

written
├─ src/prisma/contract.prisma
├─ prisma.config.ts
├─ src/prisma/db.ts
├─ prisma-8.md
├─ .env.example
├─ .env
├─ tsconfig.json
├─ .gitignore
├─ .gitattributes
└─ package.json
installed
├─ @prisma/orm-postgres
├─ dotenv
├─ prisma@latest (dev)
├─ @types/node (dev)
└─ @prisma/cli-engine@0.7.0 (dev)

✔ Done. Open prisma-8.md to get started.

You work with three of these files:

  • src/prisma/contract.prisma holds your models. It is what schema.prisma was in Prisma ORM 7, and Prisma ORM 8 calls it the contract. The summary lists it as schema.
  • prisma.config.ts tells the prisma commands where the contract is and which database to connect to.
  • src/prisma/db.ts is the file your app imports to run queries.

You do not need prisma-8.md for this page. It is a short reference for writing queries. .gitattributes marks the files that Prisma ORM generates, so that GitHub collapses them in pull request diffs.

orm init wrote src/prisma/db.ts, and you do not need to change it:

src/prisma/db.ts
import 'dotenv/config';
import postgres from '@prisma/orm-postgres/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };

export const db = postgres<Contract>({
  contractJson,
  url: process.env['DATABASE_URL']!,
});

The first line loads .env, so every file that imports db reads DATABASE_URL from there. The two contract files it imports are written by contract emit in step 4.

From now on, every prisma command ends with the line Prisma agent skills are out of date. You can ignore it, because it does not change what the command does. Agent skills are instruction files that AI coding tools read, and the line appears because your project has none yet. To add them and stop the line, run npx prisma@latest init. In Prisma ORM 8, init adds the skills and a postinstall script that keeps them up to date, and it leaves your contract and your .env alone. orm init is the command that sets up Prisma ORM.

2. Set the connection string

If you had no .env, orm init wrote one with a placeholder, DATABASE_URL="postgresql://user:password@localhost:5432/mydb". Open .env and set DATABASE_URL to the connection string of your database:

.env
DATABASE_URL="postgresql://username:password@host:5432/database"

If you have no database at all, npx create-db@latest creates a temporary Prisma Postgres database and prints its connection string.

orm init added .env to .gitignore, so the password stays out of version control.

3. Write one model

orm init wrote a starter contract with a User and a Post model. Replace the contents of src/prisma/contract.prisma with one model:

src/prisma/contract.prisma
// use prisma-8

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
}

Keep the first line, because contract emit reads only .prisma files that start with it. The file has no datasource or generator block. The connection string comes from .env, and contract emit in the next step takes the place of the generator. Data modeling covers field types and relations.

4. Generate the files your code imports

Run contract emit after every change to the contract. It takes the place of prisma generate. From here on, commands start with npx prisma, without @latest. That runs the version of Prisma ORM that orm init installed in your project:

bunx prisma contract emit
✔ Resolving contract source...
✔ Emitting contract...
│  contract:  src/prisma/contract.json
│  types:     src/prisma/contract.d.ts

✔ Emitted contract.json and contract.d.ts

The command writes contract.json and contract.d.ts next to the contract. Commit both files, because db.ts imports them.

5. Create the table

bunx prisma db init
✔ Introspecting database schema
✔ Planning migration
✔ Initialising database across spaces
│  contract:  src/prisma/contract.json
│  database:  postgres://****@127.0.0.1:54329/app1

✔ Applied 2 operation(s) across 1 contract space

App space
├─ Create table "User"
├─ Add unique constraint on "User" (email)
└─ marker b5ae4e66fad1f0b7dd5590568f75a12a6f4576e9f367a787a69a4e729abac1fb

✔ Advanced ref "db" → b5ae4e66fad1f0b7dd5590568f75a12a6f4576e9f367a787a69a4e729abac1fb

db init creates the tables your contract declares. The output names two more things it wrote:

  • The marker is a record in the database. It holds the hash of your contract, which is the long text in the output, so that later commands can tell which version of the contract the database has.
  • The db ref is the file migrations/app/refs/db.json in your project. It holds the same hash. The line Advanced ref "db" means that the command wrote this file. migration plan in step 7 reads it to find out which contract your development database has.

You can ignore the word space in the output, because all your migrations are in migrations/app/.

Commit the migrations/ directory together with your code, because migration plan reads it on every later change. If the command fails with DRIVER.CONNECTION_FAILED, the value of DATABASE_URL in .env is wrong or the database is not reachable.

6. Write and read a row

Create script.ts in the root of your project:

script.ts
import { db } from "./src/prisma/db";

async function main() {
  const created = await db.orm.public.User.create({
    email: "alice@example.com",
    name: "Alice",
  });
  console.log("Created:", created);

  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 queries your models, the way Prisma Client did in Prisma ORM 7. You reach this model as db.orm.public.User, where public is the name of the PostgreSQL schema that holds your tables. await db.close() closes the database connections, and without it the script keeps running after the last query. create takes the fields directly, with no data wrapper, and .all() does what findMany() did.

Run it:

bunx tsx script.ts
Created: { email: 'alice@example.com', id: 1, name: 'Alice' }
All users: [ { email: 'alice@example.com', id: 1, name: 'Alice' } ]

In your app, import db from src/prisma/db.ts the same way. Reading data and Writing data show the other queries.

7. Change the model and apply the change

Add a field to the model:

src/prisma/contract.prisma
// use prisma-8

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  phone String?
}

Generate the files again, then plan a migration. migration plan compares the new contract with the last one you applied to your development database, and writes the difference into a new directory under migrations/app/. --name sets the end of the directory's name, after the date and time.

bunx prisma contract emit
bunx prisma migration plan --name add_user_phone
│  contract:    src/prisma/contract.json
│  migrations:  migrations/app
│  name:        add_user_phone

✔ Planned baseline (3 operation(s)) + 1 operation(s)

migrations/app/20260929T2120_baseline
├─ Create schema "public"
├─ Create table "User"
└─ Add unique constraint on "User" (email)
migrations/app/20260929T2121_add_user_phone
└─ Add column "phone" to "User"

from:       b5ae4e66fad1f0b7dd5590568f75a12a6f4576e9f367a787a69a4e729abac1fb
to:         300832575ace8feebb3d3442d1bca52150c46628de3e4c6c32a61cb0ab21100f
baseline:   migrations/app/20260929T2120_baseline
app space:  migrations/app/20260929T2121_add_user_phone

The output continues with a preview of the SQL. The first plan in a project writes two directories. The baseline holds the steps that create the table, which db init already ran on your development database. The second directory is your change.

Apply the migration:

bunx prisma db migrate --advance-ref db
✔ Running migration plan across spaces
│  migrations:  migrations
│  database:    postgres://****@127.0.0.1:54329/app1

✔ Applied 1 migration(s) (1 operation(s)) across 1 contract space(s)

App space
├─ Add column "phone" to "User"
└─ marker 300832575ace8feebb3d3442d1bca52150c46628de3e4c6c32a61cb0ab21100f

✔ Advanced ref "db" → 300832575ace8feebb3d3442d1bca52150c46628de3e4c6c32a61cb0ab21100f

db migrate runs only the migrations that the database does not have yet, so here it runs only your change. --advance-ref db writes the new hash to the db ref, so that the next migration plan contains only your next change. Pass it every time you apply a migration to your development database. Commit the migrations/ directory again.

To apply the migrations to any other database, such as production, pass its connection string. Leave out --advance-ref, because the db ref describes your development database:

bunx prisma db migrate --db "$PRODUCTION_DATABASE_URL"

db migrate runs the migrations that this database does not have yet. An empty database has no marker, so there db migrate runs the baseline and then your change.

Every later change follows the same four steps, which take the place of prisma migrate dev: edit src/prisma/contract.prisma, run npx prisma contract emit, run npx prisma migration plan --name <name>, and run npx prisma db migrate --advance-ref db.

Next steps

On this page