# 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 ORM 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 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](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 ORM 7? > > Prisma ORM 8 is a [release candidate](https://www.prisma.io/docs/orm/release-status). Prisma ORM 7 remains fully supported; the Prisma ORM 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) 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](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 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: turn the emitted JSON into `const contract = contractFromJson(contractJson)` (from `@prisma/orm-postgres/static`) once at module level, without exporting it, and create one `postgres({ contract, 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 ORM [#2-add-prisma-orm] ### 2.1. Initialize Prisma ORM in the project [#21-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: #### 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 ``` With `--yes`, the command asks no questions and uses the default for anything the flags do not set, such as the schema path `src/prisma/contract.prisma`. It installs the packages and emits the contract. `@prisma/cli-engine` is the library the `prisma` commands are built on, not a query engine: ```text no-copy ✔ npm add @prisma/orm-postgres dotenv ✔ npm add -D prisma@latest @types/node ✔ npm add -D @prisma/cli-engine@0.9.0 ✔ Emit the contract │ target: postgres │ authoring: psl │ schema: src/prisma/contract.prisma written ├─ src/prisma/contract.prisma ├─ prisma.config.ts ├─ src/prisma/db.ts ├─ prisma-8.md ├─ .env.example ├─ tsconfig.json ├─ .gitignore ├─ .gitattributes ├─ package.json └─ README.md installed ├─ @prisma/orm-postgres ├─ dotenv ├─ prisma@latest (dev) ├─ @types/node (dev) └─ @prisma/cli-engine@0.9.0 (dev) ✔ Done. Open prisma-8.md to get started. ``` 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. * `prisma-8.md`: a short quick reference for writing your first typed query. `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; claim it with a free Prisma account 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 db init ``` #### pnpm ```bash pnpm prisma db init ``` #### yarn ```bash yarn prisma db init ``` #### npm ```bash npx prisma 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 ORM; 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 { contractFromJson } from "@prisma/orm-postgres/static"; import type { Contract } from "./prisma/contract.d"; import contractJson from "./prisma/contract.json" with { type: "json" }; const contract = contractFromJson(contractJson); export default { async fetch(request, env, ctx): Promise { const path = new URL(request.url).pathname; if (path === "/favicon.ico") return new Response("Not found", { status: 404 }); const db = postgres({ contract, 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; ``` Keep these points in mind when you change the handler: * 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`. * On PostgreSQL, you reach a model through the database namespace it lives in: `db.orm.public.User`, where `public` is the PostgreSQL schema (a namespace inside the database, not your `contract.prisma` file). `.create(...)` returns the inserted row, database defaults included. * `contract.json` is imported with `with { type: "json" }`. Wrangler bundles it into the Worker. The client does not take this JSON directly, so pass it through `contractFromJson` first. The handler does this once, outside `fetch`, and passes the result as `contract` to the client it creates in each request. * Do not export `contract` from `src/index.ts`. Cloudflare treats every named export of the Worker's entry file as an entrypoint. If another file needs `contract`, create it in its own file and import it from there in both places. ### 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 ORM included) and prints the live URL, `https://prisma-cloudflare-worker..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": "", // [!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, 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 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: > > ```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] > You can also use `postgresServerless` from `@prisma/orm-postgres/serverless` in place of the `postgres(...)` client above. Both open one database connection per request, and both work in Workers with `nodejs_compat`. They differ in what you create inside `fetch`. With `postgres(...)`, you create a whole client in each request. With `postgresServerless`, you create the client once, outside `fetch`, and each request only opens a connection from it with `connect()`: > > ```typescript title="src/index.ts" > import postgresServerless from "@prisma/orm-postgres/serverless"; > import { contractFromJson } from "@prisma/orm-postgres/static"; > import type { Contract } from "./prisma/contract.d"; > import contractJson from "./prisma/contract.json" with { type: "json" }; > > const contract = contractFromJson(contractJson); > const postgres = postgresServerless({ contract }); > > export default { > async fetch(_request, env): Promise { > await using db = await postgres.connect({ url: env.DATABASE_URL }); > const { total } = await db.orm.public.User.aggregate((a) => ({ total: a.count() })); > return new Response(`Number of users in the database: ${total}.\n`); > }, > } satisfies ExportedHandler; > ``` > > The connection `db` has `db.orm`, `db.sql`, and `db.transaction(...)`, like the client this guide creates. `await using` closes the connection when `fetch` returns, so await every query before then. If the database cannot be reached, `connect()` rejects with the error code `DRIVER.CONNECTION_FAILED`. > > Reads load the whole result into memory unless you pass a `cursor` option. Leave `cursor` out behind Hyperdrive, because reads through a cursor hang there. See [Serverless client on PostgreSQL](https://www.prisma.io/docs/orm/reference/transactions-and-runtime#serverless-client-on-postgresql). > [!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 ORM 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 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 ORM 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 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. - [`Docker`](https://www.prisma.io/docs/guides/deployment/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`](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.