Cloudflare Workers

Bundle your hadars app into a single self-contained Worker script and deploy to Cloudflare's global edge network. SSR runs in the Worker; JS, CSS, and other static assets are served from R2 or a CDN.

How it works

hadars export cloudflare runs your production build and then uses esbuild to bundle the SSR module, HTML template, and all runtime dependencies into a single cloudflare.mjs. The Worker receives standard Web Request objects and returns Response objects — no event format conversion needed.

ResponsibilityHandled by
HTML rendering (SSR)Cloudflare Worker
JS / CSS bundlesR2 bucket (or CDN)
User static filesR2 bucket (or CDN)
API routes (fetch hook)Cloudflare Worker
Cachehadars cache config (in-Worker)

1 — Bundle the Worker

Run this from your project root. It builds the app first, then produces cloudflare.mjs in the same directory.

hadars export cloudflare

# Custom output path
hadars export cloudflare dist/worker.mjs

2 — Upload static assets to R2

Build artifacts in .hadars/static/ are content-hashed and safe to cache indefinitely. Your own files in static/ use a shorter TTL. Skip out.html — that's the SSR template used by the Worker at runtime, not a public file.

# Upload build output — content-hashed, safe for long-term caching
wrangler r2 object put my-bucket/ \
    --file .hadars/static/ \
    --recursive \
    --cache-control "public, max-age=31536000, immutable"

# Upload your own static files
wrangler r2 object put my-bucket/ \
    --file static/ \
    --recursive \
    --cache-control "public, max-age=3600"

3 — Configure wrangler.toml

Point main at your bundle, bind the R2 bucket, and add routing rules so static file requests go to R2 and everything else goes to the Worker.

name = "my-app"
main = "cloudflare.mjs"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]

[[r2_buckets]]
binding = "ASSETS"
bucket_name = "my-bucket"

# Route static assets to R2 — the Worker only handles HTML + API requests.
# Adjust the patterns to match your asset extensions.
[[rules]]
type = "ESModule"
globs = ["**/*.mjs"]

[[routes]]
pattern = "example.com/*.js"
zone_name = "example.com"

[[routes]]
pattern = "example.com/*.css"
zone_name = "example.com"

Alternatively use a Cloudflare Cache Rule or R2 public bucket with a custom domain to serve all static extensions without listing each one in wrangler.toml.

4 — Deploy

wrangler deploy

Manual entry shim

hadars export cloudflare generates and bundles an entry shim automatically. If you prefer to manage the bundling step yourself (e.g. to integrate with an existing esbuild or Vite pipeline), write the shim by hand:

// worker.ts — your Worker entry point
import * as ssrModule from './.hadars/index.ssr.js';
import outHtml from './.hadars/static/out.html';
import { createCloudflareHandler } from 'hadars/cloudflare';
import config from './hadars.config';

export default createCloudflareHandler(config, { ssrModule, outHtml });

Then bundle it yourself:

esbuild worker.ts \
    --bundle \
    --platform=browser \
    --format=esm \
    --target=es2022 \
    --outfile=cloudflare.mjs \
    --loader:.html=text \
    --external:@rspack/*

CPU time limit

Cloudflare Workers on the free plan have a 10 ms CPU time limit per request. hadars uses slim-react for SSR which is synchronous and typically renders a page in under 3 ms, well within the budget. Paid plans (Workers Paid) have no CPU time limit. If you hit the limit, upgrade the plan or reduce the complexity of the initial render.

Local development

Use hadars dev for local development with full HMR — you don't need wrangler for day-to-day work. When you want to test the Worker bundle specifically, use Wrangler's local mode:

# Normal development (recommended)
hadars dev

# Test the bundled Worker locally
hadars export cloudflare && wrangler dev cloudflare.mjs

Also deploying to AWS? See the Deployment page for Lambda and production server instructions.