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:

  1. pg detects Cloudflare Workers at runtime and delegates to pg-cloudflare for socket handling
  2. pg-cloudflare has conditional exports in its package.json:
    • "workerd"./dist/index.js (real CloudflareSocket using cloudflare:sockets)
    • "default"./dist/empty.js (empty module — exports {})
  3. Next.js 16 uses Turbopack for production builds, which does NOT support the workerd export condition
  4. Turbopack resolves pg-cloudflare to empty.jsnew 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.js to ./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-cloudflare version 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 workerd condition gap may be fixed upstream, but we can’t depend on when
  • transpilePackages — Forces Turbopack to bundle all pg-* 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/serverless handles cloudflare:sockets internally 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-cloudflare dependency, no patch needed
  • @prisma/adapter-neon@6.19.2 matches current Prisma version

Migration Steps

1. Install dependencies:

pnpm add @neondatabase/serverless @prisma/adapter-neon
pnpm remove pg @prisma/adapter-pg @types/pg pg-cloudflare

2. 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/ws

Trade-offs

Current (pg + patch)Proposed (@neondatabase/serverless)
StabilityDepends on pnpm patch surviving upgradesNo patch — maintained by Neon team for edge
Bundle configNeeds transpilePackages + resolveAliasMinimal config — works out of the box
Local devUses pg natively (fast)Needs ws package for WebSocket (negligible overhead)
Driver maturitypg is the gold standard (20+ years)Neon driver is newer but battle-tested on edge
Dependencypg (established)@neondatabase/serverless (Neon-maintained, active)
HyperdriveWorks via pg.PoolWorks via neon.Pool (same connection string)

When to Migrate

  • If pg-cloudflare releases 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 transpilePackages complexity 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.

References