Prisma ORM 8 is here.Read the docs
Deployment

pnpm workspaces

Set up Prisma 8 in a shared database package inside a pnpm workspaces monorepo and query it from a Next.js app.

Introduction

This guide shows you how to set up Prisma 8 in its own package inside a pnpm workspaces monorepo. The database package owns the contract, the emitted types, and the client. A Next.js app in the same workspace imports that client and renders users from the database.

Every command and output below was run end to end with pnpm 12 against a PostgreSQL database.

Prerequisites

  • Node.js 24 or later
  • pnpm 10 or later (this guide uses pnpm 12)
  • A PostgreSQL connection string, or nothing at all: npx create-db@latest can create a Prisma Postgres database for you

Use with your agent

To delegate this guide to your coding agent, copy the prompt below and hand it over:

Use with your agent
Use with your agent
Set up a pnpm workspaces monorepo with a shared Prisma 8 database package and a Next.js app that renders users from it.

1. Create `my-monorepo` with `pnpm init`, a `pnpm-workspace.yaml` listing `apps/*` and `packages/*` with `allowBuilds` for `esbuild`, `msgpackr-extract`, and `workerd`, and the directories `apps` and `packages/database`.
2. In `packages/database`, run `pnpm init`, then `npx prisma@latest orm init --yes --target postgres --authoring psl`. Its final "Emit the contract" step fails under pnpm; fix it with `pnpm add -D prisma@latest @prisma/cli-engine@latest`, then run `pnpm prisma contract emit`. Then run `pnpm prisma init` in the same directory so the Prisma agent skills are installed, and use them.
3. Write `packages/database/.env` with `DATABASE_URL` (use the connection string I give you, or create a Prisma Postgres database with `npx create-db@latest` and show me the claim URL it prints). Run `pnpm prisma db init` in `packages/database`.
4. Add `src/index.ts` exporting `db` from `./prisma/db`, set `"exports": { ".": "./src/index.ts" }` in the package's package.json, add a `src/seed.ts` that upserts two users with `db.orm.public.User.upsert(...)` and closes with `await db.runtime().close()`, and run it with `node src/seed.ts`.
5. In `apps`, run `pnpm create next-app@latest web --yes --skip-install`, delete `apps/web/.git` and `apps/web/pnpm-workspace.yaml`, add `"database": "workspace:*"` to `apps/web/package.json`, copy `packages/database/.env` to `apps/web/.env`, and run `pnpm install` from the workspace root.
6. Replace `apps/web/app/page.tsx` with a server component that imports `{ db } from "database"`, exports `dynamic = "force-dynamic"`, queries `db.orm.public.User.select("id", "email", "name").all()`, and renders the list.
7. Add root scripts `dev`, `build`, `start`, `db:init`, `db:update`, and `seed` that filter to the right package, start `pnpm dev` in the background, verify http://localhost:3000 renders the seeded users, then stop it. Finally run `pnpm build` and confirm it completes.

1. Create the workspace

Create the monorepo directory and initialize it:

mkdir my-monorepo
cd my-monorepo
pnpm init

pnpm 12 writes a root package.json with "type": "module" and a pinned packageManager field. Next, create pnpm-workspace.yaml:

pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

allowBuilds:
  esbuild: true
  msgpackr-extract: true
  workerd: true

The allowBuilds block matters. pnpm does not run dependency install scripts unless you approve them, and pnpm 11 and later turn an unapproved script into an install error. The Prisma 8 CLI's toolchain pulls in three packages with install scripts, so approve them up front. The same key works on pnpm 10.26 and later; without it you get ERR_PNPM_IGNORED_BUILDS the first time orm init runs pnpm add.

Create the directories for apps and shared packages:

mkdir -p apps packages/database

2. Set up the shared database package

The database package holds the contract (your schema), the emitted contract.json and contract.d.ts, and the db client every app imports. Prisma 8 has no prisma generate step and no generated client directory: contract emit writes the two artifacts next to the contract, and the runtime reads them.

2.1. Initialize Prisma 8 in the package

cd packages/database
pnpm init
npx prisma@latest orm init --target postgres

Answer the prompts: choose PSL for the authoring style and keep the default schema path, src/prisma/contract.prisma. orm init detects pnpm from the workspace, adds the dependencies to this package, and writes the Prisma 8 files:

▸ pnpm add @prisma/orm-postgres dotenv
✔ pnpm add @prisma/orm-postgres dotenv
▸ pnpm add -D prisma@latest @types/node
✔ pnpm add -D prisma@latest @types/node
▸ pnpm add -D @prisma/cli-engine
✔ pnpm add -D @prisma/cli-engine
▸ Emit the contract
✘ Emit the contract
│  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
├─ tsconfig.json
├─ .gitignore
├─ .gitattributes
└─ package.json
✘ [CLI.INIT_EMIT_FAILED] Failed to emit contract

The scaffold is complete, but the final emit step fails in a pnpm workspace: pnpm links the @prisma/cli-engine that orm init adds last to a directory that does not exist, so the CLI cannot evaluate prisma.config.ts. Re-add both packages; this repairs the link:

pnpm add -D prisma@latest @prisma/cli-engine@latest

pnpm replaces both dev dependencies and relinks them.

Now emit the contract with the package's own CLI:

pnpm prisma contract emit
▸ Resolving contract source...
✔ Resolving contract source...
▸ Emitting contract...
✔ Emitting contract...
│  contract:  src/prisma/contract.json
│  types:     src/prisma/contract.d.ts
✔ Emitted contract.json and contract.d.ts

orm init wrote a starter contract with User and Post models in src/prisma/contract.prisma, and a client in src/prisma/db.ts:

packages/database/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']!,
});

There is no driver adapter and no engine to configure. The with { type: 'json' } import attribute is required by Node's ESM loader; pnpm's pnpm init already set "type": "module" on the package, which is what the generated files expect. If your package declares "type": "commonjs", orm init leaves it alone and prints a warning; change it to "module".

2.2. Connect the database

prisma.config.ts loads .env through dotenv/config and reads DATABASE_URL. Create .env in the package with your connection string, or the one npx create-db@latest prints:

packages/database/.env
DATABASE_URL="postgres://user:password@localhost:5432/mydb"

Apply the contract to the database and sign it:

pnpm prisma db init
✔ Initialising database across spaces
│  contract:  src/prisma/contract.json
│  database:  postgres://****@localhost:5432/mydb
✔ Applied 5 operation(s) across 1 contract space
App space
├─ Create table "post"
├─ Create table "user"
├─ Add unique constraint on "user" (email)
├─ Create index "post_authorId_idx_e47547ed" on "post"
├─ Add foreign key "post_authorId_fkey" on "post"
└─ marker 1e8412e162dbbe69f4bb3bf8d07f0280ae67eaab15c34dcf201e67468315428d
✔ Advanced ref "db" → 1e8412e162dbbe69f4bb3bf8d07f0280ae67eaab15c34dcf201e67468315428d

If db init stops with Connection terminated unexpectedly, a database you just created is still starting; wait a few seconds and run it again. The command is safe to repeat and reports Database already matches contract when there is nothing left to do.

2.3. Export the client and add a seed script

Create the package entry point that apps will import:

packages/database/src/index.ts
export { db } from "./prisma/db";

Add a seed script so the app has rows to render. Node.js 24 runs TypeScript directly, but it needs the .ts extension on relative imports, so this file names it:

packages/database/src/seed.ts
import { db } from "./prisma/db.ts";

const users = [
  { email: "alice@prisma.io", name: "Alice" },
  { email: "bob@prisma.io", name: "Bob" },
];

for (const user of users) {
  await db.orm.public.User.upsert({
    create: user,
    update: {},
    conflictOn: { email: user.email },
  });
}

console.log(await db.orm.public.User.select("id", "email", "name").all());

await db.runtime().close();

Point the package at the entry point and add scripts for the database steps. Replace the main field pnpm init wrote with an exports map, and drop the placeholder test script:

packages/database/package.json
{
  "name": "database",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    ".": "./src/index.ts"
  },
  "scripts": {
    "contract:emit": "prisma contract emit",
    "db:init": "prisma db init",
    "db:update": "prisma db update",
    "seed": "node src/seed.ts"
  }
}

Keep the dependencies and devDependencies that orm init added. Run the seed:

pnpm seed
$ node src/seed.ts
[
  { id: 1, email: 'alice@prisma.io', name: 'Alice' },
  { id: 2, email: 'bob@prisma.io', name: 'Bob' }
]

The script resolves @prisma/orm-postgres through pnpm's isolated node_modules without any hoisting configuration, because the package declares it as a direct dependency. That is the rule to keep in mind for the rest of the workspace: the database package depends on @prisma/orm-postgres, and apps depend on database.

3. Set up the Next.js app

3.1. Scaffold the app into the workspace

cd ../../apps
pnpm create next-app@latest web --yes --skip-install

--yes accepts the defaults (App Router, TypeScript, Tailwind CSS, no src/ directory). --skip-install keeps create-next-app from installing into a nested node_modules; the workspace root installs for every package. The scaffold still writes two files that belong to the root, so remove them:

rm -rf web/.git web/pnpm-workspace.yaml

Add the shared package as a dependency of the app:

apps/web/package.json
"dependencies": {
  "database": "workspace:*", 
  "next": "16.3.4",
  "react": "19.2.8",
  "react-dom": "19.2.8"
}

Next.js reads .env from the app directory, and db.ts reads it from the working directory of the dev server, which is the same place. Copy the file from the database package:

cp ../packages/database/.env web/.env

Install from the workspace root so pnpm links database into the app:

cd ..
pnpm install
Scope: all 3 workspace projects
Progress: resolved 8, reused 454, downloaded 0, added 8, done
Done in 5.8s using pnpm v12.3.4

3.2. Render users from the shared package

Replace apps/web/app/page.tsx with a server component that queries through the shared client:

apps/web/app/page.tsx
import { db } from "database";

export const dynamic = "force-dynamic";

export default async function Home() {
  const users = await db.orm.public.User.select("id", "email", "name").all();

  return (
    <main className="p-8">
      <h1 className="text-2xl font-semibold">Users</h1>
      {users.length === 0 ? (
        <p>No users in the database yet.</p>
      ) : (
        <ul>
          {users.map((user) => (
            <li key={user.id}>
              {user.name ?? "Anonymous"} ({user.email})
            </li>
          ))}
        </ul>
      )}
    </main>
  );
}

Model access is namespace-qualified on PostgreSQL: db.orm.public.User. force-dynamic makes Next.js query on each request instead of at build time, so pnpm build does not need a reachable database. Next.js compiles the package's TypeScript source through the exports map; no transpilePackages entry is needed.

3.3. Add root scripts

Add scripts to the root package.json that run each step in the right package. build re-emits the contract before the app builds, so the types the app compiles against always match contract.prisma:

package.json
"scripts": {
  "dev": "pnpm --filter web dev",
  "build": "pnpm --filter database contract:emit && pnpm --filter web build",
  "start": "pnpm --filter web start",
  "db:init": "pnpm --filter database db:init",
  "db:update": "pnpm --filter database db:update",
  "seed": "pnpm --filter database seed"
}

3.4. Run the app

From the workspace root:

pnpm dev
$ next dev
▲ Next.js 16.3.4 (Turbopack)
- Local:         http://localhost:3000
- Environments: .env
✓ Ready in 479ms

Open http://localhost

(set PORT to change it). The page lists Alice and Bob, rendered by a server component calling Prisma 8 through the database package.

4. Build for production

pnpm build
$ prisma contract emit
$ next build
▲ Next.js 16.3.4 (Turbopack)
✓ Compiled successfully in 1026ms
  Running TypeScript ...
  Finished TypeScript in 2.2s ...
✓ Generating static pages using 5 workers (3/3) in 519ms

Route (app)
┌ ƒ /
└ ○ /_not-found

The type check runs across the package boundary: next build type-checks packages/database/src/index.ts along with the app. Then serve the build:

pnpm start

The page renders the same users from the production server.

5. (Optional) Browse your data in Prisma Studio

Prisma Studio ships with the Prisma 7 CLI and connects to a Prisma 8 database through --url. Run it from the workspace root, which has no prisma.config.ts for the Prisma 7 CLI to trip over:

pnpm dlx prisma@7.10.0 studio --url "postgres://user:password@localhost:5432/mydb"
Prisma Studio is running at: http://localhost:51212

Open the URL Studio prints. The user and post tables appear under Tables, with the seeded rows ready to edit. See Studio with Prisma 8 for the migration history view.

Common gotchas

  • If pnpm prisma contract emit reports CLI.CONFIG_UNREADABLE with Cannot find module '@prisma/cli-engine' or No "exports" main defined, the @prisma/cli-engine link in packages/database/node_modules is dangling. pnpm install --force does not repair it; pnpm add -D prisma@latest @prisma/cli-engine@latest does.
  • Every Prisma command reads DATABASE_URL from packages/database/.env through prisma.config.ts, and the app reads apps/web/.env. Keep the two files in sync, or export the variable in your shell and drop both files.
  • Do not call db.runtime().close() in a page or route handler. The client is a module-level singleton whose connection pool is shared across requests; close it only in scripts that exit, like seed.ts.
  • After you change src/prisma/contract.prisma, run pnpm --filter database contract:emit so the app sees the new types, then pnpm db:update to apply the change. The root build script emits for you before every build.

Prompt your coding agent

Run pnpm prisma init once inside packages/database to install the Prisma 8 skills for your coding agent. It adds a postinstall script that keeps the skills matching your installed packages, and it installs them into .claude/skills, .cursor/skills, .agents/skills, and .devin/skills under the package. Prompts that map to this guide:

  • "Using the prisma-8 skill, add a Post list under each user on the home page with .include('posts')."
  • "Add a packages/database script that creates a post for a given user email."
  • "Add a role enum to the User model in contract.prisma, emit the contract, and update the database."

Next steps

You now have a pnpm workspace where one package owns the Prisma 8 contract and client, and a Next.js app that renders through it.

On this page