Skip to content

ADR-0003: Worker Splitting & Client-Side Session KV Elimination

ADR-0003: Worker Splitting & Client-Side Session KV Elimination

Section titled “ADR-0003: Worker Splitting & Client-Side Session KV Elimination”

Status: 🟢 Approved & Adopted
Date: 2026-09-29
Deciders: SiteSwarm Architecture Council, Platform Engineering
Related Epics & Issues: Epic #66 (Standardization & Fleet Wave 1), Issue #65, Epic #38 (Content Invalidation & Freshness), Issue #57 (Ingress & Preview Routing), Issue #28 (Client CMS)
Related Architecture Specifications: HIGH_LEVEL_DESIGN.md, docs/INGRESS_AND_PREVIEW_ROUTING.md, docs/CLIENT_CMS.md, docs/DATA_ISOLATION_AND_STORAGE.md


SiteSwarm powers dozens of independent client web applications for local small businesses (bakeries, boutique agencies, medical clinics, auto repair shops) from a unified monorepo. Small business client applications are fundamentally content-driven, static-first websites. Anonymous public visitors reading menus, inspecting portfolios, or checking business hours do not maintain stateful server sessions; their interactions are globally readable, pre-renderable, and cache-friendly.

However, our empirical preview deployment pipelines and CMS integration spikes uncovered two acute architectural tensions:

1.1 The Astro Session KV Auto-Provisioning Problem

Section titled “1.1 The Astro Session KV Auto-Provisioning Problem”

When @astrojs/cloudflare is configured in Server-Side Rendering (SSR) mode or when server session capabilities are detected, Astro’s build pipeline automatically injects a default KV namespace binding:

{
"kv_namespaces": [
{
"binding": "SESSION"
}
]
}

During ephemeral pull request preview deployments (e.g. green-leaf-bakery-preview-pr-55), Wrangler interprets this declared binding as a deployment requirement. If an explicit KV namespace ID is not supplied in the environment configuration, Wrangler dynamically provisions an ephemeral KV namespace in the Cloudflare account.

This introduces severe operational friction:

  1. Quota Exhaustion: Cloudflare accounts have hard limits on the number of active KV namespaces (e.g. 100 on standard plans). Ephemeral preview namespaces generated per PR across multiple apps risk exhausting account quotas.
  2. Teardown Overhead: Destroying preview workers requires orchestrating cascading KV namespace deletions. Any orphaned KV namespace remains as persistent account-level technical debt.
  3. Architectural Mismatch: Public visitors reading a static bakery menu or agency case study have zero need for visitor session KV. Binding session storage to public marketing frontends is 100% dead weight.

1.2 The Monolithic Worker Blast Radius Problem

Section titled “1.2 The Monolithic Worker Blast Radius Problem”

In initial prototypes, EmDash CMS (emdash + @emdash-cms/cloudflare) was bundled directly inside the client’s public Astro application. While convenient for single-site prototypes, hosting the CMS within the public web worker causes severe side-effects:

  1. Bundle Bloat & Cold Start Penalties: The public edge worker bundles SSR middleware, Kysely SQL query builders, SQLite D1 connection handlers, and administrative UI assets. This inflates the worker bundle from < 50 KB to > 1.5 MB, increasing edge cold starts from sub-15ms to 150ms–250ms.
  2. Direct D1 Binding Exposure: The public frontend worker holds a direct, privileged binding to the client’s Cloudflare D1 database. If a viral traffic spike hits the public site or a denial-of-service attack targets public endpoints, uncached SSR routes can saturate D1 SQLite connection queues.
  3. Shared Blast Radius: A runtime exception, unhandled error, or heavy database migration inside the CMS administrative engine can crash the V8 isolate, taking down public visitor availability for the entire client website.
  4. Security Perimeter Impossibility: We cannot cleanly apply Cloudflare Access / Zero Trust policies to /admin and /_emdash/* without risking authentication challenges leaking into public visitor traffic or complex edge WAF path exclusions.

We require an architectural pattern that guarantees 100% zero-KV, sub-20ms static edge delivery for public visitors while providing an isolated, secure, dynamic edge runtime for client CMS operations.


  1. Zero-KV Public Delivery: Public marketing frontends must build, deploy, and execute with zero KV namespace bindings, zero auto-provisioned preview KV namespaces, and 0 KB client JS runtime overhead.
  2. Sub-20ms Global Edge TTFB: All public pages must be delivered from Cloudflare Anycast edge cache or Cloudflare Assets (SSG / edge prerender) without executing server-side rendering or database queries on the visitor path.
  3. Strict Blast Radius Containment: Administrative operations, heavy CMS dependencies, schema migrations, and admin sessions must be physically isolated in an independent compute isolate. Admin failures must never degrade public site availability.
  4. Zero Trust Perimeter Protection: CMS routes (/admin, /_emdash/*) must be gated behind Cloudflare Zero Trust / Cloudflare Access without requiring non-technical business owners to navigate separate confusing hostnames, and without affecting public visitors.
  5. Single-Domain User Experience: Business owners expect to manage their website at https://client.com/admin (or /_emdash/admin) without forced cross-origin redirects to third-party subdomains, while maintaining zero latency overhead.
  6. Clean Ephemeral Lifecycle: PR preview environments must deploy in seconds with zero stateful resource provisioning (no ephemeral KV) and clean up instantaneously on PR merge.

3. The Decision: Worker Splitting Architecture

Section titled “3. The Decision: Worker Splitting Architecture”

We formally adopt a Worker Splitting Topology that decouples the public static frontend from the dynamic CMS and admin runtime:

flowchart TD
subgraph ClientBrowsers["Client & Visitor Traffic"]
Visitor["Public Visitor Browser\n(Anonymous Customer)"]
AdminUser["Business Owner / Admin Mobile\n(Authenticated Staff)"]
end
subgraph EdgeIngress["Cloudflare Edge Anycast Ingress (Cloudflare for SaaS)"]
CF_WAF["Cloudflare WAF & Security Layer\n(DDoS Shield, Turnstile, Rate Limiting)"]
CF_Access["Cloudflare Access / Zero Trust Perimeter\n(Path Rule: /admin* & /_emdash/*)"]
end
subgraph WorkerFleet["SiteSwarm Decoupled Worker Fleet"]
direction TB
subgraph WorkerA["Worker A: Public Static Frontend Worker (@siteswarm/app-*)"]
direction TB
Assets["Cloudflare Assets Engine (Static Prerender)"]
Router["Zero-Latency Path Router"]
PublicPages["/, /menu, /about, /contact\n(Sub-20ms TTFB, 0 KB JS, Zero Session KV)"]
end
subgraph WorkerB["Worker B: Isolated CMS & Admin Worker (@siteswarm/service-cms-*)"]
direction TB
EmDash["EmDash CMS Engine (Astro/React Studio)"]
AdminAuth["Admin Auth & Session Handler (Encrypted Cookies)"]
MCP["Model Context Protocol Agent API (/_emdash/api/mcp)"]
D1Binding["Cloudflare D1 Binding (Atomic SQLite)"]
R2Binding["Cloudflare R2 Binding (Media Storage)"]
end
end
Visitor -->|GET /menu| CF_WAF
CF_WAF -->|Public Route| WorkerA
WorkerA --> Assets --> PublicPages
AdminUser -->|GET /admin| CF_WAF
CF_WAF --> CF_Access
CF_Access -->|Authorized Staff| WorkerA
WorkerA -->|Path Match: /admin* or /_emdash/*\nService Binding: env.CMS_SERVICE| WorkerB
WorkerB --> EmDash
EmDash --> D1Binding
EmDash --> R2Binding

Worker A: Public Client Frontend (apps/<client>)

Section titled “Worker A: Public Client Frontend (apps/<client>)”
  • Execution Mode: Pure static edge delivery (output: "static" / Cloudflare Workers static assets).
  • Bindings: Zero KV bindings (SESSION explicitly disabled/omitted). Zero D1 database bindings on public render routes.
  • Client JS Overhead: 0 KB client JS for content pages.
  • Performance: Sub-20ms edge TTFB delivered directly from Cloudflare’s Anycast edge cache.
  • Service Binding: Exposes an internal Service Binding client to Worker B:
    // wrangler.jsonc (Worker A)
    {
    "name": "green-leaf-bakery",
    "compatibility_date": "2026-09-29",
    "compatibility_flags": ["nodejs_compat"],
    "services": [
    {
    "binding": "CMS_SERVICE",
    "service": "green-leaf-bakery-cms"
    }
    ]
    }
  • Edge Ingress Routing Logic:
    // src/middleware.ts or edge fetch handler in Worker A
    export default {
    async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    // Route admin & CMS paths directly to Worker B via 0ms Service Binding
    if (url.pathname.startsWith('/admin') || url.pathname.startsWith('/_emdash')) {
    return env.CMS_SERVICE.fetch(request.clone());
    }
    // Public visitor requests are served from static assets & edge cache
    return env.ASSETS.fetch(request);
    }
    };

Worker B: Isolated CMS & Admin Worker (services/cms-<client>)

Section titled “Worker B: Isolated CMS & Admin Worker (services/cms-<client>)”
  • Execution Mode: Dynamic SSR Worker isolate dedicated exclusively to administrative workflows.
  • Hosted Applications: EmDash CMS Studio, /admin, /_emdash/*, admin API endpoints, and Model Context Protocol (MCP) agent endpoints (/_emdash/api/mcp).
  • Bindings: Holds the privileged Cloudflare D1 database binding (binding: "DB") and Cloudflare R2 media bucket binding (binding: "MEDIA").
  • Session Management: Session state is strictly confined to Worker B. Worker B uses encrypted HTTP-only session cookies (or an isolated, non-ephemeral administrative KV namespace if needed) without leaking any session dependency into Worker A.
  • Blast Radius Isolation: Memory consumption, CPU spikes from image optimization, schema migrations, and admin errors are physically confined to Worker B. If Worker B fails or is taken offline for maintenance, Worker A continues serving public visitors at 100% availability.

4. Ingress & Routing Evaluation: Subdomains vs. Service Bindings

Section titled “4. Ingress & Routing Evaluation: Subdomains vs. Service Bindings”

We evaluated two candidate ingress architectures for routing traffic between the public frontend and the isolated CMS worker:

Architectural Dimension Option A: Subdomain Routing (admin.client.com) Option B: Cloudflare Service Bindings (Selected Canonical)
Ingress Topology client.com -> Worker A
admin.client.com -> Worker B
client.com/* -> Worker A
Worker A proxies /admin* to Worker B via Service Binding
Network Latency Direct public edge DNS resolution to Worker B 0ms in-process V8 isolate hop via Cloudflare Service Binding
Cloudflare for SaaS Cost Requires 2 Custom Hostnames per client (www + admin) Requires only 1 Custom Hostname per client (www.client.com)
DNS Configuration Friction Client must configure additional DNS CNAME for admin subdomain Zero DNS friction: Single CNAME handles public site and /admin
Zero Trust / Access Perimeter Cloudflare Access application scoped to subdomain admin.client.com Cloudflare Access path-based rule scoped to /admin* and /_emdash/*
Cookie & CORS Isolation Cross-subdomain cookie scope or CORS headers required Same-origin (client.com/admin): Zero CORS preflight, strict same-site cookies
Mobile Business Owner UX Client must remember separate URL (admin.bakery.com) Client opens familiar bookmark: bakery.com/admin
Architectural Verdict ⚠️ Supported for Enterprise Tier 🟢 Adopted as Canonical Platform Standard

4.1 Why Option B (Service Bindings) is the Canonical Standard

Section titled “4.1 Why Option B (Service Bindings) is the Canonical Standard”
  1. SaaS Custom Hostname Quota Efficiency: Cloudflare for SaaS plans include a quota of custom hostnames (100 included on Workers Paid). Subdomain routing burns 2 custom hostnames per client, halving fleet capacity. Service Bindings route everything through the primary custom hostname (www.client.com), doubling our client density per Cloudflare account.
  2. Zero In-Network Overhead: Cloudflare Service Bindings do not perform external TCP/TLS network calls. Invoking env.CMS_SERVICE.fetch(request) executes an in-memory V8 isolate dispatch within the same Cloudflare edge data center at 0ms network latency.
  3. Frictionless Mobile Onboarding: Business owners simply append /admin to their familiar website domain on their mobile phones.

4.2 Support for Option A (Subdomain Routing)

Section titled “4.2 Support for Option A (Subdomain Routing)”

For large enterprise clients or organizations requiring strict subdomain DNS delegation, Option A is fully supported as an alternative configuration by routing admin.client.com directly to Worker B in Cloudflare for SaaS.


Worker B is shielded from unauthorized access and vulnerability scans using a defense-in-depth security perimeter:

sequenceDiagram
autonumber
actor Attacker as Internet Bot / Attacker
actor Owner as Authenticated Business Owner
participant Edge as Cloudflare Anycast Edge (SaaS)
participant Access as Cloudflare Zero Trust (Access)
participant WorkerA as Worker A (Public Frontend)
participant WorkerB as Worker B (Isolated CMS Worker)
participant D1 as Cloudflare D1 Database
Attacker->>Edge: GET /admin (Scan / Exploit attempt)
Edge->>Access: Evaluate Path: /admin*
Access-->>Attacker: 403 Forbidden / Challenge (Blocked at Edge)
Owner->>Edge: GET /admin (Mobile Safari)
Edge->>Access: Evaluate Path: /admin*
Access->>Owner: Magic Link / Passkey Auth Challenge
Owner-->>Access: Complete Verification
Access->>Edge: Inject Cf-Access-Jwt-Assertion Header
Edge->>WorkerA: Dispatch request
WorkerA->>WorkerB: env.CMS_SERVICE.fetch() (Zero Latency)
WorkerB->>WorkerB: Verify Cf-Access-Jwt-Assertion & Role
WorkerB->>D1: Fetch CMS Content
D1-->>WorkerB: Return Data
WorkerB-->>WorkerA: Admin Studio HTML
WorkerA-->>Owner: 200 OK (EmDash Studio Loaded)
  1. Cloudflare Access (Zero Trust): An Access application rule gates paths matching client.com/admin* and client.com/_emdash/*. Unauthenticated requests are challenged or blocked at Cloudflare’s edge before any compute code in Worker B executes.
  2. Header Assertion Validation: Worker B cryptographically verifies the Cf-Access-Jwt-Assertion header to ensure requests cannot bypass Cloudflare Access.
  3. Physical Isolation from Public Routes: Public visitors browsing /, /menu, or /contact never trigger Cloudflare Access policies, ensuring completely frictionless, public browsing.
  4. Database Credential Boundary: Worker A has zero bindings to D1 or R2. Even if a hypothetical vulnerability existed in Worker A’s static asset router, it has no cryptographic or capability access to the underlying SQLite database.

6. Content Invalidation Bridge (Epic #38 Alignment)

Section titled “6. Content Invalidation Bridge (Epic #38 Alignment)”

When a business owner edits content (e.g. updating operating hours or posting an emergency banner) via Worker B, the updated content must propagate globally to Worker A’s edge delivery tier in under 2 seconds without introducing database contention.

This aligns directly with Epic #38: Content Invalidation, Edge Caching, and Data Freshness Model.

sequenceDiagram
autonumber
actor Owner as Business Owner (Mobile Phone)
participant WorkerB as Worker B (CMS Studio)
participant D1 as Cloudflare D1
participant Bridge as Invalidation Bridge
participant Cache as Cloudflare Anycast Edge Cache
actor Visitor as Global Visitor
Owner->>WorkerB: Save Update: "Closed for Snow Day" (POST /_emdash/api/content)
WorkerB->>D1: Atomic SQL Write (UPDATE hours_and_announcements)
D1-->>WorkerB: Commit Confirmed
WorkerB->>Bridge: Trigger Invalidation Event
Note over Bridge: Tags: ["tenant:bakery", "entity:banner", "entity:hours"]
Bridge->>Cache: Cloudflare Cache-Tag Purge (Purge by Tag API)
Cache-->>Bridge: Cache Invalidated Globally (< 150ms)
WorkerB-->>Owner: 200 OK ("Published in 1.2s")
Visitor->>Cache: GET / (Visitor opens website)
Cache->>WorkerA: Cache Miss -> Revalidate Static Slot
WorkerA->>D1: Read Fresh Slot Data (or In-Memory KV Cache)
WorkerA-->>Cache: Fresh HTML + Cache-Tag: ["tenant:bakery"]
Cache-->>Visitor: Sub-20ms Fresh Page Delivered
  1. Tagged Content Caching: When Worker A renders or serves content slots, it attaches Cloudflare Cache-Tag headers:
    Cache-Control: public, max-age=0, s-maxage=31536000, stale-while-revalidate=60
    Cache-Tag: tenant:green-leaf-bakery, entity:banner, entity:hours
  2. Targeted Tag Purging: Upon successful D1 write, Worker B invokes the Cloudflare Purge API via Service Binding or background task, targeting only the affected entity tags (e.g. tenant:green-leaf-bakery).
  3. Global Edge Purge in < 150ms: Cloudflare purges the cached assets from all 330+ global edge locations in under 150ms.
  4. D1 Read Shielding: Public visitors only hit D1 on edge cache misses. Under heavy traffic (e.g. local viral news), 99.9% of requests are served from edge cache, completely shielding D1 from cache stampedes.

  • Zero Ephemeral KV Overhead: PR previews build cleanly with static assets; Wrangler never provisions ephemeral KV namespaces; zero KV quotas are consumed.
  • Extreme Frontend Performance: Worker A is a featherweight static router delivering sub-20ms TTFB globally with 0 KB client JS.
  • Enhanced Security: Administrative endpoints and D1 database credentials are physically isolated in Worker B and protected by Cloudflare Access.
  • Unified Domain UX: Business owners manage their site at client.com/admin via 0ms Service Bindings.
  • Two Worker Configurations: Each client application maintains two worker definitions (apps/<client> and services/cms-<client>), slightly increasing repository configuration surface.
  • Service Binding Dependency: Local development requires Wrangler multi-worker configuration (wrangler dev with service bindings) or mock fallback.

8. Implementation Roadmap & Migration Steps

Section titled “8. Implementation Roadmap & Migration Steps”
  1. Step 1: Disable Session KV in Frontend Configurations:
    • Update apps/<client>/astro.config.mjs to ensure static prerender mode with zero session KV bindings.
    • Verify wrangler.jsonc has no kv_namespaces declared for the frontend.
  2. Step 2: Isolate CMS Engine into Dedicated Worker:
    • Move EmDash CMS integration into @siteswarm/service-cms or per-client CMS worker definitions.
    • Bind D1 and R2 exclusively to Worker B.
  3. Step 3: Establish Service Binding in Frontend Worker:
    • Declare services: [{ binding: "CMS_SERVICE", service: "<client>-cms" }] in Worker A.
    • Route /admin* and /_emdash/* through env.CMS_SERVICE.
  4. Step 4: Configure Cloudflare Access Zero Trust Rules:
    • Apply Zero Trust perimeter to /admin* paths on custom hostnames.