zfb

Type to search...

to open search from anywhere

Architecture overview

CreatedJun 1, 2026Takeshi Takatsudo

Top-down mental model of zfb's runtime stack — the two bundles, what runs where, and why the Hono/Workers shape is the stable contract.

ℹ️ What this page covers

The two-bundle model, a diagram of the full build-time pipeline, why the worker bundle is shaped as a Cloudflare Workers module, and the layering principle that keeps esbuild and the JS runtime swappable. For the step-by-step build pipeline, read Build pipeline.

zfb produces two kinds of JavaScript output, sends them to two different destinations, and uses two different execution environments to get there. Getting that split clear in your head first makes the rest of the architecture fall into place.

The two bundles, two destinations

Worker bundle — build-time only, never shipped to users.

When you run zfb build, the first thing that happens is that esbuild compiles your TSX pages, layouts, and components into a single worker bundle. This bundle is shaped as a Cloudflare Workers module — it exports a fetch handler and nothing else:

export default {
  fetch(request: Request, env: Env, ctx: ExecutionContext): Response { ... }
};

The Rust orchestrator loads this bundle into an embedded V8 isolate and drives it with synthetic HTTP requests — one request per page URL. Each synthetic request produces an HTML Response. The orchestrator writes those responses to dist/ as plain .html files. When the build finishes, the isolate is torn down. At build time the worker bundle never leaves your machine — it is the tool the Rust orchestrator drives to produce the static output. (The same bundle shape also deploys unchanged to Cloudflare Workers for any prerender = false routes; that production path is covered later in this page and in the SSR guide.)

Island bundles — shipped to the browser, on-demand.

Components marked with "use client" are a different story. esbuild bundles each one as a separate ESM module, and those bundles land in dist/ alongside the HTML files. The browser downloads them on-demand and hydrates the corresponding DOM nodes. Island bundles are the only JavaScript that ever reaches end users.

The distinction matters because the two bundles have completely different lifetimes, different execution environments, and different optimization targets. Conflating them is the source of most confusion about how zfb works.

See Islands for how to opt in with "use client".

What runs where

flowchart TD src["TSX / MDX source files\n(pages/, components/, content/)"] subgraph build["zfb build — your machine"] esbuild_worker["esbuild\n(worker bundle)"] v8["embedded V8\n(synthetic HTTP requests → HTML)"] esbuild_islands["esbuild\n(island bundles)"] dist_html["dist/\nHTML files"] dist_js["dist/\nisland JS files"] end deploy["Static host\n(any CDN / Cloudflare Pages)"] browser["Browser\n(island bundles hydrate on-demand)"] src --> esbuild_worker esbuild_worker --> v8 v8 --> dist_html src --> esbuild_islands esbuild_islands --> dist_js dist_html --> deploy dist_js --> deploy deploy --> browser

Everything inside the zfb build box runs on your machine at build time and exits when the build finishes. Nothing in that box runs on a server at request time for static deployments.

For the deeper story on how the build orchestrator coordinates these steps, read Build engine.

The Hono / Cloudflare Workers portability bet

The worker bundle shape — export default { fetch } — is not accidental. It is the same contract a Cloudflare Workers deployment expects. That means the bundle the Rust orchestrator drives at build time can be deployed to Cloudflare Workers unchanged, and workerd will execute it in production for any route that opts out of static prerendering.

The routing core (createPageRouter from @takazudo/zfb-runtime) follows the Hono adapter pattern: one router, multiple entry adapters. The build-time adapter feeds it synthetic Request objects and collects Response strings. The Cloudflare Workers adapter registers it as the worker’s fetch handler. The router itself does not know which adapter is driving it.

This is the portability bet: by fixing the bundle shape as the stable contract, zfb stays decoupled from which JS engine executes it. The build-time host is an embedded V8 isolate. The production host is workerd. The contract — export default { fetch } — is the same in both contexts.

packages/zfb-adapter-cloudflare is the deployment adapter for Cloudflare Workers. It takes the bundle produced by zfb build --target=cloudflare and wraps it in the thin shell that wrangler deploy expects. For a deep dive into the two-file worker output and how requests are dispatched at runtime, see SSR on a Worker (adapter mode).

The full rationale for this design is in JS Runtime: the embedded V8 host design and why the bundle contract is engine-agnostic. The SSG-first, Hono/Workers bundle shape was chosen to keep the build-time and production execution environments interchangeable, and the original in-process deno_core approach was superseded by the embedded V8 isolate once performance and isolation requirements became clear.

Layering summary

Three layers, each with a distinct job:

LayerWhat it ownsWhat it does NOT own
Your sourcePages, components, content, stylesBuild mechanics
zfbThe build contract — route table, page props shape, island protocol, dist/ layoutWhich bundler, which JS runtime
Toolsesbuild (bundling), embedded V8 (build-time evaluation)Your code’s semantics

zfb owns the contract — what inputs it accepts, what the output looks like, and what invariants hold across the build. esbuild and the embedded V8 runtime are implementation details that can be swapped behind the RenderHost trait without touching user code.

The bundler is the cheap, fast saw. zfb is the carpenter.

Downstream of the contract:

  • Islands — how "use client" opts a component into the island bundle
  • Build engine — how the Rust crates coordinate the pipeline
  • Incremental rebuild — how the dependency graph keeps dev rebuilds fast

Revision History