Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

Cloudflare Workers

In this guide, you add Prisma ORM 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 ORM runs inside the Worker without a special build. This works with Prisma Postgres, which pools connections for you, with any PostgreSQL server reachable over TCP and TLS, and with 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.

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

with
Add Prisma ORM 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 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.

Scaffold a TypeScript "Hello World" Worker:

bunx create-cloudflare prisma-cloudflare-worker --type hello-world --lang=ts --git --no-deploy --no-agents
Bash
pnpm create cloudflare prisma-cloudflare-worker --type=hello-world --lang=ts --git --no-deploy --no-agents
Bash
yarn create cloudflare prisma-cloudflare-worker --type=hello-world --lang=ts --git --no-deploy --no-agents
Bash
npm create cloudflare@latest prisma-cloudflare-worker -- --type=hello-world --lang=ts --git --no-deploy --no-agents
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
╭ 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:

cd prisma-cloudflare-worker

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

bunx prisma@latest orm init --yes --target postgres --authoring psl
Bash
pnpm dlx prisma@latest orm init --yes --target postgres --authoring psl
Bash
yarn dlx prisma@latest orm init --yes --target postgres --authoring psl
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:

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

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:

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

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:

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

  "observability": {

    "enabled": true

  },

  "upload_source_maps": true

}

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.

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.

bunx prisma db init
Bash
pnpm prisma db init
Bash
yarn prisma db init
Bash
npx prisma db init
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 ORM; use db update for later schema changes during development and migration plan when you want a checked-in migration.

"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 ORM; use db update for later schema changes during development and migration plan when you want a checked-in migration.

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

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.

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

bunx wrangler types
Bash
pnpm dlx wrangler types
Bash
yarn dlx wrangler types
Bash
npx wrangler types
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.

✨ Types written to worker-configuration.d.ts

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

bun run dev
Bash
pnpm run dev
Bash
yarn dev
Bash
npm run dev
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 or call it from another terminal. Each request creates a user:

Bash
curl http://localhost:8787
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.

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 or call it from another terminal. Each request creates a user:

curl http://localhost:8787
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.

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:

bunx wrangler secret put DATABASE_URL
Bash
pnpm dlx wrangler secret put DATABASE_URL
Bash
yarn dlx wrangler secret put DATABASE_URL
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.

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.

bun run deploy
Bash
pnpm run deploy
Bash
yarn deploy
Bash
npm run deploy

Wrangler bundles the Worker (about 1.4 MB, 300 KB gzipped, with Prisma ORM 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.

Wrangler bundles the Worker (about 1.4 MB, 300 KB gzipped, with Prisma ORM 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.

If your database is not Prisma Postgres, 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:

bunx wrangler hyperdrive create prisma-cloudflare-worker --connection-string="postgres://user:password@host:5432/database"
Bash
pnpm dlx wrangler hyperdrive create prisma-cloudflare-worker --connection-string="postgres://user:password@host:5432/database"
Bash
yarn dlx wrangler hyperdrive create prisma-cloudflare-worker --connection-string="postgres://user:password@host:5432/database"
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:

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

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

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.

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:

title="wrangler.jsonc"
{

  "compatibility_flags": ["nodejs_compat"],

  "hyperdrive": [ 

    { 

      "binding": "HYPERDRIVE", 

      "id": "<id printed by hyperdrive create>", 

      "localConnectionString": "postgres://user:password@localhost:5432/mydb"

    } 

  ] 

}

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

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.

Run npx prisma@latest init once to install the Prisma ORM skills 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."
Suggest an edit

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

Export
Documentation menu