Bun workspaces (/docs/guides/deployment/bun-workspaces)
For the complete Prisma documentation index, see llms.txt. A markdown version of any docs page is available by appending
.mdto its URL.
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.
Location: Guides > Deployment > Bun workspaces
Introduction
Section titled “Introduction”This guide shows you how to use Prisma ORM in a Bun workspaces monorepo. You create a shared database package that owns the schema, the database client, and a seed script, then import that client from a Next.js app in the same workspace. Every workspace member shares one lockfile and one bun install, so the database package is wired into the app like any other dependency.
Every command, file, and output below was run end to end against a live PostgreSQL database.
[!NOTE] Using Prisma ORM 7?
Prisma ORM 8 is the current release. Prisma ORM 7 remains fully supported; the Prisma ORM 7 version of this guide is at /guides/v7/deployment/bun-workspaces.
Prerequisites
Section titled “Prerequisites”- Bun 1.1 or later (
bun --version) - Node.js 24 or later: Bun runs the scripts, and Next.js runs on Node.js
- A PostgreSQL connection string, or nothing at all:
bunx create-db@latestcan create a Prisma Postgres database for you
Use with your agent
Section titled “Use with your agent”To delegate this guide to your coding agent, copy the prompt below and hand it over:
Create a Bun workspaces monorepo with a shared Prisma ORM database package and a Next.js app that queries it.
1. Create `my-monorepo` with a root `package.json` that sets `"workspaces": ["apps/*", "packages/*"]`, then create `apps/` and `packages/database/`. Give `packages/database` a `package.json` with `"name": "database"`, `"private": true`, `"type": "module"` and `"main": "index.ts"`.
2. In `packages/database`, run `bunx prisma@latest orm init --yes --target postgres --authoring psl`. Then run `bunx prisma@latest init` there so the Prisma agent skills are installed and stay current, and use them. Get a database connection string: use the one I give you, or create a Prisma Postgres database with `bunx create-db@latest` and show me the claim URL it prints. Write it as `DATABASE_URL` into `packages/database/.env`.
3. Add package scripts to `packages/database`: `db:init` (`prisma db init`), `db:update` (`prisma db update`), `db:seed` (`bun src/prisma/seed.ts`). Run `bun run db:init` there.
4. Create `packages/database/index.ts` that re-exports `db` from `./src/prisma/db`, and `packages/database/src/prisma/seed.ts` that upserts three users with `db.orm.public.User.upsert({ create, update: {}, conflictOn: { email } })` and ends with `await db.close()`, following https://www.prisma.io/docs/guides/deployment/bun-workspaces.md. Run `bun run db:seed`.
5. Add root scripts `dev`, `build`, `start`, `db:init`, `db:update` and `seed` that use `bun run --filter <package> <script>`.
6. In `apps/`, run `bun create next-app@latest web --yes`, delete `apps/web/.git`, add `"database": "workspace:*"` to its dependencies, copy `packages/database/.env` to `apps/web/.env`, and run `bun install` from the root.
7. Replace `apps/web/app/page.tsx` with a server component that exports `dynamic = "force-dynamic"` and lists users from `db.orm.public.User.select("id", "name", "email").all()`.
8. Start `bun run dev` from the root in the background, wait until it reports ready, verify `curl http://localhost:3000` returns the seeded users, then stop the dev server.
Use the installed Prisma ORM skills.1. Set up the workspace
Section titled “1. Set up the workspace”Create a directory for the monorepo and a root package.json that declares where the apps and shared packages live:
mkdir my-monorepo
cd my-monorepo{
"name": "my-monorepo",
"private": true,
"workspaces": ["apps/*", "packages/*"]
}Create the directories for the app and the shared database package:
mkdir apps
mkdir -p packages/database2. Set up the database package
Section titled “2. Set up the database package”The database package owns the schema, the emitted contract, the Prisma ORM client, and the seed script. Every app in the workspace imports the client from it.
2.1. Initialize Prisma ORM
Section titled “2.1. Initialize Prisma ORM”Give the package a package.json. "main": "index.ts" lets other workspace members import the TypeScript source directly, so there is no build step for the package:
{
"name": "database",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "index.ts"
}Then add Prisma ORM to the package:
cd packages/database
bunx prisma@latest orm init --target postgresChoose PSL as the authoring style and keep the default schema path. The command detects Bun as the package manager, installs @prisma/orm-postgres and dotenv plus the dev dependencies prisma, @types/node, and @prisma/cli-engine, and writes:
src/prisma/contract.prisma: the schema, with starterUserandPostmodelssrc/prisma/db.ts: the client the rest of the workspace importsprisma.config.ts: loads.envand points the CLI at the contract andDATABASE_URL.env.example,tsconfig.json,.gitignore, andprisma-8.md- a
contract:emitscript inpackage.json
It also runs the first contract emit, which compiles the schema into src/prisma/contract.json and src/prisma/contract.d.ts. Those two files are what your queries are type-checked against, so there is no prisma generate step and no generated client folder to export from the package.
Bun installs the workspace's dependencies into the root node_modules and writes a single bun.lock at the root, even though you ran the command inside packages/database.
The generated client reads DATABASE_URL from the environment and loads .env first:
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 runtime talks to PostgreSQL directly.
2.2. Set the connection string and add scripts
Section titled “2.2. Set the connection string and add scripts”Create .env in the database package. Use your own PostgreSQL connection string, or create a Prisma Postgres database with bunx create-db@latest; it prints a connection string and a claim URL you can open to keep the database.
DATABASE_URL="postgres://user:password@localhost:5432/mydb"Add the database scripts next to the contract:emit script that orm init created. The seed script runs with bun, which executes TypeScript directly:
{ "scripts": { "contract:emit": "prisma contract emit", "db:init": "prisma db init", "db:update": "prisma db update", "db:verify": "prisma db verify", "db:seed": "bun src/prisma/seed.ts" }}2.3. Initialize the database
Section titled “2.3. Initialize the database”Apply the schema to the database and sign it:
bun run db:init"summary": "Applied 5 operation(s) across 1 space(s), database signed"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 Applied 0 operation(s) when there is nothing left to do.
2.4. Export the client
Section titled “2.4. Export the client”Create index.ts at the package root. It re-exports the client and the Contract type so apps import from database and never reach into the package's internals:
export { db } from "./src/prisma/db";
export type { Contract } from "./src/prisma/contract.d";2.5. Seed the database
Section titled “2.5. Seed the database”Create the seed script. upsert with conflictOn makes it safe to run more than once, and await db.close() at the end lets the process exit instead of waiting on the connection pool:
import { db } from "./db";
const users = [
{ email: "alice@example.com", name: "Alice" },
{ email: "bob@example.com", name: "Bob" },
{ email: "charlie@example.com", name: "Charlie" },
];
for (const user of users) {
await db.orm.public.User.upsert({
create: user,
update: {},
conflictOn: { email: user.email },
});
}
console.log(`Seeded ${users.length} users.`);
await db.close();Model access is namespace-qualified on PostgreSQL: db.orm.public.User, where public is the default schema.
Run it:
bun run db:seed$ bun src/prisma/seed.ts
Seeded 3 users.The shared database package is now complete.
3. Add root scripts
Section titled “3. Add root scripts”Go back to the workspace root and add scripts that fan out to the packages with bun run --filter:
cd ../..{ "name": "my-monorepo", "private": true, "workspaces": ["apps/*", "packages/*"], "scripts": { "dev": "bun run --filter web dev", "build": "bun run --filter database contract:emit && bun run --filter web build", "start": "bun run --filter web start", "db:init": "bun run --filter database db:init", "db:update": "bun run --filter database db:update", "seed": "bun run --filter database db:seed" }}build emits the contract before building the app, so a schema edit is never stale in a production build. There is no generate step to run before dev: the emitted contract.json and contract.d.ts are plain files that the app imports.
Check that the seed works from the root:
bun run seed$ bun run --filter database db:seed
database db:seed: Seeded 3 users.
database db:seed: Exited with code 04. Set up the Next.js app
Section titled “4. Set up the Next.js app”4.1. Create the app
Section titled “4.1. Create the app”Create a Next.js app named web inside apps/:
cd apps
bun create next-app@latest web --yesInitializing project with template: app-tw
Installing dependencies:
- next
- react
- react-dom
...
Success! Created web at /path/to/my-monorepo/apps/web--yes accepts the defaults (App Router, TypeScript, Tailwind, no src/ directory) and uses Bun as the package manager because you ran it with bun create. Bun recognizes that apps/web is inside a workspace and records its dependencies in the root bun.lock.
The scaffold also initializes a Git repository inside apps/web. Remove it, because the monorepo root should be the only repository:
cd web
rm -rf .git4.2. Add the database package
Section titled “4.2. Add the database package”Add the shared package as a workspace dependency:
{ "dependencies": { "database": "workspace:*", "next": "16.3.4", "react": "19.2.8", "react-dom": "19.2.8" }}The client in the database package reads DATABASE_URL from .env in the current working directory, and Next.js loads .env from the app directory. Copy the file so both find it:
cp ../../packages/database/.env .The Next.js .gitignore already excludes .env*, so the copy stays out of Git.
Then link the package by installing from the root:
cd ../..
bun installbun install v1.3.3 (274e01c7)
Saved lockfile
Checked 478 installs across 616 packages (no changes) [440.00ms]apps/web/node_modules/database is now a symlink to packages/database.
4.3. Query the database from a server component
Section titled “4.3. Query the database from a server component”Replace apps/web/app/page.tsx with a server component that lists the users. dynamic = "force-dynamic" makes the page query the database on every request instead of once at build time:
import { db } from "database";
export const dynamic = "force-dynamic";
export default async function Home() {
const users = await db.orm.public.User.select("id", "name", "email").all();
return (
<main>
<h1>Users</h1>
{users.length === 0 && <p>No users have been added to the database yet.</p>}
<ul>
{users.map((user) => (
<li key={user.id}>{`${user.name} (${user.email})`}</li>
))}
</ul>
</main>
);
}users is typed from the contract, so user.name and user.email autocomplete in the app even though the schema lives in another package. Turbopack compiles the package's TypeScript source through the workspace symlink; no transpilePackages setting is needed.
4.4. Run the app
Section titled “4.4. Run the app”Start the dev server from the workspace root:
bun run dev$ bun run --filter web dev
web dev: ▲ Next.js 16.3.4 (Turbopack)
web dev: - Local: http://localhost:3000
web dev: - Network: http://192.168.1.16:3000
web dev: - Environments: .env
web dev: ✓ Ready in 4.0sOpen http://localhost:3000, or check it from another terminal:
curl -s http://localhost:3000 | grep -o '<h1>.*</ul>'<h1>Users</h1><ul><li>Alice (alice@example.com)</li><li>Bob (bob@example.com)</li><li>Charlie (charlie@example.com)</li></ul>The production build works the same way. bun run build emits the contract and then runs next build; bun run start serves the result:
bun run build$ bun run --filter database contract:emit && bun run --filter web build
database contract:emit: Exited with code 0
web build: ▲ Next.js 16.3.4 (Turbopack)
web build: Creating an optimized production build ...
web build: ✓ Compiled successfully in 26.2s
web build: Running TypeScript ...
web build: Finished TypeScript in 15.8s ...
web build:
web build: Route (app)
web build: ┌ ƒ /
web build: └ ○ /_not-found
web build:
web build: ƒ (Dynamic) server-rendered on demand
web build: Exited with code 05. Change the schema
Section titled “5. Change the schema”Schema changes happen in one place. Edit packages/database/src/prisma/contract.prisma, for example to add a field to User:
model User { id Int @id @default(autoincrement()) email String @unique username String? name String? bio String? posts Post[] createdAt TimestamptzString @default(now()) updatedAt temporal.updatedAtString()}Then, from the root, emit the contract and update the database:
bun run --filter database contract:emit
bun run db:update"summary": "Applied 1 operation(s) across 1 space(s), signature updated"The Next.js app picks up the new field on its next request because it imports the package's emitted contract.d.ts directly. For a checked-in migration instead of a direct update, use migration plan and db migrate.
Common gotchas
Section titled “Common gotchas”[!WARNING] If the app fails with
CONTRACT.MARKER_READ_FAILEDandPostgres driver not connected,DATABASE_URLis not set in the app's environment. The client is constructed inpackages/database, but the environment it reads comes from the process that imports it. Make sureapps/web/.envexists, or exportDATABASE_URLin the shell that runsbun run dev.
[!WARNING]
db updaterefuses to apply an operation that destroys data, such as dropping a column, without consent. Interactively it asks you to type the database name; in a script or CI job, pass--confirm <database name>.
[!NOTE] Without
export const dynamic = "force-dynamic", Next.js prerenders the page at build time and the users list is frozen at whatever the database held duringbun run build.
Do not call db.close() in a server component or route handler. The client in packages/database is a module-level singleton whose connection pool is shared across requests; close it only in short-lived scripts such as the seed.
Prompt your coding agent
Section titled “Prompt your coding agent”Run bunx prisma@latest init once inside packages/database to install the Prisma ORM skills for your coding agent and keep them matching your installed packages. It adds a postinstall script that re-syncs the skills after every bun install. Prompts that map to this guide:
- "Using the prisma-8 skill, add a
packages/database/src/prisma/reset.tsscript that deletes all posts and users." - "Add a
/users/[id]page inapps/webthat loads one user with their posts from the shareddatabasepackage." - "Add an
apps/apiBun server to the workspace that exposesGET /usersfrom the shareddatabasepackage."
Next steps
Section titled “Next steps”You now have a Bun workspaces monorepo with a shared Prisma ORM database package integrated into a Next.js app.
- To add task orchestration and caching on top of this setup, see the Turborepo guide.
- To serve the same package from a plain Bun server instead of Next.js, see the Bun guide.
- Learn the fundamentals: filtering, sorting, pagination, and writes.
- Read the Prisma ORM overview for the concepts behind contracts and typed queries.
Related pages
Section titled “Related pages”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.pnpm workspaces: Set up Prisma 8 in a shared database package inside a pnpm workspaces monorepo and query it from a Next.js app.Turborepo: Share one Prisma 8 database package across the apps in a Turborepo monorepo, with contract emit and migrations wired into turbo tasks.