Skip to content
Stelixx
Back to WritingENGINEERING · August 2026 · 4 min read

Zero-Runtime Edge Bundling: Next.js 16 on Cloudflare Workers

OBS LOG ENTRY · EL −0.5°CHANNEL ENGINEERINGFILED 2026-08READ 4 MIN READ
CANONICAL THESIS

By decoupling Next.js App Router compilation from Node.js dependencies and targeting OpenNext Cloudflare Worker primitives, we eliminate container cold starts and serve pre-rendered HTML from the edge.

A deep dive into our production stack: eliminating Node.js runtime overhead by running pure static and edge SSR builds across Cloudflare’s global network.

Why Edge Infrastructure Matters for Studios

Venture studios face a specific infrastructure dilemma: multiple independent products, each needing production-grade hosting, while operational overhead must stay near zero. Every hour spent maintaining a container fleet is an hour not spent on the ventures themselves.

Traditional container deployments (Kubernetes, ECS) introduce idle costs, cold-start latencies, and a standing maintenance burden that scales with the number of products — exactly the wrong shape for a studio model. This piece records how this site's production architecture resolves that dilemma, and where the real complexity lives.

The OpenNext Worker Pipeline

The baseline pairs Next.js 16 App Router with OpenNext on Cloudflare Workers:

Next.js Source → Webpack Build → OpenNext Adapter → Cloudflare V8 Worker + Static Assets

The build is deliberately two-stage. next build --webpack produces the standard output; the OpenNext adapter then repackages it into a Worker bundle (.open-next/worker.js) plus a static-asset tree (.open-next/assets). Of the site's eighteen routes, every content page is prerendered at build time; the single dynamic route is the contact-intake endpoint.

Two compatibility flags do the heavy lifting: nodejs_compat maps the Node APIs Next.js expects onto workerd implementations, and global_fetch_strictly_public forces all outbound fetches through the public-fetch path — a small but meaningful guarantee that the worker cannot accidentally reach internal network surfaces.

The Wrapper Layer: Where Headers Actually Live

The subtlest part of the architecture is the entrypoint. Wrangler's main does not point at the generated worker directly; it points at a thin wrapper (worker.ts) that imports the OpenNext handler, awaits the response, and stamps the security-header set onto it before returning.

This exists because of a real gap: headers configured via next.config.ts apply to routes Next.js renders, but not to responses produced earlier in the pipeline — redirects resolved by the adapter, and files served straight from the static-asset binding. Without the wrapper, a /notes → /writing 308 and every file under /_next/static/ would ship without CSP, HSTS, or frame protection. The wrapper closes that surface; public/_headers mirrors the same policy for asset-served files; src/lib/security-headers.ts is the single source of truth both consume.

The policy itself is strict because it can be: default-src 'self', scripts limited to 'self' and the inline blocks the site genuinely needs (theme initialization, JSON-LD), no third-party script hosts, frame-ancestors 'none', HSTS with preload.

Contact Intake: A Contract, Not a Passthrough

The one dynamic route deserves its own note because it demonstrates the pattern that matters. The browser never talks to the backend datastore. /api/contact validates the payload locally, then forwards it to an internal shared-data service under a service-key contract — with an explicit 22-key wire whitelist enforced in the adapter.

The whitelist is the point. An earlier version spread the request object into the outbound call, which silently leaked form-internal fields onto the wire; the downstream schema rejected every real submission. The fix was not just an explicit field list — it was a boundary test that asserts wire keys stay within the contract set, so the failure mode cannot regress. Fail-closed is the other half: without a configured service key, the endpoint returns a structured not_enabled rather than a half-working submission.

What This Buys, and What It Costs

The wins are structural: no idle compute, no container patching, content pages served as static assets from the edge, and a uniform security posture applied at the outermost layer where nothing downstream can bypass it.

The costs are honest ones. The worker bundle must carry the Node-compatibility layer, so bundle size discipline matters more than it would on a plain static host. Webpack is pinned because the adapter's behavior there is the exercised path. And every header, every generated file, every redirect has to be verified at the built-output level — the source code passing lint tells you nothing about what the worker actually emits. The build's verification suite (typecheck, lint, link/structure validation, injection scanning on generated HTML, and assertions that the emitted worker config actually carries the security header set) exists because each layer was added after a failure that source-level checks missed.

For a studio running many surfaces, the pattern scales: each new property inherits the wrapper, the header source, the intake contract, and the verification suite — rather than re-deriving them.

  • #Next.js
  • #Cloudflare Workers
  • #OpenNext
  • #Turbopack
  • #Performance
Browse all writings