Skip to content

Architecture Overview

React Starter Kit runs on three Cloudflare Workers connected by service bindings. A single domain receives all traffic – the web worker routes each request to the right destination without any cross-worker public URLs.

Request Flow

Workers

WorkerWorkspacePurposeHas nodejs_compat
webapps/webEdge router – receives all traffic, routes to app/apiNo
appapps/appSPA static assets (React, TanStack Router)No
apiapps/apiHono server – tRPC, Better Auth, webhooksYes

Web Worker

The web worker is the only worker with a public route (example.com/*). It decides where each request goes:

  • /api/* – forwarded to the API worker
  • /login, /signup, /settings, /analytics, /reports, /_app/* – forwarded to the app worker
  • / – routed by auth hint cookie (app if signed in, marketing site if not)
  • Everything else – served from the web worker's own static assets (marketing pages)
ts
// apps/web/worker.ts (simplified)
app.all("/api/*", (c) => c.env.API_SERVICE.fetch(c.req.raw));
app.all("/login*", (c) => c.env.APP_SERVICE.fetch(c.req.raw));

app.on(["GET", "HEAD"], "/", async (c) => {
  const hasAuthHint =
    getCookie(c, "__Host-auth") === "1" || getCookie(c, "auth") === "1";
  const upstream = await (hasAuthHint ? c.env.APP_SERVICE : c.env.ASSETS).fetch(
    c.req.raw,
  );
  // ...
});

App Worker

A static asset worker with not_found_handling: "single-page-application" – any path that doesn't match a file returns index.html, enabling client-side routing via TanStack Router.

The app worker has no custom worker script. It is accessed only through service bindings from the web worker.

API Worker

Runs the Hono HTTP server with the following middleware chain:

ts
// apps/api/worker.ts (simplified)
worker.onError(errorHandler);
worker.notFound(notFoundHandler);
worker.use(secureHeaders());
worker.use(requestId({ generator: requestIdGenerator }));
worker.use(logger());

// Initialize shared context
worker.use(async (c, next) => {
  const db = createDb(c.env.HYPERDRIVE_CACHED);
  c.set("db", db);
  c.set("dbDirect", createDb(c.env.HYPERDRIVE_DIRECT));
  c.set("auth", createAuth(db, c.env));
  await next();
});

worker.route("/", app); // Mounts tRPC + auth + health routes

Primary endpoints:

PathHandler
/api/auth/*Better Auth (login, signup, sessions, OAuth callbacks)
/api/trpc/*tRPC procedures (batching enabled)
/apiAPI info (name, version, endpoint list)
/healthHealth check

Service Bindings

Service bindings let workers call each other directly over Cloudflare's internal network – no HTTP round-trip through the public internet.

jsonc
// apps/web/wrangler.jsonc
"services": [
  { "binding": "APP_SERVICE", "service": "example-app" },
  { "binding": "API_SERVICE", "service": "example-api" }
]

WARNING

Service bindings are non-inheritable in Wrangler – they must be declared in every environment block. Forgetting this causes staging/preview workers to bind to production services.

Naming convention: <project>-<worker>-<env> (e.g. example-api-staging). See Edge > Service Bindings for the full per-environment config.

Database Connection

The API worker connects to Neon PostgreSQL via Cloudflare Hyperdrive – a connection pool that sits between Workers and your database.

Two bindings are available:

BindingCachingUse case
HYPERDRIVE_CACHEDEnabledDefault reads – most queries go here
HYPERDRIVE_DIRECTDisabledWrites and reads that need fresh data

Both bindings are initialized in the API worker middleware and available on every request context as db and dbDirect. See Database for schema and query patterns.

The / route serves two different experiences – a marketing page for visitors and the app dashboard for signed-in users. The web worker needs a fast signal to choose without owning auth logic.

How it works: Better Auth sets a lightweight __Host-auth=1 cookie on sign-in and clears it on sign-out. The web worker checks only for cookie presence – it never validates sessions. If the cookie exists, the request goes to the app worker; otherwise it serves the marketing page.

This cookie is a routing hint only, not a security boundary. A false positive (stale cookie) results in one extra redirect to /login – the app worker validates the real session.

INFO

In local development the cookie is named auth (HTTP), since browsers reject the __Host- prefix without HTTPS.

See ADR-001 for the full decision record and Sessions & Protected Routes for the auth flow.

Environments

EnvironmentWorkersDomainDatabaseDeploy command
Developmentwrangler devlocalhost:5173Dev branchbun dev
Preview*-previewpreview.example.comPreview branchwrangler deploy --env preview
Staging*-stagingstaging.example.comStaging branchwrangler deploy --env staging
Production* (no suffix)example.comMain branchwrangler deploy

Each environment has its own Hyperdrive bindings, service binding targets, and APP_ORIGIN / ALLOWED_ORIGINS variables. See Edge > Service Bindings for the full wrangler config.

Build Order

The workspaces must build in dependency order:

email → web → api → app

Email templates are compiled first because the API server imports them. The bun build command handles this automatically.

Key Invariants

  • The API worker is the sole authority for authentication and data access – the web worker never validates sessions or queries the database.
  • Only the web worker has public routes. App and API workers are accessed exclusively through service bindings.
  • Service bindings are non-inheritable – every Wrangler environment must declare its own bindings.
  • The auth hint cookie is a routing optimization, not a security mechanism.
  • The API worker is the only worker with nodejs_compat enabled.