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
1. Context & Problem Statement
Section titled “1. Context & Problem Statement”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 (output: "server"), Astro’s session subsystem historically defaults to Cloudflare KV for server session persistence, automatically injecting a default KV namespace binding into the Cloudflare deployment manifest:
{ "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:
- 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.
- Teardown Overhead: Destroying preview workers requires orchestrating cascading KV namespace deletions. Any orphaned KV namespace remains as persistent account-level technical debt.
- Architectural Mismatch: Public visitors reading a bakery menu or agency case study have zero need for visitor session KV. Binding session storage to public marketing frontends is 100% dead weight.
The Resolution: Non-KV Session Driver Configuration
Section titled “The Resolution: Non-KV Session Driver Configuration”Early prototypes attempted to bypass this issue by forcing public client applications into pure build-time static site generation (output: "static"). However, this created a severe architectural contradiction: if public frontends were purely build-time static, updating CMS content (e.g. an 8:15 AM sold-out pastry or emergency weather closure) would require triggering a CI/CD build and redeploying the worker, violating the sub-2-second phone-to-edge freshness invariant.
The authoritative resolution is configuring Astro with a non-KV session driver (such as session: { driver: "memory" } in astro.config.mjs) or omitting KV session configuration entirely. This completely eliminates @astrojs/cloudflare’s automatic SESSION KV namespace injection while allowing CMS-enabled client frontends (Worker A) to execute in stateless Edge SSR mode (output: "server"). Public frontends re-render fresh HTML on edge cache misses at runtime without requiring CI/CD rebuilds, while maintaining zero KV bindings. Non-CMS static sites (marketing, docs, me-portfolio) remain permitted to use output: "static".
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:
- 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.
- 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.
- 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.
- Security Perimeter Impossibility: We cannot cleanly apply Cloudflare Access / Zero Trust policies to
/adminand/_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 edge delivery for public visitors backed by heavy Anycast edge caching and instant Cache-Tag purge invalidation, while providing an isolated, secure, dynamic edge runtime for client CMS operations (Worker B).
2. Decision Drivers
Section titled “2. Decision Drivers”- Zero-KV Public Delivery: Public marketing frontends must build, deploy, and execute with zero KV namespace bindings, zero auto-provisioned preview KV namespaces, and zero client JS runtime overhead. This is achieved via
session: { driver: "memory" }without forcing build-time SSG. - Sub-20ms Global Edge TTFB via Heavy Caching: All public pages must be delivered from Cloudflare Anycast edge cache (
s-maxage,stale-while-revalidate,Cache-Tag). On edge cache misses (after a tag purge or cache expiration), Worker A executes stateless Edge SSR to dynamically render fresh HTML and re-cache it, achieving instant freshness without CI/CD rebuilds. - Zero CI/CD Rebuilds on Content Updates: Business owner updates in the CMS must reflect live globally in under 2 seconds via instant Cache-Tag purge (
@siteswarm/service-cache-purge) rather than triggering multi-minute git commits and CI build pipelines. - Strict Blast Radius Containment: Administrative operations, heavy CMS dependencies, schema migrations, and admin sessions must be physically isolated in an independent compute isolate (Worker B). Admin failures must never degrade public site availability.
- 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. - 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. - 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 Edge SSR frontend (with heavy Anycast caching) 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_Cache["Cloudflare Anycast Edge Cache\n(s-maxage=86400, SWR=60, Cache-Tag)"] 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 Frontend Worker (@siteswarm/app-*)"] direction TB EdgeSSR["Stateless Edge SSR (output: 'server')\nHeavy Caching | Sub-20ms TTFB via Cache\nZero Session KV (driver: 'memory')"] Router["Zero-Latency Path Router"] PublicPages["/, /menu, /about, /contact\n(Full HTML, 0 KB Client 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
subgraph PlatformPurge["Platform Service (@siteswarm/service-cache-purge)"] PurgeAPI["Cloudflare Cache Purge API (<150ms Global Purge)"] end end
Visitor -->|GET /menu| CF_WAF CF_WAF --> CF_Cache CF_Cache -->|"Cache HIT (>99%)\n<20ms TTFB"| Visitor CF_Cache -->|"Cache MISS\n(Post-Purge or Expiration)"| WorkerA WorkerA --> PublicPages PublicPages -->|"Dynamic Edge SSR\n(Fetch CMS state via Service Binding)"| WorkerB WorkerA -->|"Re-cache with Cache-Tag"| CF_Cache
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 EmDash -->|"Content Publish -> Trigger Purge"| PlatformPurge PlatformPurge -.->|"Purge by Cache-Tag (<150ms)"| CF_Cache3.1 The Two Decoupled Workers
Section titled “3.1 The Two Decoupled Workers”Worker A: Public Client Frontend (apps/<client>)
Section titled “Worker A: Public Client Frontend (apps/<client>)”- Execution Mode: Stateless Edge SSR (
output: "server"for CMS-enabled client sites, eliminating the need for CI builds on content updates;output: "static"is permitted for non-CMS sites such asmarketing,docs, andme-portfolio). - Heavy Anycast Edge Caching: Responses attach edge cache control headers with tenant surrogate keys:
Cache-Control: public, max-age=0, s-maxage=86400, stale-while-revalidate=60Cache-Tag: tenant:<client-slug>
- Zero-Session-KV Guarantee: Configured with
session: { driver: "memory" }inastro.config.mjs, completely suppressing the@astrojs/cloudflareSESSIONKV namespace injection. Zero KV namespaces are created or bound in preview or production. - Shielded Database Boundary: Zero direct D1 database bindings on public render routes. CMS state is fetched over internal Service Bindings (
env.CMS_SERVICE) and protected by an in-memory micro-cache with single-flight promise coalescing. - Client JS Overhead: 0 KB client JS for standard content pages.
- Performance: Sub-20ms edge TTFB delivered directly from Cloudflare’s Anycast edge cache on cache hits. On cache misses, Worker A executes Edge SSR in < 50ms and re-populates the 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"}]}
- Astro Edge Configuration:
// astro.config.mjs (Worker A - CMS-enabled client)import { defineConfig } from "astro/config";import cloudflare from "@astrojs/cloudflare";export default defineConfig({output: "server", // Stateless Edge SSR for dynamic revalidation without CI buildsadapter: cloudflare({imageService: "passthrough",}),session: {driver: "memory", // Eliminates @astrojs/cloudflare default SESSION KV auto-provisioning},});
- Edge Ingress Routing Logic:
// src/middleware.ts or edge fetch handler in Worker Aexport 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 Bindingif (url.pathname.startsWith('/admin') || url.pathname.startsWith('/_emdash')) {return env.CMS_SERVICE.fetch(request.clone());}// Public visitor requests are served from Edge SSR with Anycast edge cachingreturn env.ASSETS ? env.ASSETS.fetch(request) : handlePublicRequest(request, env, ctx);}};
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 Aadmin.client.com -> Worker B |
client.com/* -> Worker AWorker 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”- 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. - 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. - Frictionless Mobile Onboarding: Business owners simply append
/adminto 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.
5. Security & Zero Trust Perimeter
Section titled “5. Security & Zero Trust Perimeter”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)5.1 Perimeter Protection Layers:
Section titled “5.1 Perimeter Protection Layers:”- Cloudflare Access (Zero Trust): An Access application rule gates paths matching
client.com/admin*andclient.com/_emdash/*. Unauthenticated requests are challenged or blocked at Cloudflare’s edge before any compute code in Worker B executes. - Header Assertion Validation: Worker B cryptographically verifies the
Cf-Access-Jwt-Assertionheader to ensure requests cannot bypass Cloudflare Access. - Physical Isolation from Public Routes: Public visitors browsing
/,/menu, or/contactnever trigger Cloudflare Access policies, ensuring completely frictionless, public browsing. - 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)”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 triggering CI/CD rebuilds or 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 Platform Purge Service (@siteswarm/service-cache-purge) participant Cache as Cloudflare Anycast Edge Cache actor Visitor as Global Visitor participant WorkerA as Worker A (Public Frontend - Edge SSR)
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 (env.CACHE_PURGE_SERVICE) Note over Bridge: Tags: ["tenant:green-leaf-bakery"]
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 -> Trigger Edge SSR Render WorkerA->>WorkerB: Fetch Fresh CMS State via Service Binding (env.CMS_SERVICE) WorkerB-->>WorkerA: Fresh Content JSON WorkerA-->>Cache: Fresh HTML + Cache-Tag: ["tenant:green-leaf-bakery"] Cache-->>Visitor: Sub-20ms Fresh Page Delivered (Zero CI Build Required)6.1 Cache Invalidation Protocol
Section titled “6.1 Cache Invalidation Protocol”- Tagged Content Caching: When Worker A executes Edge SSR, it attaches Cloudflare
Cache-Tagheaders and heavy cache controls:Cache-Control: public, max-age=0, s-maxage=86400, stale-while-revalidate=60Cache-Tag: tenant:green-leaf-bakery - Targeted Tag Purging: Upon successful D1 write, Worker B invokes the centralized Platform Purge Service (
@siteswarm/service-cache-purge) via zero-latency Cloudflare Service Binding (env.CACHE_PURGE_SERVICE), targeting the tenant entity tag (e.g.tenant:green-leaf-bakery). - Global Edge Purge in < 150ms: Cloudflare purges the cached assets from all 330+ global edge locations in under 150ms.
- Dynamic Edge SSR on Cache Miss (No CI Builds): On the next visitor request following a purge, Cloudflare Anycast encounters an edge cache miss and dispatches to Worker A. Worker A executes stateless Edge SSR, fetches fresh CMS state via
env.CMS_SERVICE, renders complete accessible HTML with 0 KB client JS, and re-populates the edge cache. Subsequent visitors hit warm edge cache at sub-20ms TTFB. - D1 Read Shielding: Public visitors never hit D1 directly. Worker A queries Worker B over internal Service Bindings, protected by an in-memory micro-cache (10s TTL) and single-flight promise coalescing. Under viral traffic, 99.9% of requests are absorbed by edge cache, completely shielding D1 from cache stampedes.
7. Consequences & Verification
Section titled “7. Consequences & Verification”Positive Consequences
Section titled “Positive Consequences”- Zero CI/CD Rebuild Latency for Content: Content mutations reflect globally in under 2 seconds. Small business owners make phone updates without waiting 2-5 minutes for git commits, GitHub Actions runs, and full site redeployments.
- Zero Ephemeral KV Overhead: Worker A configures
session: { driver: "memory" }, completely eliminating the@astrojs/cloudflaredefaultSESSIONKV namespace injection. Preview environments deploy cleanly with zero KV quotas consumed. - Extreme Frontend Performance: Over 99% of requests serve directly from Cloudflare Anycast edge cache with sub-20ms TTFB and 0 KB client JS.
- Enhanced Security & Blast Radius Containment: 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/adminvia 0ms Service Bindings with zero CORS friction.
Negative Consequences / Trade-offs
Section titled “Negative Consequences / Trade-offs”- Two Worker Configurations: Each client application maintains two worker definitions (
apps/<client>andservices/cms-<client>), slightly increasing repository configuration surface. - Service Binding Dependency: Local development requires Wrangler multi-worker configuration (
wrangler devwith service bindings) or mock fallback.
8. Implementation Roadmap & Migration Steps
Section titled “8. Implementation Roadmap & Migration Steps”- Step 1: Configure Non-KV Session Driver in Frontend Configurations:
- Configure
session: { driver: "memory" }inapps/<client>/astro.config.mjsto eliminate the defaultSESSIONKV binding. - For CMS-enabled client applications, configure
output: "server"to enable stateless Edge SSR revalidation on edge cache misses. For non-CMS sites (marketing,docs,me-portfolio), preserveoutput: "static". - Verify
wrangler.jsonchas nokv_namespacesdeclared for the frontend.
- Configure
- Step 2: Isolate CMS Engine into Dedicated Worker:
- Move EmDash CMS integration into
@siteswarm/service-cmsor per-client CMS worker definitions. - Bind D1 and R2 exclusively to Worker B.
- Move EmDash CMS integration into
- Step 3: Establish Service Binding in Frontend Worker:
- Declare
services: [{ binding: "CMS_SERVICE", service: "<client>-cms" }]in Worker A. - Route
/admin*and/_emdash/*throughenv.CMS_SERVICE.
- Declare
- Step 4: Configure Cloudflare Access Zero Trust Rules:
- Apply Zero Trust perimeter to
/admin*paths on custom hostnames.
- Apply Zero Trust perimeter to
- Step 5: Wire Centralized Cache Purge Service:
- Connect Worker B CMS publish hooks to
@siteswarm/service-cache-purgeviaenv.CACHE_PURGE_SERVICEto purgetenant:<client-slug>tags on publish.
- Connect Worker B CMS publish hooks to