Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

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 .md to 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

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.

  • 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@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
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.

Create a directory for the monorepo and a root package.json that declares where the apps and shared packages live:

Bash
mkdir my-monorepo
cd my-monorepo
package.json
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"]
}

Create the directories for the app and the shared database package:

Bash
mkdir apps
mkdir -p packages/database

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.

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:

packages/database/package.json
{
  "name": "database",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "main": "index.ts"
}

Then add Prisma ORM to the package:

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

Choose 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 starter User and Post models
  • src/prisma/db.ts: the client the rest of the workspace imports
  • prisma.config.ts: loads .env and points the CLI at the contract and DATABASE_URL
  • .env.example, tsconfig.json, .gitignore, and prisma-8.md
  • a contract:emit script in package.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:

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

packages/database/.env
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:

packages/database/package.json
{  "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"  }}

Apply the schema to the database and sign it:

Bash
bun run db:init
no-copy
"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.

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:

packages/database/index.ts
export { db } from "./src/prisma/db";
export type { Contract } from "./src/prisma/contract.d";

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:

packages/database/src/prisma/seed.ts
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:

Bash
bun run db:seed
no-copy
$ bun src/prisma/seed.ts
Seeded 3 users.

The shared database package is now complete.

Go back to the workspace root and add scripts that fan out to the packages with bun run --filter:

Bash
cd ../..
package.json
{  "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:

Bash
bun run seed
no-copy
$ bun run --filter database db:seed
database db:seed: Seeded 3 users.
database db:seed: Exited with code 0

Create a Next.js app named web inside apps/:

Bash
cd apps
bun create next-app@latest web --yes
no-copy
Initializing 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:

Bash
cd web
rm -rf .git

Add the shared package as a workspace dependency:

apps/web/package.json
{  "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:

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

Bash
cd ../..
bun install
no-copy
bun 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:

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

Start the dev server from the workspace root:

Bash
bun run dev
no-copy
$ 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.0s

Open http://localhost:3000, or check it from another terminal:

Bash
curl -s http://localhost:3000 | grep -o '<h1>.*</ul>'
no-copy
<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:

Bash
bun run build
no-copy
$ 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 0

Schema changes happen in one place. Edit packages/database/src/prisma/contract.prisma, for example to add a field to User:

packages/database/src/prisma/contract.prisma
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:

Bash
bun run --filter database contract:emit
bun run db:update
no-copy
"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.

[!WARNING] If the app fails with CONTRACT.MARKER_READ_FAILED and Postgres driver not connected, DATABASE_URL is not set in the app's environment. The client is constructed in packages/database, but the environment it reads comes from the process that imports it. Make sure apps/web/.env exists, or export DATABASE_URL in the shell that runs bun run dev.

[!WARNING] db update refuses 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 during bun 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.

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.ts script that deletes all posts and users."
  • "Add a /users/[id] page in apps/web that loads one user with their posts from the shared database package."
  • "Add an apps/api Bun server to the workspace that exposes GET /users from the shared database package."

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.
  • 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.
Suggest an edit

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

Export
Documentation menu