Skip to main content
Cloudflare Workers is the upgrade path when you outgrow Vercel — high-traffic sites, sub-50ms global p99 latency, or just cheaper egress at scale. The migration is a single adapter swap (OpenNext for Cloudflare) plus a switch to a serverless-friendly Postgres pooler. Plan on an afternoon if it’s your first Workers deploy; less if you’ve shipped Workers before.

Prerequisites

  • A Cloudflare account with Workers enabled (the free plan is enough for a staging deploy).
  • wrangler CLI authenticated: pnpm dlx wrangler login.
  • Your Cloudflare account ID (dashboard → right sidebar).
  • A serverless-friendly Postgres pooler URL — Workers cannot hold long-lived TCP connections, so direct DB URLs are out. Good options: Neon serverless driver (HTTP-based), Supabase pgbouncer (transaction mode), or a hosted PgBouncer.
  • Your existing env values from the Vercel deploy (you’ve already shipped on Vercel, right?). Vercel deploy guide first.
The repo ships a wrangler.toml.example at the root — copy it, don’t write one from scratch.

Step-by-step

1. Install the OpenNext Cloudflare adapter

pnpm add -D @opennextjs/cloudflare
This bundles your Next.js app into a Worker-compatible build under .open-next/worker.js.

2. Create your wrangler.toml

cp wrangler.toml.example wrangler.toml
Edit two fields:
  • name — your Worker’s name (lowercase, dash-separated).
  • account_id — uncomment and paste your Cloudflare account ID.
The shipped config already sets compatibility_date, nodejs_compat flag, the .open-next/assets binding, and an [assets] block. Don’t change those unless you know why.

3. Switch to a serverless-friendly DB pooler

Workers run for milliseconds and tear down — the classic pg driver dies on cold starts. Pick one:
# Neon — uses HTTP, no connection pool needed
DATABASE_URL=postgresql://user:[email protected]/db?sslmode=require

# Supabase — pgbouncer in transaction mode (port 6543)
DATABASE_URL=postgresql://user:[email protected]:6543/postgres?pgbouncer=true
If you’re staying on Drizzle (you are — it’s the only ORM in the scaffold), you may need to swap the driver in src/db/index.ts to drizzle-orm/neon-http or drizzle-orm/postgres-js configured for pooled mode. The pg driver does not work on Workers.

4. Push secrets via wrangler

Never commit secrets to wrangler.toml. Push each one:
wrangler secret put DATABASE_URL
wrangler secret put BETTER_AUTH_SECRET
wrangler secret put STRIPE_SECRET_KEY
wrangler secret put STRIPE_WEBHOOK_SECRET
wrangler secret put RESEND_API_KEY
# …repeat for every server-side env var you use
Public vars (the NEXT_PUBLIC_* ones) live under [vars] in wrangler.toml — they’re shipped in the client bundle anyway, so no harm.

5. Build and deploy

pnpm exec opennextjs-cloudflare build
pnpm exec wrangler deploy
The first build is slow (~3-5 minutes) because OpenNext ahead-of-time bundles every page. Subsequent builds incremental-cache better. The deploy URL prints to stdout — it’ll be https://<your-worker-name>.<your-subdomain>.workers.dev. Custom domains attach through the Cloudflare dashboard (Workers & Pages → your worker → Custom Domains).

6. Re-point payment webhooks

Same drill as Vercel: in your payment provider’s dashboard, change the webhook endpoint URL to the new Workers domain. Verify the signing secret you pushed via wrangler secret put matches the value the dashboard expects.

Verify it works

Identical checklist to the Vercel deploy:
  • https://your-worker.workers.dev/ — home renders
  • https://your-worker.workers.dev/api/ping{ ok: true }
  • https://your-worker.workers.dev/sitemap.xml — populated
  • A real $1 test charge → payment row appears within 5s.
Workers gives you per-request CPU-time metrics in the dashboard. If you see sustained > 50ms CPU times on simple pages, you have a misconfigured DB driver holding connections.

Common pitfalls

  • Long-lived DB connections. The number-one mistake. Workers cannot hold a pool — every request is a new isolate, and TCP sockets vanish at the end. Use Neon’s HTTP driver or Supabase’s pgbouncer URL. Symptom: random 522 / 524 errors under load.
  • Missing nodejs_compat flag. Already present in the shipped config; if you removed it, webhook signature verification (node:crypto) explodes at runtime.
  • Node-only deps. Audit your dependencies with pnpm dlx knip before deploying. Anything that imports fs, child_process, net, or worker_threads will silently break. The scaffold itself is clean — third-party libs you add are the risk.
  • KV consistency confusion. Workers KV is eventually consistent. Don’t use it as the source of truth for auth sessions or payment idempotency. Postgres remains the source of truth for everything ledger-shaped.
  • Wrangler vars vs secrets. [vars] are public (shipped in source). wrangler secret put is encrypted. Putting STRIPE_SECRET_KEY in [vars] is a wallet-emptying mistake.
  • Deploy size limits. Free Workers plan caps at 1 MB compressed. OpenNext hits this fast — bump to the Paid plan ($5/mo, 10 MB) before you ship anything with images or fonts.

Official docs