Deploying Next.js to Cloudflare Workers with OpenNext
wrangler.toml, R2 for ISR cache, D1 for tag cache, KV bindings, Hyperdrive for Postgres, and cron triggers — a complete walkthrough.
Running Next.js on Cloudflare Workers gives you global edge distribution, zero cold starts, and Cloudflare's entire binding ecosystem — KV, R2, D1, Hyperdrive, cron triggers. The setup is non-obvious though. Here's exactly how NodWatch is deployed.
The Stack
- OpenNext (
@opennextjs/cloudflare) — adapts Next.js output for the Workers runtime - Wrangler — local dev and deployment
- R2 — ISR page cache and fetch cache
- D1 — tag-based cache invalidation
- KV — per-user data (recommendations, sessions)
- Hyperdrive — connection pooling to Neon PostgreSQL
wrangler.toml
name = "nodwatch"
main = "worker.js"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
routes = [
{ pattern = "nodwatch.online", custom_domain = true },
{ pattern = "*.nodwatch.online/*" }
]
[[r2_buckets]]
binding = "NEXT_INC_CACHE_R2_BUCKET"
bucket_name = "nodwatch-cache"
[[d1_databases]]
binding = "NEXT_TAG_CACHE_D1"
database_name = "nodwatch-tag-cache"
database_id = "YOUR_D1_ID"
[[kv_namespaces]]
binding = "RECOMMENDATIONS_KV"
id = "YOUR_KV_ID"
[[hyperdrive]]
binding = "HYPERDRIVE"
id = "YOUR_HYPERDRIVE_ID"
[[services]]
binding = "WORKER_SELF_REFERENCE"
service = "nodwatch"
[triggers]
crons = ["0 * * * *"]
[assets]
directory = ".open-next/assets"
binding = "ASSETS"open-next.config.ts
import type { OpenNextConfig } from "@opennextjs/cloudflare";
export default {
default: {
override: {
wrapper: "cloudflare-node",
converter: "edge",
incrementalCache: "cloudflare-r2",
tagCache: "cloudflare-d1",
queue: "cloudflare-queue",
},
},
} satisfies OpenNextConfig;The Worker Entry Point
OpenNext outputs .open-next/worker.js. Wrap it to add the cron handler:
// worker.js
import worker from "./.open-next/worker.js";
export default {
...worker,
async scheduled(event, env, ctx) {
// Hourly cron — expire download licenses
ctx.waitUntil(
fetch(`${env.BETTER_AUTH_URL}/api/downloads/expire-sweep`, {
method: "POST",
headers: { "x-cron-secret": env.CRON_SECRET },
})
);
},
};Hyperdrive for Postgres
Hyperdrive proxies connections to Neon PostgreSQL with built-in connection pooling. In your DB client:
// lib/db.ts
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
function getConnectionString() {
// In Workers: use Hyperdrive's connection string
if (typeof process === "undefined" || process.env.NODE_ENV !== "development") {
// @ts-ignore — injected by Workers runtime
return globalThis.HYPERDRIVE?.connectionString ?? process.env.DATABASE_URL;
}
return process.env.DATABASE_URL;
}
const sql = neon(getConnectionString()!);
export const db = drizzle(sql);Hyperdrive removes the latency of establishing a new Postgres connection on every Worker invocation — critical for edge deployments where connections don't persist.
KV for Per-User Caching
Recommendations are expensive to compute. Cache them per user:
// api/recommendations/route.ts
export async function GET(req: Request) {
const { userId } = await getSession();
const kv = (process.env as any).RECOMMENDATIONS_KV;
const cached = await kv?.get(`rec:${userId}`, "json");
if (cached) return Response.json(cached);
const recommendations = await computeRecommendations(userId);
await kv?.put(`rec:${userId}`, JSON.stringify(recommendations), {
expirationTtl: 3600,
});
return Response.json(recommendations);
}Local Development
# Install
npm install -g wrangler @opennextjs/cloudflare
# Build
npx @opennextjs/cloudflare build
# Local preview with real bindings
wrangler dev
# Deploy
wrangler deployUse wrangler kv:key put, wrangler r2 object put, and wrangler d1 execute to seed data locally.
What Doesn't Work
- Node.js APIs — Workers use the V8 isolate runtime.
fs,path,cryptowork vianodejs_compatbut some packages still break. Test early. - Long-running tasks — Workers have a 30-second CPU limit. Offload to a Queue or Durable Object.
- WebSockets — Use Durable Objects or Cloudflare Calls instead.
The Cloudflare ecosystem payoff is real: a global edge network, sub-millisecond KV reads, and Hyperdrive eliminating cold-connection penalties makes NodWatch feel snappy anywhere in the world.