Workers for Platforms
Multi-tenant worker deployment with native isolation and metering.
Prerequisites: PRINCIPLES.md, PRIMITIVES.md, OVERVIEW.md
Overview
Section titled “Overview”Workers for Platforms (WfP) enables multi-tenant worker deployment where each tenant gets isolated compute with usage-based metering. z0 uses WfP to provide tenant isolation at the compute layer.
| Concept | What It Is | z0 Usage |
|---|---|---|
| Dispatch Namespace | Container that holds user workers | One namespace per environment |
| User Worker | Tenant-specific worker code | Per-tenant business logic, integrations |
| Dispatch Worker | Router that invokes user workers | Traffic routing, auth, metering entry point |
| Script Tags | Metadata attached to user workers | tenant_id, version, deployment info |
Key Insight: WfP gives you SaaS-grade isolation without managing separate deployments per tenant. Tenants share infrastructure but get isolated execution.
Architecture
Section titled “Architecture”Incoming Request │ ▼┌─────────────────────────────────────────────────────────┐│ Dispatch Worker (z0-controlled) ││ - Authenticate request ││ - Resolve tenant from request ││ - Check tenant status (active, suspended) ││ - Route to appropriate user worker │└─────────────────────────────────────────────────────────┘ │ │ dispatch_namespace.get(script_name) ▼┌─────────────────────────────────────────────────────────┐│ Dispatch Namespace ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ User Worker │ │ User Worker │ │ User Worker │ ││ │ tenant_a │ │ tenant_b │ │ tenant_c │ ││ └─────────────┘ └─────────────┘ └─────────────┘ │└─────────────────────────────────────────────────────────┘ │ ▼Response (metered by WfP)z0 Mapping
Section titled “z0 Mapping”| WfP Concept | z0 Concept | Notes |
|---|---|---|
| Dispatch Namespace | Environment (prod, staging) | One namespace per environment |
| User Worker | Tenant deployment | Custom logic, webhooks, integrations |
| Script Name | tenant_id | Direct mapping for O(1) lookup |
| Script Tags | Entity metadata | version, deployed_at, config_version |
Dispatch Namespaces
Section titled “Dispatch Namespaces”A dispatch namespace is a collection of user workers that can be invoked by a dispatch worker.
Creating a Namespace
Section titled “Creating a Namespace”// wrangler.toml (dispatch worker)[[dispatch_namespaces]]binding = "TENANT_WORKERS"namespace = "z0-prod-tenants"Namespace Strategy
Section titled “Namespace Strategy”| Environment | Namespace | Purpose |
|---|---|---|
| Production | z0-prod-tenants | Live tenant code |
| Staging | z0-staging-tenants | Pre-release testing |
| Development | z0-dev-tenants | Local development |
z0 Recommendation: One namespace per environment. Avoid per-tenant namespaces (operational overhead, no isolation benefit).
User Workers
Section titled “User Workers”User workers are tenant-specific scripts deployed to a dispatch namespace.
Deploying a User Worker
Section titled “Deploying a User Worker”// Deploy tenant-specific workerawait api.workers.scripts.deploy({ account_id: ACCOUNT_ID, dispatch_namespace: "z0-prod-tenants", script_name: tenant_id, // e.g., "tenant_acme" content: workerCode, metadata: { tags: [ { key: "tenant_id", value: tenant_id }, { key: "version", value: deployment_version }, { key: "deployed_at", value: new Date().toISOString() } ] }});Script Naming Convention
Section titled “Script Naming Convention”{tenant_id} // Primary tenant worker{tenant_id}-webhook // Webhook handler{tenant_id}-integration-{name} // Third-party integrationWorker Bindings for User Workers
Section titled “Worker Bindings for User Workers”User workers can have bindings scoped to their tenant:
// User worker bindings (set at deployment time){ bindings: [ { type: "kv", name: "TENANT_KV", namespace_id: tenantKvId }, { type: "d1", name: "TENANT_DB", database_id: tenantD1Id }, { type: "secret_text", name: "API_KEY", text: encryptedApiKey } ]}z0 Pattern: User workers get read-only access to their tenant’s Config and Facts. Write operations go through the dispatch worker to enforce invariants.
Dispatch Worker
Section titled “Dispatch Worker”The dispatch worker is the entry point. It handles routing, authentication, and metering.
Basic Dispatch Pattern
Section titled “Basic Dispatch Pattern”export default { async fetch(request, env) { // 1. Authenticate and resolve tenant const tenant_id = await resolveTenant(request, env); if (!tenant_id) { return new Response("Unauthorized", { status: 401 }); }
// 2. Check tenant status const tenant = await env.TENANT_DO.get( env.TENANT_DO.idFromName(tenant_id) ).fetch("/status");
if (tenant.status !== "active") { return new Response("Tenant suspended", { status: 403 }); }
// 3. Get user worker from namespace const userWorker = env.TENANT_WORKERS.get(tenant_id); if (!userWorker) { return new Response("Tenant not deployed", { status: 404 }); }
// 4. Dispatch request (metering happens automatically) return userWorker.fetch(request); }};Passing Context to User Workers
Section titled “Passing Context to User Workers”// Dispatch worker adds context headersconst userRequest = new Request(request.url, { method: request.method, headers: new Headers(request.headers), body: request.body});
userRequest.headers.set("x-z0-tenant-id", tenant_id);userRequest.headers.set("x-z0-request-id", crypto.randomUUID());userRequest.headers.set("x-z0-trace-id", traceId);
return userWorker.fetch(userRequest);Security Isolation Model
Section titled “Security Isolation Model”WfP provides multiple isolation boundaries:
1. Memory Isolation
Section titled “1. Memory Isolation”Each user worker runs in its own V8 isolate. A tenant cannot access another tenant’s memory, variables, or state.
┌───────────────────────────────────────────────────────┐│ V8 Isolate: tenant_a ││ - Own memory space ││ - Own global scope ││ - Cannot access tenant_b's data │└───────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────┐│ V8 Isolate: tenant_b ││ - Completely separate ││ - No shared state │└───────────────────────────────────────────────────────┘2. Binding Isolation
Section titled “2. Binding Isolation”Each user worker gets tenant-specific bindings. tenant_a cannot access tenant_b’s KV, D1, or secrets.
3. CPU Time Limits
Section titled “3. CPU Time Limits”Per-request CPU limits prevent one tenant from monopolizing resources:
| Limit | Default | z0 Configuration |
|---|---|---|
| CPU time per request | 50ms | 50ms (standard), 15min (Workflows) |
| Subrequest limit | 1000 | 1000 |
| Memory | 128MB | 128MB |
4. Network Isolation
Section titled “4. Network Isolation”User workers can only make outbound requests. They cannot:
- Open arbitrary ports
- Access other workers directly (must go through dispatch)
- Access internal Cloudflare infrastructure
z0 Additional Isolation
Section titled “z0 Additional Isolation”On top of WfP isolation, z0 adds:
| Layer | Enforcement | Purpose |
|---|---|---|
| tenant_id on Facts | DO ledger append | Prevent cross-tenant writes |
| Config scope | Config lookup | Tenant sees only their Configs |
| D1 sharding | Database per tenant | Query isolation |
Native Metering and Billing
Section titled “Native Metering and Billing”WfP provides usage data that flows directly into z0’s economic model.
What WfP Meters
Section titled “What WfP Meters”| Metric | Granularity | z0 Usage |
|---|---|---|
| Requests | Per user worker | Invocation Facts |
| CPU time | Per request | Cost calculation |
| Duration | Per request | Latency tracking |
| Subrequests | Per request | External call costing |
Accessing Usage Data
Section titled “Accessing Usage Data”// Cloudflare API: Get usage for a user workerconst usage = await api.workers.scripts.usage({ account_id: ACCOUNT_ID, dispatch_namespace: "z0-prod-tenants", script_name: tenant_id, start: startDate, end: endDate});
// Returns:{ requests: 1_234_567, cpu_time_ms: 45_678, duration_ms: 123_456, subrequests: 234_567}Mapping to z0 Facts
Section titled “Mapping to z0 Facts”Platform infrastructure costs are tracked differently from tool invocations. Workers for Platforms is infrastructure, not a Tool entity. Tools are external capabilities (Twilio, OpenAI, etc.) that agents invoke—infrastructure costs like compute are platform overhead.
// WfP usage becomes z0 platform cost FactsFact { type: "cost", subtype: "platform_compute", tenant_id: tenant_id, // NO tool_id - this is infrastructure cost, not tool invocation from_entity: tenant_account_id, to_entity: "platform_z0", // Platform account amount: calculateCost(cpu_time_ms, requests), currency: "USD", data: { compute_provider: "cloudflare_wfp", // metadata, not entity cpu_time_ms: usage.cpu_time_ms, requests: usage.requests, period_start: startDate, period_end: endDate }}Billing Integration
Section titled “Billing Integration”WfP Usage → z0 platform cost Facts → Tenant charges → Invoice
1. WfP tracks compute usage per tenant2. z0 records platform cost Facts (subtype: platform_compute)3. Pricing Config determines markup4. Charges flow to tenant invoicesWhen to Use WfP vs Regular Workers
Section titled “When to Use WfP vs Regular Workers”| Scenario | Use | Reason |
|---|---|---|
| Tenant-specific business logic | WfP | Isolation + metering |
| Tenant-specific webhooks | WfP | Custom code per tenant |
| Shared z0 infrastructure | Regular Workers | No tenant variation |
| RTB routing logic | Regular Workers | Same logic for all tenants |
| Tenant integrations (CRM, etc.) | WfP | Tenant-specific credentials |
| Analytics aggregation | Regular Workers | Operates across tenants |
| Dispatch/gateway | Regular Workers | Routing layer, not tenant code |
Decision Tree
Section titled “Decision Tree”Does the code vary per tenant?├── No → Regular Worker└── Yes → Does it need tenant secrets/credentials? ├── No → Regular Worker with tenant_id parameter └── Yes → WfP User Workerz0 Guideline: Use WfP when tenants need to run their own code or have tenant-specific integrations. Use regular Workers for shared z0 infrastructure.
Deployment Patterns
Section titled “Deployment Patterns”Pattern 1: Dispatch-Only
Section titled “Pattern 1: Dispatch-Only”All tenant logic lives in the dispatch worker. User workers are placeholders for metering.
Dispatch Worker├── All business logic├── Tenant resolution└── Calls user worker for metering onlyUse when: Tenants have identical logic, but you need per-tenant metering.
Pattern 2: Full Delegation
Section titled “Pattern 2: Full Delegation”Dispatch worker handles auth/routing. User workers handle all business logic.
Dispatch Worker├── Auth├── Tenant resolution└── Delegates everything to user worker
User Worker├── Business logic├── External integrations└── Tenant-specific behaviorUse when: Tenants have significantly different logic or integrations.
Pattern 3: Hybrid (z0 Recommended)
Section titled “Pattern 3: Hybrid (z0 Recommended)”Dispatch worker handles core z0 logic. User workers extend with tenant-specific behavior.
Dispatch Worker├── Auth├── Tenant resolution├── Core z0 logic (Facts, Configs)└── Calls user worker for tenant-specific extensions
User Worker├── Webhook transformations├── CRM-specific logic└── Custom qualification rulesUse when: You want standard z0 behavior with tenant customization points.
Observability
Section titled “Observability”Metrics to Track
Section titled “Metrics to Track”z0_wfp_dispatches_total{tenant_id, script_name, status}z0_wfp_dispatch_duration_ms{tenant_id}z0_wfp_user_worker_cpu_ms{tenant_id}z0_wfp_user_worker_errors_total{tenant_id, error_type}z0_wfp_deployments_total{tenant_id, status}Tracing Across Dispatch
Section titled “Tracing Across Dispatch”// Dispatch worker creates parent spanconst span = tracer.startSpan("z0.wfp.dispatch", { attributes: { tenant_id, script_name, request_path: request.url }});
// Pass trace context to user workerrequest.headers.set("x-z0-trace-id", span.traceId);request.headers.set("x-z0-span-id", span.spanId);
const response = await userWorker.fetch(request);span.end();Alerting
Section titled “Alerting”| Condition | Alert Level | Action |
|---|---|---|
| User worker deploy failed | Warning | Check deployment pipeline |
| User worker error rate > 5% | Critical | Page on-call |
| Dispatch latency p99 > 500ms | Warning | Review user worker performance |
| CPU limit exceeded | Warning | Optimize user worker or increase limit |
Implementation Checklist
Section titled “Implementation Checklist”Before using WfP for a tenant feature:
- Dispatch namespace created per environment
- Dispatch worker deployed with namespace binding
- Tenant resolution logic implemented
- Script naming convention documented
- User worker deployment pipeline ready
- Tenant-scoped bindings (KV, D1, secrets) configured
- Metering → cost Fact pipeline implemented
- trace_id propagation from dispatch to user workers
- Observability metrics and alerts configured
- Tenant suspension logic in dispatch worker
Summary
Section titled “Summary”| Question | Answer |
|---|---|
| What is WfP? | Multi-tenant worker deployment with isolation and metering |
| Why use it? | Tenant isolation + native usage tracking + per-tenant code |
| When to use? | Tenant-specific logic, credentials, or customization |
| How does z0 use it? | Dispatch worker + user workers per tenant |
| How does billing work? | WfP usage → cost Facts → charges → invoices |
Workers for Platforms turns Cloudflare Workers into a SaaS platform primitive. z0 uses it to provide tenant isolation while maintaining a single deployment pipeline and unified economic model.