Staticbot logoStaticbot.dev
    Technical deep dive

    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.

    Published June 16, 2026 · Updated September 23, 2026•10 min read•Audience: experienced/curious developers
    By , Co-founder of StaticbotLinkedIn

    Ravel works on Staticbot’s migration platform, moving apps built with Lovable and Base44 onto Supabase and hosting their owners control.

    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:

    1. 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.
    2. One user Worker per customer app, deployed into that namespace.
    3. 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 in STATICBOT_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

    There are two build layouts, and Staticbot detects which one your repository produces.

    Current Lovable projects: Cloudflare's Vite plugin

    Lovable's @lovable.dev/vite-tanstack-config bundles @cloudflare/vite-plugin. Its build writes the Worker into dist/ together with a generated wrangler.json that declares the entry, the assets, and the compatibility settings. Staticbot treats that generated config as the source of truth and changes only what the platform owns:

    • the Worker name and the runtime vars for your stack;
    • Workers Logs turned on (unless your app explicitly turned them off), so runtime errors are visible after deploy;
    • an ES module rule covering **/*.mjs, so every module the build split out gets uploaded.

    The patched copy is written next to the generated one, and your repository is never edited. Then the same wrangler deploy --dispatch-namespace and wrangler secret put steps run as below.

    Other Nitro builds: a rendered config

    Builds that emit Nitro's .output/ layout (server/index.mjs plus public/) get a wrangler.generated.toml rendered from your stack's template config:

    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

    Either way, deploy-time configuration (account ID, Worker name, env values per stack) lives outside source control. Your repository's Wrangler config 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. They apply to the rendered-config path; TanStack Start built with Cloudflare's Vite plugin has its own adapter and needs no overrides. With overrides, the same template is designed to deploy:

    • Nuxt 3 with nitro.preset = "cloudflare-module" (same .output/server/index.mjs shape)
    • SolidStart with the Cloudflare adapter
    • Astro with @astrojs/cloudflare (output at dist/_worker.js — override worker_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). TanStack Start is the path we test continuously. Other frameworks deploy through template-config overrides; tell us if yours needs a dedicated adapter.

    Portability — what would change to leave

    Your app is a stock TanStack Start project built by Nitro; Lovable's wrapper simply targets Cloudflare. 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-domain for headers, dig your-domain for DNS propagation, openssl s_client -servername your-domain -connect your-domain:443 for cert validation.
    • After every deploy, Staticbot smoke-checks your hostname over HTTPS. From an AI agent, the Staticbot MCP tool recheck_dns_verification asks Cloudflare to retry domain validation and returns both the certificate and hostname status.
    • If your app misbehaves: Workers Logs are on for your Worker, and its runtime logs show in the Staticbot deployment view — the same information wrangler tail would give you, without access to our Cloudflare account.

    Coming soon: your own Cloudflare account

    Everything above describes Staticbot-managed hosting, which is live today. The second ownership model is coming soon: the same build, deployed as an ordinary Worker into the customer's own Cloudflare account and attached to their domain as a Worker Custom Domain in their own zone. No dispatch namespace, no KV route, no Custom Hostname, and no Terraform — Wrangler owns the Worker and its domain together, so the apex works directly. Staticbot keeps building, redeploying, rolling back, and removing the Worker. In the first version, apps that declare their own D1, KV, R2, or Queues bindings won't be supported on this model yet.

    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. Current Lovable builds deploy from the config Cloudflare's Vite plugin generates, with only the platform's name, vars, and logging layered on; other Nitro builds get a rendered config. The app itself is stock TanStack Start — change the preset and it runs anywhere modern serverless runs. That's the entire system.