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.
Introduction
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.
Prisma ORM 8 is a release candidate. Prisma ORM 7 remains fully supported; the Prisma ORM 7 version of this guide is at /guides/v7/deployment/cloudflare-workers.
Prerequisites
- Node.js 22.18 or newer on the 22 line, 24.11 or newer on the 24 line, or 26 or newer (24 recommended)
- A Cloudflare account for the deploy step
- A PostgreSQL connection string, or nothing at all:
npx create-db@latestcan create a Prisma Postgres database for you
Use with your agent
To delegate this guide to your coding agent, copy the prompt below and hand it over:
1. Create a Worker
Scaffold a TypeScript "Hello World" Worker:
bunx create-cloudflare prisma-cloudflare-worker --type hello-world --lang=ts --git --no-deploy --no-agents╭ 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-worker2. Add Prisma ORM
2.1. Initialize Prisma ORM in the project
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 pslWithout --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.7.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 aUserand aPostmodel.src/prisma/contract.jsonandsrc/prisma/contract.d.ts: emitted from the contract. The Worker imports both; there is noprisma generatestep.prisma.config.ts: tells the CLI where the contract is and readsDATABASE_URLfrom.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
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:
{
"$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
}2.3. 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.
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
bunx prisma db init"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.
3. Query the database from the Worker
Replace src/index.ts with a handler that creates a user and counts all users:
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 fromenv, and closed after the response withctx.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 importsrc/prisma/db.ts. - Models are namespace-qualified on PostgreSQL:
db.orm.public.User..create(...)returns the inserted row, database defaults included. contract.jsonis imported withwith { type: "json" }. Wrangler bundles it into the Worker.
3.1. Generate the Env types
wrangler dev reads DATABASE_URL from .env. Regenerate the Worker types so Env declares it:
bunx wrangler types✨ Types written to worker-configuration.d.tsworker-configuration.d.ts now contains DATABASE_URL: string on Env, and npx tsc --noEmit passes.
3.2. Run the Worker locally
bun run devUsing 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:8787Open http://localhost or call it from another terminal. Each request creates a user:
curl http://localhost:8787Created 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.1. 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:
bunx wrangler secret put DATABASE_URLPaste 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
bun run deployWrangler 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.
4.3. Optional: connect through Hyperdrive
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"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:
{
"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:
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
Do not share a Prisma ORM 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:
✘ [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.
@prisma/orm-postgres/serverless exports postgresServerless, a client built for serverless runtimes. It runs in Workers with nodejs_compat. Create it once at module level, as const postgres = postgresServerless<Contract>({ contractJson }). It holds no database connection. Inside fetch, open a connection for the request with await using db = await postgres.connect({ url: env.DATABASE_URL }). The connection has db.orm, db.sql, and db.transaction(...), like the client this guide creates, and await using closes it when the request scope ends, so await every query before then. connect() opens the connection before it returns, and rejects with DRIVER.CONNECTION_FAILED when the database cannot be reached. Reads load the whole result unless you create the serverless client with a cursor option, and reads through a cursor hang behind Hyperdrive. See Serverless client on PostgreSQL.
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
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 /usersroute to the Worker that returns all users as JSON." - "Add a
POST /usersroute that creates a user from the request body and returns 201." - "Add a
published Boolean @default(false)field toPostinsrc/prisma/contract.prisma, emit the contract, and update the database withdb update."
Next steps
- Learn the fundamentals: filtering, sorting, pagination, and writes.
- Change the schema in
src/prisma/contract.prisma, then runnpm run contract:emitandnpx prisma db update. - Use Hono for routing on Workers; the Hono guide shows the same per-route query pattern.
- Cloudflare Workers documentation and the Node.js compatibility reference.
- Read the Prisma ORM overview for the concepts behind contracts and typed queries.
