Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

pnpm workspaces (/docs/guides/deployment/pnpm-workspaces)

For the complete Prisma documentation index, see llms.txt. A markdown version of any docs page is available by appending .md to its URL.

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

Location: Guides > Deployment > pnpm workspaces

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.

[!NOTE] Using Prisma 7?

Prisma 8 is the current release of Prisma ORM. Prisma 7 remains fully supported; the Prisma 7 version of this guide is at /guides/v7/deployment/pnpm-workspaces.

  • Node.js 24 or later
  • pnpm 10.26 or later, but not 12.0 to 12.4.0, which have a defect that breaks step 2.1 (this guide uses pnpm 12.7.0)
  • A PostgreSQL connection string, or nothing at all: npx create-db@latest can create a Prisma Postgres database for you

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

text
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`. 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.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 `sharp: false` and `unrs-resolver: false` to `allowBuilds` in the root `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.

Create the monorepo directory and initialize it:

Bash
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. Since pnpm 11 an unapproved script fails the install (strictDepBuilds is on by default); pnpm 10 only warns and skips the script, which leaves the package half-installed, unless you set strictDepBuilds: true. The Prisma ORM 8 CLI's toolchain pulls in three packages with install scripts, so approve them up front. The allowBuilds key exists since pnpm 10.26, so use that version or later; without the key, the first pnpm add that orm init runs fails with ERR_PNPM_IGNORED_BUILDS on pnpm 11 (on pnpm 10 with the default settings it installs the three packages without running their scripts).

Create the directories for apps and shared packages:

Bash
mkdir -p apps packages/database

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.

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

Answer the prompts: choose PSL for the authoring style and keep the other defaults, including the 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:

no-copy
▸ 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@0.6.1
✔ pnpm add -D @prisma/cli-engine@0.6.1
▸ 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

On pnpm 12.0 to 12.4.0, orm init writes all the files and then fails at Emit the contract with CLI.INIT_EMIT_FAILED. If you hit it, apply the pnpm dedupe fix from Common gotchas and continue.

The @prisma/cli-engine version comes from the prisma package the previous step installed, so the two always match. orm init emits the contract itself, so src/prisma/contract.json and src/prisma/contract.d.ts exist as soon as it finishes. After every schema edit, re-emit with the package's own CLI:

Bash
pnpm prisma contract emit

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".

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:

Bash
pnpm prisma db init
no-copy
✔ 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 91e7f9f035806fa2789a4d726ef7724cad434fd6b00014d47ebf12d6e6bb784e
✔ Advanced ref "db" → 91e7f9f035806fa2789a4d726ef7724cad434fd6b00014d47ebf12d6e6bb784e

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

Section titled “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.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:

Bash
pnpm seed
no-copy
$ 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.

Bash
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:

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

The pnpm-workspace.yaml you removed told pnpm not to run the install scripts of two of the app's dependencies. Move that decision to the root file, or the pnpm install below fails with ERR_PNPM_IGNORED_BUILDS:

pnpm-workspace.yaml
packages:  - "apps/*"  - "packages/*"allowBuilds:  esbuild: true  msgpackr-extract: true  sharp: false  unrs-resolver: false  workerd: true

Add the shared package as a dependency of the app:

apps/web/package.json
"dependencies": {  "database": "workspace:*",  "next": "16.3.6",  "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:

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

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

Bash
cd ..
pnpm install
no-copy
Scope: all 3 workspace projects
Progress: resolved 700, reused 700, downloaded 0, added 700, done
Done in 1.9s using pnpm v12.7.0

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.

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"
}

From the workspace root:

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

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

Bash
pnpm build
no-copy
$ prisma contract emit
$ next build
▲ Next.js 16.3.6 (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:

Bash
pnpm start

The page renders the same users from the production server.

5. (Optional) Browse your data in Prisma Studio

Section titled “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:

Bash
pnpm dlx prisma@prev studio --url "postgres://user:password@localhost:5432/mydb"
no-copy
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.

[!WARNING] pnpm dlx prisma@latest stops at an interactive Choose which packages to build prompt, because dlx runs outside the workspace and ignores its allowBuilds. Either answer the prompt, or run npx prisma@latest from a package directory as this guide does. At the workspace root, npx itself fails with EBADDEVENGINES: pnpm 12's pnpm init writes a devEngines.packageManager field that npm enforces. Use the package's own CLI (pnpm prisma ...) or pnpm dlx there.

  • If orm init ends with CLI.INIT_EMIT_FAILED, or pnpm prisma contract emit reports CLI.CONFIG_UNREADABLE, and the message says Cannot find module '@prisma/cli-engine' or No "exports" main defined, the @prisma/cli-engine link in packages/database/node_modules points at a directory that does not exist. This is a defect in pnpm 12.0 to 12.4.0, and pnpm 12.4.1 or later fixes it. In a project that already hit it, run pnpm dedupe to rebuild the link, then pnpm prisma contract emit. You do not need to run orm init again, because it wrote all its files before it tried to emit. pnpm install, pnpm install --force, and adding the packages again do not repair the link.
  • 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.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.

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."

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.

  • Bun workspaces: Set up Prisma ORM in a Bun workspaces monorepo through a shared database package, seed it with Bun, and query it from a Next.js app in the same workspace.
  • Cloudflare Workers: Add Prisma ORM to a Cloudflare Worker, query PostgreSQL from the fetch handler with the nodejs_compat flag, and deploy it with Wrangler.
  • Docker: Build an Express app on Prisma ORM, run PostgreSQL from Docker Compose, then run the app and the database together in containers.
  • Turborepo: Share one Prisma 8 database package across the apps in a Turborepo monorepo, with contract emit and migrations wired into turbo tasks.
Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu