ADR-022: Cloudflare Workers Database Driver
Status: Proposed Owner: @bilal Date: 2026-02-22
Context
The envo-dashboard deploys to Cloudflare Workers via @opennextjs/cloudflare. Connecting to Supabase PostgreSQL from Workers requires a driver that works in the workerd runtime, which lacks Node.js net/tls modules.
Current State (Working)
The current setup uses pg (node-postgres) + @prisma/adapter-pg with a pnpm patch on pg-cloudflare to fix a bundling issue.
The problem chain:
pgdetects Cloudflare Workers at runtime and delegates topg-cloudflarefor socket handlingpg-cloudflarehas conditionalexportsin itspackage.json:"workerd"→./dist/index.js(realCloudflareSocketusingcloudflare:sockets)"default"→./dist/empty.js(empty module — exports{})
- Next.js 16 uses Turbopack for production builds, which does NOT support the
workerdexport condition - Turbopack resolves
pg-cloudflaretoempty.js→new CloudflareSocket()→new undefined()→TypeError: r2 is not a constructor
The patch (patches/pg-cloudflare@1.2.7.patch) does two things:
- Changes
"default"export from./dist/empty.jsto./dist/index.js - Obfuscates the
import('cloudflare:sockets')call so esbuild (OpenNext’s second bundling stage) doesn’t try to resolve it at build time:// Before (esbuild fails to resolve): const mod = await import('cloudflare:sockets'); // After (esbuild skips — runtime-only): const cfSocketModule = ['cloudflare', 'sockets'].join(':'); const mod = await import(/* webpackIgnore: true */ cfSocketModule);
Required next.config.ts:
const nextConfig: NextConfig = {
serverExternalPackages: ["@prisma/client", ".prisma/client"],
transpilePackages: ["pg", "pg-pool", "pg-protocol", "pg-types",
"pg-connection-string", "pgpass", "pg-cloudflare", "@prisma/adapter-pg"],
turbopack: {
resolveAlias: {
net: "node:net",
tls: "node:tls",
dns: "node:dns",
},
},
};Risks with Current Approach
- Fragile patch — Any
pg-cloudflareversion bump requires re-validating the patch - Two-stage bundling — Turbopack (stage 1) and esbuild (stage 2, OpenNext) have different module resolution semantics; changes to either can break the build
- Turbopack immaturity — The
workerdcondition gap may be fixed upstream, but we can’t depend on when transpilePackages— Forces Turbopack to bundle allpg-*packages, adding friction for updates
Proposed Alternative: @neondatabase/serverless + @prisma/adapter-neon
Replace pg + @prisma/adapter-pg with Neon’s serverless driver, which is purpose-built for edge/serverless runtimes.
Why It Works
@neondatabase/serverlesshandlescloudflare:socketsinternally and is designed to work across Node.js, Deno, and Cloudflare Workers without export condition hacks- Despite the name, it connects to any PostgreSQL database (not just Neon) — works with Supabase via Hyperdrive
- No
pg-cloudflaredependency, no patch needed @prisma/adapter-neon@6.19.2matches current Prisma version
Migration Steps
1. Install dependencies:
pnpm add @neondatabase/serverless @prisma/adapter-neon
pnpm remove pg @prisma/adapter-pg @types/pg pg-cloudflare2. Remove the pg-cloudflare patch:
Delete patches/pg-cloudflare@1.2.7.patch and remove the patchedDependencies entry from pnpm-lock.yaml (or just run pnpm install after removing the patch file).
3. Update lib/db/index.ts:
import { cache } from "react"
import { PrismaClient, Prisma } from '@prisma/client'
import { PrismaNeon } from '@prisma/adapter-neon'
import { Pool, neonConfig } from '@neondatabase/serverless'
// Use WebSocket only when NOT on Cloudflare Workers
// (Workers use cloudflare:sockets natively via the Pool)
// In Node.js local dev, we need the ws package for WebSocket support
if (typeof globalThis.WebSocket === 'undefined') {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
neonConfig.webSocketConstructor = require('ws')
} catch {
// ws not available — fine if running in an environment with native WebSocket
}
}
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const getDb = cache((): PrismaClient => {
// Cloudflare Workers: per-request client with Hyperdrive
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { getCloudflareContext } = require('@opennextjs/cloudflare') as {
getCloudflareContext: () => { env: { HYPERDRIVE?: { connectionString: string } } }
}
const { env } = getCloudflareContext()
if (env.HYPERDRIVE) {
const pool = new Pool({ connectionString: env.HYPERDRIVE.connectionString })
const adapter = new PrismaNeon(pool)
return new PrismaClient({ adapter })
}
} catch {
// Not on Cloudflare Workers
}
// Node.js: global singleton for local development
if (globalForPrisma.prisma) return globalForPrisma.prisma
const pool = new Pool({ connectionString: process.env.DATABASE_URL! })
const adapter = new PrismaNeon(pool)
const client = new PrismaClient({
adapter,
log: process.env.NODE_ENV !== 'production' ? ['query'] : [],
})
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = client
}
return client
})4. Simplify next.config.ts:
const nextConfig: NextConfig = {
serverExternalPackages: ["@prisma/client", ".prisma/client"],
// No transpilePackages needed — Neon driver handles Workers natively
// No turbopack.resolveAlias needed — no net/tls dependency
};5. Install ws for local dev (Neon driver needs WebSocket in Node.js):
pnpm add -D ws @types/wsTrade-offs
Current (pg + patch) | Proposed (@neondatabase/serverless) | |
|---|---|---|
| Stability | Depends on pnpm patch surviving upgrades | No patch — maintained by Neon team for edge |
| Bundle config | Needs transpilePackages + resolveAlias | Minimal config — works out of the box |
| Local dev | Uses pg natively (fast) | Needs ws package for WebSocket (negligible overhead) |
| Driver maturity | pg is the gold standard (20+ years) | Neon driver is newer but battle-tested on edge |
| Dependency | pg (established) | @neondatabase/serverless (Neon-maintained, active) |
| Hyperdrive | Works via pg.Pool | Works via neon.Pool (same connection string) |
When to Migrate
- If
pg-cloudflarereleases a version that breaks the patch - If Turbopack or OpenNext changes bundling behaviour causing new failures
- If upgrading Prisma to v7+ (good opportunity to switch adapters)
- If removing
transpilePackagescomplexity becomes a priority
Decision
Deferred. The current pg + patch approach works. Migrate to Neon adapter if the patch becomes a maintenance burden or if a Prisma major version upgrade provides a natural migration point.