User Script Dispatcher
Workers for Platforms-style dispatch via Dispatch Namespace or KV-stored handlers - multi-tenant user Worker routing
Workers for Platforms style dispatch - register customers and run tenant response handlers via a DispatchNamespace binding, with a KV fallback for local demos. Stored scripts return a fixed JSON response (safe demo - no dynamic JS eval).
API Reference
POST /scripts
Store a JSON response handler.
name string (required)
Tenant/script name (a-zA-Z0-9_-).
response object (required)
JSON object returned on KV dispatch. Max serialized size: 10,000 bytes.
Example Request
curl -X POST "https://your-worker.workers.dev/scripts" \
-H "Content-Type: application/json" \
-d '{"name":"tenant-a","response":{"hello":"world"}}'GET /scripts
name string (required)
Returns { name, updatedAt, hasResponse }.
Example Request
curl "https://your-worker.workers.dev/scripts?name=tenant-a"POST /dispatch/:name
- If
DISPATCHERis bound: forwards the request toenv.DISPATCHER.get(name).fetch(request). - If
DISPATCHER.getthrows:{ error, code: "DISPATCH_ERROR" }(502). - Without
DISPATCHER(or when using the KV path): returns the stored KVresponseobject.
Example Request
curl -X POST "https://your-worker.workers.dev/dispatch/tenant-a"POST /register
Record a customer name in KV for listing.
name string (required)
Example Request
curl -X POST "https://your-worker.workers.dev/register" \
-H "Content-Type: application/json" \
-d '{"name":"tenant-a"}'GET /customers
Returns { customers: string[] }.
Example Request
curl "https://your-worker.workers.dev/customers"Error Codes
400- Invalid name, response, body, or oversized script (INVALID_NAME,INVALID_RESPONSE,INVALID_BODY,SCRIPT_TOO_LARGE)404- Script not found (NOT_FOUND)502- Dispatch namespace failure (DISPATCH_ERROR)
Use Cases
- Learn Workers for Platforms dispatch namespace patterns
- Prototype multi-tenant routing with a KV fallback
- Register and list customers before wiring real scripts
- Teach safe demos that avoid eval while showing dispatch shape
Limitations
- KV path returns a stored JSON object - no dynamic JS execution
- Dispatch namespace (
demo-customers) may require Workers for Platforms - Script payload capped at 10,000 bytes
- No authentication on register/dispatch endpoints
Use in your project
Copy these files into an existing Worker. Prefer Deployment to try the full experiment first. Source: apps/experiments/user-script-dispatcher.
import { CUSTOMER_KEY_PREFIX, MAX_NAME_LENGTH, MAX_SCRIPT_BYTES, NAME_PATTERN, SCRIPT_KEY_PREFIX,} from "../constants/defaults";import type { ScriptRecord } from "../types/script";export function validateName(input: string | undefined): string | null { if (!input || typeof input !== "string") return null; const trimmed = input.trim(); if (!trimmed || trimmed.length > MAX_NAME_LENGTH) return null; if (!NAME_PATTERN.test(trimmed)) return null; return trimmed;}export function validateResponse(input: unknown): Record<string, unknown> | null { if (!input || typeof input !== "object" || Array.isArray(input)) return null; return input as Record<string, unknown>;}export function scriptKey(name: string): string { return `${SCRIPT_KEY_PREFIX}${name}`;}export function customerKey(name: string): string { return `${CUSTOMER_KEY_PREFIX}${name}`;}export function serializeScript(name: string, response: Record<string, unknown>): string | null { const record: ScriptRecord = { name, response, updatedAt: new Date().toISOString(), }; const raw = JSON.stringify(record); if (raw.length > MAX_SCRIPT_BYTES) return null; return raw;}export function parseScriptRecord(raw: string): ScriptRecord | null { try { const parsed = JSON.parse(raw) as Partial<ScriptRecord>; if (!parsed.name || typeof parsed.name !== "string") return null; if (!parsed.response || typeof parsed.response !== "object") return null; return { name: parsed.name, response: parsed.response as Record<string, unknown>, updatedAt: parsed.updatedAt ?? new Date(0).toISOString(), }; } catch { return null; }}export async function saveScript( kv: KVNamespace, name: string, response: Record<string, unknown>): Promise<ScriptRecord | null> { const raw = serializeScript(name, response); if (!raw) return null; const record = parseScriptRecord(raw); if (!record) return null; await kv.put(scriptKey(name), raw); return record;}export async function getScript(kv: KVNamespace, name: string): Promise<ScriptRecord | null> { const raw = await kv.get(scriptKey(name)); if (!raw) return null; return parseScriptRecord(raw);}export async function registerCustomer(kv: KVNamespace, name: string): Promise<void> { await kv.put(customerKey(name), JSON.stringify({ name, registeredAt: new Date().toISOString() }));}export async function listCustomers(kv: KVNamespace): Promise<string[]> { const listed = await kv.list({ prefix: CUSTOMER_KEY_PREFIX }); return listed.keys .map((k) => k.name.slice(CUSTOMER_KEY_PREFIX.length)) .filter(Boolean) .sort();}Deployment
Configure KV and dispatch namespace
Create a KV namespace bound as SCRIPTS. For production dispatch, create a dispatch namespace matching demo-customers in wrangler.json. Local/KV fallback works without the paid feature.
Test your deployment
curl -X POST "https://your-worker.workers.dev/scripts" \
-H "Content-Type: application/json" \
-d '{"name":"tenant-a","response":{"hello":"world"}}'
curl -X POST "https://your-worker.workers.dev/dispatch/tenant-a"Local Development
cd apps/experiments/user-script-dispatcher
npm install
npm run devcurl -X POST "http://localhost:8787/scripts" \
-H "Content-Type: application/json" \
-d '{"name":"tenant-a","response":{"hello":"world"}}'
curl -X POST "http://localhost:8787/dispatch/tenant-a"Configuration
wrangler.json declares:
- KV namespace
SCRIPTS - Dispatch namespace
DISPATCHER→demo-customers
Cloudflare Features Used
- Workers - Edge compute runtime
- Workers KV - Script metadata and KV fallback responses
- Workers for Platforms - Optional
dispatch_namespacesbinding