Inside the Cloudflare Workers Deployment
The companion to our overview post, for readers who want to see the moving parts. Workers for Platforms, dispatch namespaces, KV-backed routing, SSL for SaaS, wrangler — what each piece does and why we use it.
Who this is for: you've shipped a TanStack / Nuxt / Astro / Hono app before. You know what wrangler deploy does. You're curious how a SaaS provider actually pulls off "host customer apps on Cloudflare without making them touch Cloudflare". This post is what's behind the curtain — the topology, the resources, the commands.
The topology
There are three Worker things in play:
- A dispatch namespace (Workers for Platforms primitive) — an isolation boundary that can hold thousands of user Workers, billed and quota-managed as a single unit.
- One user Worker per customer app, deployed into that namespace.
- One dispatch Worker sitting in front, bound to the namespace, which receives every request and forwards it to the right user Worker.
Routing is done with a tiny Workers KV namespace (STATICBOT_ROUTES) that maps hostname → user-worker-name. On every request, the dispatch Worker reads the Host, looks it up in KV, and dispatches.
// dispatch_worker/index.js (simplified)
export default {
async fetch(request, env, ctx) {
const host = new URL(request.url).hostname.toLowerCase();
const workerName = await env.ROUTES.get(host);
if (!workerName) return new Response("host_not_registered", { status: 404 });
const userWorker = env.DISPATCH.get(workerName);
return userWorker.fetch(request);
}
};That's the entire hot path. KV reads are cached at the colo, so warm latency is in low single-digit milliseconds. A bad hostname (no KV entry) returns 404; a registered hostname whose user Worker hasn't been deployed yet returns 503 — distinguishable failure modes for observability.
Why KV-backed routing and not hostname-as-Worker-name? Decoupling. A user Worker's name is internal — we can rename, redeploy, or rotate without touching the customer's DNS. And KV lets us do other things (rate-limit hints, A/B flags, maintenance toggles) on a per-hostname basis without a control-plane round trip.
SSL for SaaS and Custom Hostnames
The customer's domain stays at their registrar. They add an ALIAS / ANAME at the apex (or CNAME on a subdomain) pointing at our fallback hostname, plus a DCV TXT record we generate so Cloudflare can verify ownership before issuing a certificate. Once Cloudflare's automated DCV completes, the cert goes live.
Mechanically: when a request hits Cloudflare's anycast IPs, the TLS handshake's SNI carries the customer's hostname. Cloudflare looks up the hostname in its Custom Hostnames table for our zone, finds the cert issued for it, terminates TLS, and routes the decrypted request to our fallback origin — which is also our dispatch Worker via a Workers Route on the SaaS zone. The dispatch Worker reads Host (still the customer's), KV-looks-up, dispatches.
Customers on registrars without ALIAS / ANAME / CNAME-flattening can't bare-apex this setup — the same constraint every SaaS-on-anycast hits. The deployment UI surfaces the www-canonical fallback (CNAME on www + registrar-side redirect from apex) automatically. See our DNS guides on ANAME/ALIAS and CNAME records for registrar specifics.
What the per-customer Terraform provisions
Open source at github.com/bitfiction/staticbot under infrastructure/_templates/cloudflare_workers_app_template/. Two resources per customer:
cloudflare_workers_kv— one key/value entry inSTATICBOT_ROUTES:app.acme.com → acme-app.cloudflare_custom_hostname— the SSL for SaaS enrollment,ssl.method = "txt",ssl.type = "dv". Outputs include the DCV record values surfaced in the UI.
Notably absent: the user-Worker bundle itself. In Cloudflare provider v5, individual WfP scripts aren't a Terraform resource — wrangler is the only path. We embraced that split: Terraform owns the declarative infra (KV entry + Custom Hostname), wrangler owns the application deploy (bundle upload, plain-text vars in [vars], secrets via wrangler secret put). Two systems, two responsibilities, no overlap — Terraform and wrangler never fight over the same resource.
One consequence: secrets never enter Terraform state. They go directly into Cloudflare's secret store via wrangler at deploy time. The Terraform plan stays inspectable without leaking customer credentials.
What runs against your build
Your build produces the standard TanStack Start output:
.output/ ├── server/ │ └── index.mjs # the Worker handler └── public/ # static assets
Your repo's wrangler.jsonc is ignored at deploy time. Staticbot generates its own wrangler.generated.toml from your stack's template config + Terraform outputs:
name = "acme-app" main = ".output/server/index.mjs" compatibility_date = "2025-09-24" compatibility_flags = ["nodejs_compat"] account_id = "<staticbot's account>" [assets] directory = ".output/public" binding = "ASSETS" [vars] VITE_SUPABASE_URL = "https://abc.supabase.co" # secrets are NOT here — pushed via 'wrangler secret put' after deploy
Then:
wrangler deploy \ --config wrangler.generated.toml \ --dispatch-namespace staticbot-apps-prod # then for each sensitive env var: printf %s "$DB_PASSWORD" | wrangler secret put DB_PASSWORD \ --config wrangler.generated.toml \ --dispatch-namespace staticbot-apps-prod
Keeping the wrangler config generated rather than committed to your repo means deploy-time configuration (account ID, hostname, env values per stack) lives outside source control. Your wrangler.jsonc stays at whatever Lovable committed; we don't fight it.
Framework-agnostic by design
Six template-config fields drive the wrangler-toml rendering: build_command, build_output_dir, worker_main_path, assets_directory, compatibility_date, compatibility_flags. Defaults are TanStack Start. Override them and the same template deploys:
- Nuxt 3 with
nitro.preset = "cloudflare-module"(same.output/server/index.mjsshape) - SolidStart with the Cloudflare adapter
- Astro with
@astrojs/cloudflare(output atdist/_worker.js— overrideworker_main_path) - Bare Hono Workers with a custom build script
The Terraform doesn't care about the framework — it only cares about the runtime contract (a Worker entry, optional assets, optional bindings). Anything Nitro-based "just works"; anything that produces a CF Worker bundle is one config override away.
Portability — what would change to leave
Your build artifact is a stock TanStack Start .output/ directory built by Nitro. To move off Cloudflare, change the Nitro preset:
aws-lambda— Lambda handler signature, response streaming supported since 2023. Pair with API Gateway HTTP API or Lambda Function URLs, CloudFront in front, S3 for assets.node-server— bog-standard Node HTTP server. Containerize, deploy to Fly / Render / Railway / wherever.netlify,vercel,deno-deploy, etc. — one config line, the application code is unchanged.
Two things you'd need to redo on the new host: (1) the wrangler-equivalent setup (Lambda function + API Gateway + ACM cert if AWS; netlify.toml if Netlify; etc.) and (2) DNS — point your hostname at the new origin. Your application code itself doesn't change.
For Lovable apps specifically, the @lovable.dev/vite-tanstack-config wrapper hardcodes the Cloudflare preset. Switching presets means either replacing that wrapper with a hand-rolled vite config (mechanical), or waiting for Lovable to support more targets (likely, eventually).
Verifying what we deployed
The deployment surface is a Cloudflare Custom Hostname plus a user Worker in a dispatch namespace. Customers don't get CF dashboard access (it's our account), so we expose checks the customer can run themselves:
- The Staticbot dashboard polls Custom Hostname status (
pending_validation,active,pending_deployment, etc.) live from the CF API. - Standard probes:
curl -I https://your-domainfor headers,dig your-domainfor DNS propagation,openssl s_client -servername your-domain -connect your-domain:443for cert validation. - If your app misbehaves: server logs surface in the Staticbot dashboard. Tail-level logging (the equivalent of
wrangler tail) for customer Workers is on the roadmap.
The picture in one paragraph
A small dispatch Worker fronts a Workers for Platforms namespace, doing KV-backed Host-header routing to per-customer user Workers. Custom hostnames terminate via SSL for SaaS so customer DNS stays at their existing registrar. Terraform owns the declarative infra (KV entry, Custom Hostname); wrangler owns the bundle upload and secret push. The build artifact is a stock Nitro .output/ directory — change the preset and it runs anywhere modern serverless runs. That's the entire system.
