Add Prisma ORM to an existing PostgreSQL database
Add Prisma ORM to an app whose PostgreSQL database already has tables.
This page uses PostgreSQL. Use MongoDB instead.
On this page you add Prisma ORM to an app whose PostgreSQL database already has tables. You run orm init, generate the contract from your tables, run two queries, and apply your first change to the tables. The contract is the file that holds your models. In Prisma ORM 7, it was schema.prisma.
Your app must reach its PostgreSQL database and run on Node.js 22.18 or newer on the 22 line, 24.11 or newer on the 24 line, or 26 or newer. Work against a development copy of your database, not production. Step 9 shows how to apply your changes to production afterwards.
If your database has no tables yet, follow Add Prisma ORM and PostgreSQL to an existing app instead, which creates the tables for you. If you want Prisma ORM to create a new app for you, follow Create a new app with PostgreSQL. If your app uses Prisma ORM 7 today, follow Prisma ORM 7 to 8 (PostgreSQL).
Prisma ORM 8 is a release candidate. Prisma ORM 7 remains fully supported; its docs live at /orm/v7 and its setup paths at /v7/getting-started.
For what release candidate means, when the final release is expected, and how to stay on version 7, see Release status. For the Prisma ORM 8 name of every Prisma ORM 7 API, see Coming from Prisma ORM 7.
1. Install tsx
The scripts on this page run with tsx. If your project does not have it, install it:
bun add --dev tsx typescript2. Initialize Prisma ORM
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-envprisma@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 Prisma ORM packages and writes its files. You use three of them on this page:
src/prisma/contract.prisma, an example contract. Step 4 replaces it with a contract generated from your tables.src/prisma/db.ts, the file your code imports to run queries..env, which holds the connection string.
The command also writes prisma-8.md, a short reference for writing queries. You do not need it for this page.
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.
3. Set your database connection string
If you had no .env, orm init wrote one with a placeholder, DATABASE_URL="postgresql://user:password@localhost:5432/mydb". Set DATABASE_URL in .env to the connection string of the development copy of your database:
DATABASE_URL="postgres://username:password@host:5432/database?sslmode=require"orm init wrote src/prisma/db.ts, and you do not need to change it:
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 5.
4. Generate the contract from your tables
contract infer reads the tables in your database and writes a contract that describes them. It does what prisma db pull did in Prisma ORM 7. Run:
bunx prisma contract infer --output ./src/prisma/contract.prisma✔ Connecting to database...
✔ Introspecting database schema...
Overwriting existing file: src/prisma/contract.prisma
│ database: postgres://****@127.0.0.1:54329/legacy
✔ Contract written to src/prisma/contract.prismaThe command replaces the example contract from step 2. For a database with a user table and a post table, the new contract looks like this:
// use prisma-8
// Contract inferred from the live database schema. Edit as needed, then run `prisma contract emit`.
namespace public {
model User {
id Int @id(map: "user_pkey") @default(autoincrement())
email String @unique(map: "user_email_key")
name String?
role String @default("member")
age Int?
createdAt TimestamptzString @default(now()) @map("created_at")
posts Post[]
@@check(expression: "(age >= 0)", map: "user_age_check")
@@map("user")
}
model Post {
id Int @id(map: "post_pkey") @default(autoincrement())
title VarChar(200)
published Boolean @default(false)
authorId Int @map("author_id")
author User @relation(fields: [authorId], references: [id], onDelete: Cascade, map: "post_author_id_fkey")
@@index([authorId], map: "post_author_id_idx")
@@index([title], map: "post_published_idx", where: "published")
@@index(expression: "lower(title::text)", map: "post_title_lower_idx")
@@rls
@@map("post")
}
policy_select post_read {
target = Post
roles = [public]
using = "published"
@@map("post_read")
}
}Keep the first line, because contract emit reads only .prisma files that start with it. namespace public holds the models of the tables in the PostgreSQL schema public. Where Prisma ORM 7 wrote DateTime @db.Timestamptz, the contract writes TimestamptzString, and String @db.VarChar(200) becomes VarChar(200). In the policy block, roles = [public] is the PostgreSQL role PUBLIC, not the schema.
contract infer reads these parts of the database and writes each one into the contract:
| In the database | In the contract above |
|---|---|
| Primary keys and unique constraints | @id, @unique |
| Column defaults | @default("member"), @default(now()) |
| Foreign keys, with what happens on delete | @relation(..., onDelete: Cascade) and the posts field |
| Check constraints | @@check |
| Indexes, including an index on part of a table and an index on an expression | the three @@index lines |
| Row-level security and its policies | @@rls and the policy_select block |
Review the file before you go on. You can rename models, and you can remove the models of tables you do not want to use yet. A model uses the table with exactly the model's name, unless the model has @@map. So for the table user, contract infer wrote the model User with @@map("user"). Keep the @@map lines when you rename a model, so that the model still reads the same table.
A table without a model stays in your database, and later migrations leave it alone. Do not remove the model of a table that has row-level security policies, though. If you do, db sign in step 6 fails, because it finds policies that the contract does not declare.
Date and time columns
contract infer gives a timestamptz column the type TimestamptzString, a timestamp column TimestampString, a date column DateString, and a time column TimeString. Prisma ORM returns the values of these columns as the text PostgreSQL prints, such as 2026-09-30 07:36:38.60182+02, so your app needs no Temporal.
To get Temporal objects instead, the new date and time API of JavaScript, change the type of each such field in the contract: TimestamptzString becomes Timestamptz, TimestampString becomes Timestamp, DateString becomes Date, and TimeString becomes Time. The column in the database stays as it is. Node.js 26 has Temporal built in. Node.js 24 and older do not, and there the first query that reads such a column fails with RUNTIME.TEMPORAL_UNAVAILABLE. On Node.js 24 or older, install the temporal-polyfill package:
bun add temporal-polyfillThen add this line at the top of src/prisma/db.ts:
import "temporal-polyfill/global";Tables in other PostgreSQL schemas
contract infer reads only the PostgreSQL schema named public. It skips tables in any other schema and prints no message about them.
To use a table from another schema, write its model by hand inside a namespace block with the name of that schema. For a table event in the schema audit, add this to the end of the contract:
namespace audit {
model Event {
id Int @id @default(autoincrement())
message String
@@map("event")
}
}db sign in step 6 checks this table like the tables in public. Your code reaches the model as db.orm.audit.Event.
5. Generate the files your code imports
contract emit takes the place of prisma generate, and you run it after every change to the contract:
bunx prisma contract emitThe command writes src/prisma/contract.json and src/prisma/contract.d.ts, and db.ts imports both of them. Commit them, because your app cannot run without them.
If the contract has a policy, a check, or an index that contains SQL text, contract emit prints one warning for each, and still writes both files. Each warning starts like this:
(node:15169) [PN_EXACT_NAME_BODY_COMPARISON] Warning: check "user_age_check" uses map: with a SQL body.You can ignore these warnings on a contract that contract infer wrote. They are about SQL text that you write by hand, which Prisma ORM compares character by character with the text that PostgreSQL reports.
6. Sign the database
db sign checks that your database has what the contract describes. Then it records which contract the database matches. migration plan in step 9 needs that record to start from the tables you already have, instead of from an empty database.
bunx prisma db sign✔ Connecting to database...
✔ Verifying database schema...
✔ Signing database...
│ contract: src/prisma/contract.json
│ database: postgres://****@127.0.0.1:54329/legacy
✔ Database signed
from: none
to: 596586f61799d5b2585df874a00f8d6f12d73a522d2eefbcbec62dba4e9904a4
✔ Advanced ref "db" → 596586f61799d5b2585df874a00f8d6f12d73a522d2eefbcbec62dba4e9904a4A table or a column that the database has and the contract does not declare is not a problem. When something the contract declares is missing from the database, db sign writes nothing and exits with code 4. For example, with a phone field in the User model and no phone column in the table:
✘ Schema issues
└─ ✘ missing: database/public/user/column:phone
✘ [CONTRACT.SCHEMA_VERIFICATION_FAILED] Database schema does not satisfy contract (1 failure)
why: The live schema differs: missing: database/public/user/column:phone.To fix it, remove from the contract what the database does not have, run npx prisma contract emit, and sign again.
When the check passes, db sign stores a record in the database and writes files into your project:
- A record in the database of which contract it matches, in the table
prisma_contract.marker. The record holds the hash of the contract, which is the long text afterto:in the output. - A copy of the contract, under
migrations/snapshots/in your project. - The
dbref, which is the filemigrations/app/refs/db.json. It holds the same hash. The lineAdvanced ref "db"in the output means that the command wrote this file.
The next migration plan reads the copy and the db ref, so it starts from the tables you already have. Commit the migrations/ directory.
Until you plan your first migration, you can add models for tables that are already in the database. Run npx prisma contract emit and then npx prisma db sign again, which checks the database against the new contract and replaces the record.
7. Query a model with db.orm
db.orm queries your models, the way Prisma Client did in Prisma ORM 7. Create script.ts with the code below, which reads the User model from step 4. Use one of your own models and its fields in its place:
import { db } from "./src/prisma/db";
async function main() {
const users = await db.orm.public.User
.select("id", "email", "name")
.limit(2)
.all();
console.log(users);
await db.close();
}
main().catch((error) => {
console.error(error);
process.exit(1);
});Run it:
bunx tsx script.ts[
{ id: 1, email: 'alice@example.com', name: 'Alice' },
{ id: 2, email: 'bob@example.com', name: 'Bob' }
]8. Query a table with db.sql
db.sql builds SQL queries: where db.orm names models and fields, db.sql names tables and columns as they are in the database. This example reads the table user, which the User model maps with @@map("user"). .build() returns the query without running it, and db.runtime().query() runs it.
Replace script.ts with this version:
import { db } from "./src/prisma/db";
async function main() {
const query = db.sql.public.user
.select("id", "email", "name")
.limit(2)
.build();
const rows = await db.runtime().query(query);
console.log(rows);
await db.close();
}
main().catch((error) => {
console.error(error);
process.exit(1);
});Run it again:
bunx tsx script.ts[
{ id: 1, email: 'alice@example.com', name: 'Alice' },
{ id: 2, email: 'bob@example.com', name: 'Bob' }
]9. Make your first change to the tables
From here on, you change the tables by changing the contract. Add a field to a model in src/prisma/contract.prisma. This example adds phone to the User model from step 4, so use a model and a field of your own:
model User {
id Int @id(map: "user_pkey") @default(autoincrement())
email String @unique(map: "user_email_key")
name String?
phone String?Generate the files again, then plan a migration. migration plan compares the new contract with the last one you signed or applied, 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 (10 operation(s)) + 1 operation(s)
migrations/app/20260929T2130_baseline
├─ Create schema "public"
├─ Create table "post"
├─ Create table "user"
├─ Add unique constraint on "user" (email)
├─ Create index "post_author_id_idx" on "post"
├─ Create index "post_published_idx" on "post"
├─ Create index "post_title_lower_idx" on "post"
├─ Add foreign key "post_author_id_fkey" on "post"
├─ Enable row-level security on "post"
└─ Create RLS policy "post_read" on "post"
migrations/app/20260929T2131_add_user_phone
└─ Add column "phone" to "user"
from: 596586f61799d5b2585df874a00f8d6f12d73a522d2eefbcbec62dba4e9904a4
to: 4ef74b97c298402786f23bf299e17baba815e8d521bc90aaab59ab7de747c969
baseline: migrations/app/20260929T2130_baseline
app space: migrations/app/20260929T2131_add_user_phoneThe output continues with a preview of the SQL. The first plan writes two directories. The baseline describes the tables you had when you ran db sign. It lists Create table operations, but db migrate does not run them on a database that already has those tables. The second directory is your change. Commit both directories. The automatic baseline explains when migration plan writes a baseline.
You can ignore the word space in the output, because all your migrations are in migrations/app/.
Apply the migration:
bunx prisma db migrate --advance-ref db✔ Running migration plan across spaces
│ migrations: migrations
│ database: postgres://****@127.0.0.1:54329/legacy
✔ Applied 1 migration(s) (1 operation(s)) across 1 contract space(s)
App space
├─ Add column "phone" to "user"
└─ marker 4ef74b97c298402786f23bf299e17baba815e8d521bc90aaab59ab7de747c969
✔ Advanced ref "db" → 4ef74b97c298402786f23bf299e17baba815e8d521bc90aaab59ab7de747c969db migrate runs only the migrations that the database does not have yet. Here it adds the phone column, and the rows in your tables stay as they are. marker in the output is the record from db sign, which now holds the new hash. Pass --advance-ref db every time you apply a migration to your development database, so that the next migration plan contains only your next change.
To apply the migration to another database with the same tables, such as production, pass its connection string. Leave out --advance-ref there, because the db ref tracks your development database:
bunx prisma db migrate --db "$PRODUCTION_DATABASE_URL"You do not need to run db sign on that database. db migrate finds that the tables of the baseline are already there, leaves them and their rows as they are, and then adds the phone column.
Next steps
Every later change follows the same four steps: 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.
- Generating a migration for what a migration directory contains and how to edit it.
db updateto apply a contract change to a development database without writing migration files.- Coming from Prisma ORM 7 for the Prisma ORM 8 name of each Prisma ORM 7 call.
- Prisma Compute to host the app. The Prisma CLI deploys apps to Prisma Compute with Prisma Composer, which is in Early Access. Prisma Compute does not currently support WebSocket servers and has no multi-region deployments (limitations).
