# Cloudflare Workers (/docs/guides/deployment/cloudflare-workers)

> For the complete Prisma documentation index, see [llms.txt](https://www.prisma.io/docs/llms.txt). A markdown version of any docs page is available by appending `.md` to its URL.

Add Prisma 8 to a Cloudflare Worker, query PostgreSQL from the fetch handler with the nodejs_compat flag, and deploy it with Wrangler.

Location: Guides > Deployment > Cloudflare Workers

## Introduction [#introduction]

In this guide, you add Prisma 8 to a Cloudflare Worker, create tables in a PostgreSQL database, insert and count rows from the Worker's `fetch` handler, and deploy the Worker with Wrangler.

Cloudflare Workers do not expose raw TCP sockets by default, which is why most PostgreSQL drivers cannot run in them. With the `nodejs_compat` compatibility flag enabled, the `pg` driver that `@prisma/orm-postgres` uses connects through Cloudflare's Node.js socket API, so Prisma 8 runs inside the Worker without a special build. This works with [Prisma Postgres](https://www.prisma.io/docs/postgres), which pools connections for you, with any PostgreSQL server reachable over TCP and TLS, and with [Hyperdrive](https://developers.cloudflare.com/hyperdrive/) in front of your own database.

Every command, code block, and output below was run with `wrangler dev` against a local PostgreSQL 17 database, and the same Worker was also run against a Prisma Postgres database over TLS.

> [!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/cloudflare-workers](https://www.prisma.io/docs/guides/v7/deployment/cloudflare-workers).

## Prerequisites [#prerequisites]

* [Node.js](https://nodejs.org) 24 or later
* A [Cloudflare account](https://dash.cloudflare.com/sign-up/workers-and-pages) for the deploy step
* A PostgreSQL connection string, or nothing at all: `npx create-db@latest` can create a [Prisma Postgres](https://www.prisma.io/docs/postgres) database for you

## Use with your agent [#use-with-your-agent]

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

```text
Add Prisma 8 to a new Cloudflare Worker, query PostgreSQL from it, and prepare it for deployment.

1. Scaffold: `npm create cloudflare@latest prisma-cloudflare-worker -- --type=hello-world --lang=ts --git --no-deploy --no-agents`. In `prisma-cloudflare-worker`, run `npx prisma@latest orm init --yes --target postgres --authoring psl`, then `npx prisma@latest init` so the Prisma agent skills are installed and stay current, and use them.
2. Add `"compatibility_flags": ["nodejs_compat"]` to `wrangler.jsonc`. Get a database connection string: use the one I give you, or create a Prisma Postgres database with `npx create-db@latest` and show me the claim URL it prints. Write it to `.env` as `DATABASE_URL`; both the Prisma CLI and `wrangler dev` read that file.
3. Run `npx prisma@latest db init` to create the tables from `src/prisma/contract.prisma`.
4. Replace `src/index.ts` following https://www.prisma.io/docs/guides/deployment/cloudflare-workers.md: create one `postgres<Contract>({ contractJson, url: env.DATABASE_URL })` client per request inside `fetch`, run the queries, and close it with `ctx.waitUntil(db.close())`. Do not import the module-level client from `src/prisma/db.ts` in the Worker; a connection shared across requests hangs the second request. Run `npx wrangler types` so `Env` includes `DATABASE_URL`.
5. Start `npm run dev` in the background, wait until it reports ready, verify `curl http://localhost:8787` twice returns a created user and a growing count, then stop the dev server.
6. Deploy: check `npx wrangler whoami`; if I am not logged in, stop and ask me to run `npx wrangler login`. Then run `npx wrangler secret put DATABASE_URL` with the production connection string and `npm run deploy`, and verify the live URL.
```

## 1. Create a Worker [#1-create-a-worker]

Scaffold a TypeScript "Hello World" Worker:

  

#### bun

```bash
bunx create-cloudflare prisma-cloudflare-worker --type hello-world --lang=ts --git --no-deploy --no-agents
```

#### pnpm

```bash
pnpm create cloudflare prisma-cloudflare-worker --type=hello-world --lang=ts --git --no-deploy --no-agents
```

#### yarn

```bash
yarn create cloudflare prisma-cloudflare-worker --type=hello-world --lang=ts --git --no-deploy --no-agents
```

#### npm

```bash
npm create cloudflare@latest prisma-cloudflare-worker -- --type=hello-world --lang=ts --git --no-deploy --no-agents
```

```text no-copy
╭ Create an application with Cloudflare Step 1 of 3
│
├ In which directory do you want to create your application?
│ dir ./prisma-cloudflare-worker
│
├ What would you like to start with?
│ category Hello World example
│
├ Which template would you like to use?
│ type Worker only
│
├ Which language do you want to use?
│ lang TypeScript
│
╰ Application configured

🎉  SUCCESS  Application created successfully!
```

The flags skip the interactive prompts. Drop them if you prefer to answer the questions yourself. Then enter the project:

```bash
cd prisma-cloudflare-worker
```

## 2. Add Prisma 8 [#2-add-prisma-8]

### 2.1. Initialize Prisma 8 in the project [#21-initialize-prisma-8-in-the-project]

Prisma 8 has no Prisma Client to generate, no driver adapter package, and no query engine binary. `orm init` adds everything the Worker needs:

  

#### bun

```bash
bunx prisma@latest orm init --yes --target postgres --authoring psl
```

#### pnpm

```bash
pnpm dlx prisma@latest orm init --yes --target postgres --authoring psl
```

#### yarn

```bash
yarn dlx prisma@latest orm init --yes --target postgres --authoring psl
```

#### npm

```bash
npx prisma@latest orm init --yes --target postgres --authoring psl
```

Without `--yes`, the command asks for the contract authoring style and the schema path instead; the values above are the defaults. It installs the packages and emits the contract:

```json no-copy
{"kind":"step-finished","step":"npm add @prisma/orm-postgres dotenv","outcome":"ok"}
{"kind":"step-finished","step":"npm add -D prisma@latest","outcome":"ok"}
{"kind":"step-finished","step":"npm add -D @prisma/cli-engine@0.3.0","outcome":"ok"}
{"kind":"step-finished","step":"Emit the contract","outcome":"ok"}
{"kind":"result","envelope":{"ok":true,"result":{"target":"postgres","authoring":"psl","schemaPath":"src/prisma/contract.prisma","filesWritten":["src/prisma/contract.prisma","prisma.config.ts","src/prisma/db.ts","prisma-8.md",".env.example","tsconfig.json",".gitignore",".gitattributes","package.json","README.md"]}}}
```

The important files:

* `src/prisma/contract.prisma`: your schema. It starts with a `User` and a `Post` model.
* `src/prisma/contract.json` and `src/prisma/contract.d.ts`: emitted from the contract. The Worker imports both; there is no `prisma generate` step.
* `prisma.config.ts`: tells the CLI where the contract is and reads `DATABASE_URL` from `.env`.
* `src/prisma/db.ts`: a module-level client for Node.js scripts. The Worker does not import it; step 3 explains why.

`orm init` also sets `"type": "module"` in `package.json` and adds a `contract:emit` script. Whenever you edit `src/prisma/contract.prisma`, run `npm run contract:emit` to refresh the emitted files.

### 2.2. Enable Node.js compatibility [#22-enable-nodejs-compatibility]

Add the `nodejs_compat` flag to `wrangler.jsonc`. It gives the `pg` driver inside `@prisma/orm-postgres` the `node:net` and `node:tls` modules it needs to open a PostgreSQL connection from the Worker:

```json title="wrangler.jsonc"
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "prisma-cloudflare-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-09-07",
  "compatibility_flags": ["nodejs_compat"], // [!code ++]
  "observability": {
    "enabled": true
  },
  "upload_source_maps": true
}
```

### 2.3. Set your database connection string [#23-set-your-database-connection-string]

Create a `.env` file with your PostgreSQL connection string. If you do not have a database, `npx create-db@latest` creates a Prisma Postgres database and prints a connection string and a claim URL you can open to keep it.

```bash title=".env"
DATABASE_URL="postgres://user:password@localhost:5432/mydb"
```

Both tools read this one file: the Prisma CLI through `prisma.config.ts`, and `wrangler dev` directly, which exposes the value to the Worker as `env.DATABASE_URL`.

### 2.4. Create the tables [#24-create-the-tables]

  

#### bun

```bash
bunx prisma@latest db init
```

#### pnpm

```bash
pnpm dlx prisma@latest db init
```

#### yarn

```bash
yarn dlx prisma@latest db init
```

#### npm

```bash
npx prisma@latest db init
```

```text no-copy
"summary": "Applied 5 operation(s) across 1 space(s), database signed"
```

`db init` creates the `user` and `post` tables from the contract and signs the database, which records that it matches the emitted contract. There is no `prisma migrate dev` in Prisma 8; use [`db update`](https://www.prisma.io/docs/cli/db-update) for later schema changes during development and [`migration plan`](https://www.prisma.io/docs/cli/migration-plan) when you want a checked-in migration.

## 3. Query the database from the Worker [#3-query-the-database-from-the-worker]

Replace `src/index.ts` with a handler that creates a user and counts all users:

```typescript title="src/index.ts"
import postgres from "@prisma/orm-postgres/runtime";
import type { Contract } from "./prisma/contract.d";
import contractJson from "./prisma/contract.json" with { type: "json" };

export default {
	async fetch(request, env, ctx): Promise<Response> {
		const path = new URL(request.url).pathname;
		if (path === "/favicon.ico") return new Response("Not found", { status: 404 });

		const db = postgres<Contract>({ contractJson, url: env.DATABASE_URL });
		try {
			const user = await db.orm.public.User.create({
				email: `user-${Math.ceil(Math.random() * 1000)}@prisma.io`,
				name: "Jon Doe",
			});
			const { total } = await db.orm.public.User.aggregate((a) => ({ total: a.count() }));

			return new Response(
				`Created new user: ${user.name} (${user.email}).\nNumber of users in the database: ${total}.\n`,
			);
		} finally {
			ctx.waitUntil(db.close());
		}
	},
} satisfies ExportedHandler<Env>;
```

Three things to notice:

* The client is created inside `fetch`, with the connection string from `env`, and closed after the response with `ctx.waitUntil(db.close())`. This is the opposite of a Node.js server, where you keep one client for the life of the process. Workers do not let a socket opened during one request be used by another, so a module-level client works for the first request and hangs the next one. That is why the Worker does not import `src/prisma/db.ts`.
* Models are namespace-qualified on PostgreSQL: `db.orm.public.User`. `.create(...)` returns the inserted row, database defaults included.
* `contract.json` is imported with `with { type: "json" }`. Wrangler bundles it into the Worker.

### 3.1. Generate the Env types [#31-generate-the-env-types]

`wrangler dev` reads `DATABASE_URL` from `.env`. Regenerate the Worker types so `Env` declares it:

  

#### bun

```bash
bunx wrangler types
```

#### pnpm

```bash
pnpm dlx wrangler types
```

#### yarn

```bash
yarn dlx wrangler types
```

#### npm

```bash
npx wrangler types
```

```text no-copy
✨ Types written to worker-configuration.d.ts
```

`worker-configuration.d.ts` now contains `DATABASE_URL: string` on `Env`, and `npx tsc --noEmit` passes.

### 3.2. Run the Worker locally [#32-run-the-worker-locally]

  

#### bun

```bash
bun run dev
```

#### pnpm

```bash
pnpm run dev
```

#### yarn

```bash
yarn dev
```

#### npm

```bash
npm run dev
```

```text no-copy
Using secrets defined in .env
Your Worker has access to the following bindings:
Binding                            Resource                  Mode
env.DATABASE_URL ("(hidden)")      Environment Variable      local
⎔ Starting local server...
[wrangler:info] Ready on http://localhost:8787
```

Open [http://localhost:8787](http://localhost:8787) or call it from another terminal. Each request creates a user:

```bash
curl http://localhost:8787
```

```text no-copy
Created new user: Jon Doe (user-241@prisma.io).
Number of users in the database: 16.
```

Call it again and the count goes up by one. The queries run inside `workerd`, the same runtime Cloudflare uses in production.

## 4. Deploy to Cloudflare [#4-deploy-to-cloudflare]

### 4.1. Store the connection string as a secret [#41-store-the-connection-string-as-a-secret]

Wrangler does not upload `.env`. Store the production connection string as a Worker secret instead. Log in first if you have not (`npx wrangler login` opens a browser), then run:

  

#### bun

```bash
bunx wrangler secret put DATABASE_URL
```

#### pnpm

```bash
pnpm dlx wrangler secret put DATABASE_URL
```

#### yarn

```bash
yarn dlx wrangler secret put DATABASE_URL
```

#### npm

```bash
npx wrangler secret put DATABASE_URL
```

Paste the connection string when prompted. In production, the database must accept TCP connections with TLS from Cloudflare's network. A Prisma Postgres direct connection string (`postgres://...@db.prisma.io:5432/postgres?sslmode=require`) works as is and pools connections on the database side, so a Worker creating one client per request stays within the connection limit.

### 4.2. Deploy the Worker [#42-deploy-the-worker]

  

#### bun

```bash
bun run deploy
```

#### pnpm

```bash
pnpm run deploy
```

#### yarn

```bash
yarn deploy
```

#### npm

```bash
npm run deploy
```

Wrangler bundles the Worker (about 1.4 MB, 300 KB gzipped, with Prisma 8 included) and prints the live URL, `https://prisma-cloudflare-worker.<your-subdomain>.workers.dev`. Open it: the Worker creates a user in your production database on every request, exactly as it did locally. The upload itself was not run while validating this guide; the bundle size comes from `wrangler deploy --dry-run`, and the production path (TLS to Prisma Postgres from inside `workerd`) was verified with `wrangler dev`.

### 4.3. Optional: connect through Hyperdrive [#43-optional-connect-through-hyperdrive]

If your database is not Prisma Postgres, [Hyperdrive](https://developers.cloudflare.com/hyperdrive/) keeps a warm connection pool close to your database so each request does not pay for a new TCP and TLS handshake. Create a Hyperdrive config from your connection string:

  

#### bun

```bash
bunx wrangler hyperdrive create prisma-cloudflare-worker --connection-string="postgres://user:password@host:5432/database"
```

#### pnpm

```bash
pnpm dlx wrangler hyperdrive create prisma-cloudflare-worker --connection-string="postgres://user:password@host:5432/database"
```

#### yarn

```bash
yarn dlx wrangler hyperdrive create prisma-cloudflare-worker --connection-string="postgres://user:password@host:5432/database"
```

#### npm

```bash
npx wrangler hyperdrive create prisma-cloudflare-worker --connection-string="postgres://user:password@host:5432/database"
```

Add the binding it prints to `wrangler.jsonc`. The `localConnectionString` is what `wrangler dev` uses instead of Hyperdrive. The `hyperdrive create` command was not run while validating this guide; the local binding below was:

```json title="wrangler.jsonc"
{
  "compatibility_flags": ["nodejs_compat"],
  "hyperdrive": [ // [!code ++]
    { // [!code ++]
      "binding": "HYPERDRIVE", // [!code ++]
      "id": "<id printed by hyperdrive create>", // [!code ++]
      "localConnectionString": "postgres://user:password@localhost:5432/mydb" // [!code ++]
    } // [!code ++]
  ] // [!code ++]
}
```

Run `npx wrangler types` again, then pass the Hyperdrive connection string to the client instead of `env.DATABASE_URL`:

```typescript title="src/index.ts"
const db = postgres<Contract>({ contractJson, url: env.HYPERDRIVE.connectionString });
```

Everything else in the handler stays the same. The `localConnectionString` must include a password, even for a local server that does not check one; otherwise `wrangler dev` refuses to start with `You must provide a password`.

## Common gotchas [#common-gotchas]

> [!WARNING]
> Do not share a Prisma 8 client across requests in a Worker. The module-level `db` in `src/prisma/db.ts` keeps a connection pool alive between requests, and the second request that reuses a pooled socket never completes. `wrangler dev` reports:
> 
> ```text no-copy
> ✘ [ERROR] Uncaught Error: The Workers runtime canceled this request because it detected that your Worker's code had hung and would never generate a response.
> ```
>
> Create the client inside `fetch` and close it with `ctx.waitUntil(db.close())`, as the handler above does.

> [!NOTE]
> `@prisma/orm-postgres/serverless` exports `postgresServerless`, a per-request client built for serverless runtimes. It opens one `pg` connection per `await db.connect({ url })` call and disposes it when the request scope ends (`await using`). It runs in Workers with `nodejs_compat`, but it exposes only the SQL builder (`db.sql`), not `db.orm`, so this guide uses the regular runtime client per request instead. Reach for it when you only need [SQL builder](https://www.prisma.io/docs/orm/fundamentals/advanced-queries) queries.

> [!NOTE]
> `wrangler dev` reads `.env` for local values, but `wrangler deploy` never uploads it. Production values come from `wrangler secret put` or from bindings such as Hyperdrive.

## Prompt your coding agent [#prompt-your-coding-agent]

Run [`npx prisma@latest init`](https://www.prisma.io/docs/cli/init) once to install the [Prisma 8 skills](https://www.prisma.io/docs/ai/tools/skills#available-skills-for-prisma-8) for your coding agent and keep them matching your installed packages. Prompts that map to this guide:

* "Using the prisma-8 skill, add a `GET /users` route to the Worker that returns all users as JSON."
* "Add a `POST /users` route that creates a user from the request body and returns 201."
* "Add a `published Boolean @default(false)` field to `Post` in `src/prisma/contract.prisma`, emit the contract, and update the database with `db update`."

## Next steps [#next-steps]

* [Learn the fundamentals](https://www.prisma.io/docs/orm/fundamentals/reading-data): filtering, sorting, pagination, and writes.
* Change the schema in `src/prisma/contract.prisma`, then run `npm run contract:emit` and [`npx prisma@latest db update`](https://www.prisma.io/docs/cli/db-update).
* Use [Hono](https://hono.dev/) for routing on Workers; the [Hono guide](https://www.prisma.io/docs/guides/frameworks/hono) shows the same per-route query pattern.
* [Cloudflare Workers documentation](https://developers.cloudflare.com/workers/) and the [Node.js compatibility reference](https://developers.cloudflare.com/workers/runtime-apis/nodejs/).
* [Read the Prisma 8 overview](https://www.prisma.io/docs/orm) for the concepts behind contracts and typed queries.

## Related pages

- [`Bun workspaces`](https://www.prisma.io/docs/guides/deployment/bun-workspaces): Set up Prisma 8 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.
- [`Docker`](https://www.prisma.io/docs/guides/deployment/docker): Build an Express app on Prisma 8, run PostgreSQL from Docker Compose, then run the app and the database together in containers.
- [`pnpm workspaces`](https://www.prisma.io/docs/guides/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.
- [`Turborepo`](https://www.prisma.io/docs/guides/deployment/turborepo): Share one Prisma 8 database package across the apps in a Turborepo monorepo, with contract emit and migrations wired into turbo tasks.