Deploy Next.js to Cloudflare Workers
Vercel has the best developer experience of any deployment platform, but costs can spiral quickly once your site gets real traffic. Dokploy and Coolify are great self-hosting tools that let you run unlimited projects on a single VPS — but you're responsible for keeping that server secure.
Cloudflare Workers sits at an interesting middle ground: $5/month gets you 10 million requests and 30 million milliseconds of CPU time, making it dramatically more cost-effective than Vercel at scale. It's no surprise that more and more developers are moving their deployments here.
That said, the deployment experience is rough. Expect to work through a lot of documentation and community threads before everything clicks.
To smooth out that process, the NEXTY.DEV boilerplate includes a dedicated cf-pg branch built specifically for Cloudflare Workers + Postgres deployments. It supports Neon, Supabase, and self-hosted Postgres out of the box.
This guide walks you through deploying the NEXTY.DEV boilerplate (cf-pg branch) to Cloudflare Workers.
Step 1: Set Up the CLI
Before you begin, make sure you have wrangler (Cloudflare's CLI) installed and that you're logged in:
npx wrangler loginThis will open a browser window for you to authorize Wrangler. You'll need this for all R2, D1, and deployment operations that follow.
Step 2: Create wrangler.jsonc
The cf-pg branch ships with a wrangler.example.jsonc as a starting point:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "<YOUR_WORKER_APP_NAME>",
"main": ".open-next/worker.js",
// When you start your project, you should always set compatibility_date to the current date
// https://developers.cloudflare.com/workers/configuration/compatibility-dates/
"compatibility_date": "2026-02-22",
"compatibility_flags": [
"nodejs_compat",
"global_fetch_strictly_public"
],
// Your Cloudflare Account ID: Cloudflare Dashboard → Workers & Pages → Overview → Account ID
"account_id": "<YOUR_ACCOUNT_ID>",
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"services": [
{
// Required by @opennextjs/cloudflare for ISR/revalidation
"binding": "WORKER_SELF_REFERENCE",
"service": "<YOUR_WORKER_APP_NAME>"
}
],
"images": {
"binding": "IMAGES"
},
"observability": {
"logs": {
"enabled": true,
"invocation_logs": true
}
},
"vars": {
"DEPLOYMENT_PLATFORM": "cloudflare"
},
// --- Incremental Cache (R2) ---
// Required for ISR/SSG caching
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "<YOUR_BUCKET_NAME>"
}
],
// --- Queue (Durable Objects) ---
// Required for time-based revalidation (ISR)
"durable_objects": {
"bindings": [
{
"name": "NEXT_CACHE_DO_QUEUE",
"class_name": "DOQueueHandler"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"DOQueueHandler"
]
}
],
// --- Tag Cache (D1) ---
// Required for On-demand revalidation (revalidatePath/revalidateTag)
// Create D1 database: npx wrangler d1 create <YOUR_D1_DATABASE_NAME>
// Then update the database_id below
"d1_databases": [
{
"binding": "NEXT_TAG_CACHE_D1",
"database_name": "<YOUR_D1_DATABASE_NAME>",
"database_id": "<YOUR_D1_DATABASE_ID>"
}
],
// Use Hyperdrive for TCP-based PostgreSQL connections (Supabase, Self-hosted, or Neon via TCP).
// You can omit this if using provider-specific HTTP drivers (e.g., @neondatabase/serverless).
"hyperdrive": [
{
"binding": "HYPERDRIVE",
"id": "<YOUR_HYPERDRIVE_ID>",
"localConnectionString": "<YOUR_POOLER_CONNECTION_STRING>"
}
]
}Copy this file to wrangler.jsonc and update the following fields:
-
name: Choose a name for your Worker -
compatibility_date: Set this to your project's creation date -
account_id: Find this atCloudflare Dashboard → Workers & Pages → Overview → Account ID -
WORKER_SELF_REFERENCE
service: Must match thenamefield above -
NEXT_INC_CACHE_R2_BUCKET
bucket_name:
Run npx wrangler r2 bucket create <YOUR_BUCKET_NAME>, replacing <YOUR_BUCKET_NAME> with your preferred name. Copy the resulting bucket name into this field. Leave binding unchanged.
d1_databases:
Run npx wrangler d1 create <YOUR_D1_DATABASE_NAME>, replacing <YOUR_D1_DATABASE_NAME> with your preferred name. Copy the resulting database_name and database_id into the config. Leave binding unchanged.
hyperdrive:
If you're using Neon, comment out the hyperdrive section entirely. Neon's serverless driver (@neondatabase/serverless) already handles connection proxying and is purpose-built for serverless environments — no Hyperdrive needed.
If you're using Supabase or a self-hosted Postgres instance, you'll want to enable Hyperdrive. These are standard Postgres services with TCP connection overhead and connection count limits — Hyperdrive helps significantly. Follow these steps:
a. Create a Hyperdrive instance: In the Cloudflare dashboard, go to Workers & Pages → Hyperdrive.


b. Supabase setup: Use the direct connection string on port 5432, not the pooler on port 6543. Since Hyperdrive is already a connection pooler, stacking it on top of Supabase's built-in pooler will cause instability.

Paste the direct connection string into Hyperdrive and uncheck Enable caching.

c. Copy the Hyperdrive ID: Paste the generated ID into your wrangler.jsonc.

d. Local development: Set localConnectionString to your port 6543 connection string (the pooler URL). Your DATABASE_URL environment variable should also use port 6543 locally.
Step 3: Deploy and Configure
These steps are the same regardless of whether you're using Neon or Supabase.
Create a new project and deploy it.

The Project Name must match the name field in your wrangler.jsonc.

The first build will fail — that's expected. Don't worry about it.
Head to Settings and scroll to the bottom to configure the GitHub integration:
- Add all your production environment variables. This has to be done manually. Variables prefixed with
NEXT_PUBLIC_can be added as plain text; all others should be encrypted by clicking the Encrypt button. Don't include surrounding quotes when entering values. - Set Build cache to Disabled.

If you only want specific branches to trigger builds, go to Branch control and uncheck Builds for non-production branches, then push your branch.

Scroll back to the top and add your domain under Domains & Routes.

You'll also need to configure runtime environment variables. The dashboard only lets you add them one at a time, but the boilerplate includes a script that handles this in bulk. Just run:
node scripts/sync-env-to-cloudflare.mjs .envThis syncs all variables from .env to your Worker. You can swap .env for .env.local or any other env file as needed.
Since you already added NEXT_PUBLIC_ variables to the Build environment, you don't need to include them in the runtime environment variables.
Once everything is set, trigger a new build — either by pushing a commit, or by going to Deployments, finding the latest build, and clicking Retry Build.
Your app should now be live at your custom domain.