mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 18:11:27 -07:00
update skills
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# Cloudflare Workers for Platforms
|
||||
|
||||
Multi-tenant platform with isolated customer code execution at scale.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Multi-tenant SaaS running customer code
|
||||
- AI-generated code execution in secure sandboxes
|
||||
- Programmable platforms with isolated compute
|
||||
- Edge functions/serverless platforms
|
||||
- Website builders with static + dynamic content
|
||||
- Unlimited app deployment at scale
|
||||
|
||||
**NOT for general Workers** - only for Workers for Platforms architecture.
|
||||
|
||||
## Quick Start
|
||||
|
||||
**One-click deploy:** [Platform Starter Kit](https://github.com/cloudflare/workers-for-platforms-example) deploys complete WfP setup with dispatch namespace, dispatch worker, and user worker example.
|
||||
|
||||
[](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/workers-for-platforms-example)
|
||||
|
||||
**Manual setup:** See [configuration.md](./configuration.md) for namespace creation and dispatch worker configuration.
|
||||
|
||||
## Key Features
|
||||
|
||||
- Unlimited Workers per namespace (no script limits)
|
||||
- Automatic tenant isolation
|
||||
- Custom CPU/subrequest limits per customer
|
||||
- Hostname routing (subdomains/vanity domains)
|
||||
- Egress/ingress control
|
||||
- Static assets support
|
||||
- Tags for bulk operations
|
||||
|
||||
## Architecture
|
||||
|
||||
**4 Components:**
|
||||
1. **Dispatch Namespace** - Container for unlimited customer Workers, automatic isolation (untrusted mode by default - no request.cf access, no shared cache)
|
||||
2. **Dynamic Dispatch Worker** - Entry point, routes requests, enforces platform logic (auth, limits, validation)
|
||||
3. **User Workers** - Customer code in isolated sandboxes, API-deployed, optional bindings (KV/D1/R2/DO)
|
||||
4. **Outbound Worker** (optional) - Intercepts external fetch, controls egress, logs subrequests (blocks TCP socket connect() API)
|
||||
|
||||
**Request Flow:**
|
||||
```
|
||||
Request → Dispatch Worker → Determines user Worker → env.DISPATCHER.get("customer")
|
||||
→ User Worker executes (Outbound Worker for external fetch) → Response → Dispatch Worker → Client
|
||||
```
|
||||
|
||||
## Decision Trees
|
||||
|
||||
### When to Use Workers for Platforms
|
||||
```
|
||||
Need to run code?
|
||||
├─ Your code only → Regular Workers
|
||||
├─ Customer/AI code → Workers for Platforms
|
||||
└─ Untrusted code in sandbox → Workers for Platforms OR Sandbox API
|
||||
```
|
||||
|
||||
### Routing Strategy Selection
|
||||
```
|
||||
Hostname routing needed?
|
||||
├─ Subdomains only (*.saas.com) → `*.saas.com/*` route + subdomain extraction
|
||||
├─ Custom domains → `*/*` wildcard + Cloudflare for SaaS + KV/metadata routing
|
||||
└─ Path-based (/customer/app) → Any route + path parsing
|
||||
```
|
||||
|
||||
### Isolation Mode Selection
|
||||
```
|
||||
Worker mode?
|
||||
├─ Running customer code → Untrusted (default)
|
||||
├─ Need request.cf geolocation → Trusted mode
|
||||
├─ Internal platform, controlled code → Trusted mode with cache key prefixes
|
||||
└─ Maximum isolation → Untrusted + unique resources per customer
|
||||
```
|
||||
|
||||
## In This Reference
|
||||
|
||||
| File | Purpose | When to Read |
|
||||
|------|---------|--------------|
|
||||
| [configuration.md](./configuration.md) | Namespace setup, dispatch worker config | First-time setup, changing limits |
|
||||
| [api.md](./api.md) | User worker API, dispatch API, outbound worker | Deploying workers, SDK integration |
|
||||
| [patterns.md](./patterns.md) | Multi-tenancy, routing, egress control | Planning architecture, scaling |
|
||||
| [gotchas.md](./gotchas.md) | Limits, isolation issues, best practices | Debugging, production prep |
|
||||
|
||||
## See Also
|
||||
- [workers](../workers/) - Core Workers runtime documentation
|
||||
- [durable-objects](../durable-objects/) - Stateful multi-tenant patterns
|
||||
- [sandbox](../sandbox/) - Alternative for untrusted code execution
|
||||
- [Reference Architecture: Programmable Platforms](https://developers.cloudflare.com/reference-architecture/diagrams/serverless/programmable-platforms/)
|
||||
- [Reference Architecture: AI Vibe Coding Platform](https://developers.cloudflare.com/reference-architecture/diagrams/ai/ai-vibe-coding-platform/)
|
||||
@@ -0,0 +1,196 @@
|
||||
# API Operations
|
||||
|
||||
## Deploy User Worker
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/dispatch/namespaces/$NAMESPACE/scripts/$SCRIPT_NAME" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-F 'metadata={"main_module": "worker.mjs"};type=application/json' \
|
||||
-F 'worker.mjs=@worker.mjs;type=application/javascript+module'
|
||||
```
|
||||
|
||||
### TypeScript SDK
|
||||
```typescript
|
||||
import Cloudflare from "cloudflare";
|
||||
|
||||
const client = new Cloudflare({ apiToken: process.env.API_TOKEN });
|
||||
|
||||
const scriptFile = new File([scriptContent], `${scriptName}.mjs`, {
|
||||
type: "application/javascript+module",
|
||||
});
|
||||
|
||||
await client.workersForPlatforms.dispatch.namespaces.scripts.update(
|
||||
namespace, scriptName,
|
||||
{
|
||||
account_id: accountId,
|
||||
metadata: { main_module: `${scriptName}.mjs` },
|
||||
files: [scriptFile],
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## TypeScript Types
|
||||
|
||||
```typescript
|
||||
import type { DispatchNamespace } from '@cloudflare/workers-types';
|
||||
|
||||
interface DispatchNamespace {
|
||||
get(name: string, options?: Record<string, unknown>, dispatchOptions?: DynamicDispatchOptions): Fetcher;
|
||||
}
|
||||
|
||||
interface DynamicDispatchOptions {
|
||||
limits?: DynamicDispatchLimits;
|
||||
outbound?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface DynamicDispatchLimits {
|
||||
cpuMs?: number; // Max CPU milliseconds
|
||||
subRequests?: number; // Max fetch() calls
|
||||
}
|
||||
|
||||
// Usage
|
||||
const userWorker = env.DISPATCHER.get('customer-123', {}, {
|
||||
limits: { cpuMs: 50, subRequests: 20 },
|
||||
outbound: { customerId: '123', url: request.url }
|
||||
});
|
||||
```
|
||||
|
||||
## Deploy with Bindings
|
||||
```bash
|
||||
curl -X PUT ".../scripts/$SCRIPT_NAME" \
|
||||
-F 'metadata={
|
||||
"main_module": "worker.mjs",
|
||||
"bindings": [
|
||||
{"type": "kv_namespace", "name": "MY_KV", "namespace_id": "'$KV_ID'"}
|
||||
],
|
||||
"tags": ["customer-123", "production"],
|
||||
"compatibility_date": "2026-01-01" // Use current date for new projects
|
||||
};type=application/json' \
|
||||
-F 'worker.mjs=@worker.mjs;type=application/javascript+module'
|
||||
```
|
||||
|
||||
## List/Delete Workers
|
||||
|
||||
```bash
|
||||
# List
|
||||
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/dispatch/namespaces/$NAMESPACE/scripts" \
|
||||
-H "Authorization: Bearer $API_TOKEN"
|
||||
|
||||
# Delete by name
|
||||
curl -X DELETE ".../scripts/$SCRIPT_NAME" -H "Authorization: Bearer $API_TOKEN"
|
||||
|
||||
# Delete by tag
|
||||
curl -X DELETE ".../scripts?tags=customer-123%3Ayes" -H "Authorization: Bearer $API_TOKEN"
|
||||
```
|
||||
|
||||
**Pagination:** SDK supports async iteration. Manual: add `?per_page=100&page=1` query params.
|
||||
|
||||
## Static Assets
|
||||
|
||||
**3-step process:** Create session → Upload files → Deploy Worker
|
||||
|
||||
### 1. Create Upload Session
|
||||
```bash
|
||||
curl -X POST ".../scripts/$SCRIPT_NAME/assets-upload-session" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-d '{
|
||||
"manifest": {
|
||||
"/index.html": {"hash": "08f1dfda4574284ab3c21666d1ee8c7d4", "size": 1234}
|
||||
}
|
||||
}'
|
||||
# Returns: jwt, buckets
|
||||
```
|
||||
|
||||
**Hash:** SHA-256 truncated to first 16 bytes (32 hex characters)
|
||||
|
||||
### 2. Upload Files
|
||||
```bash
|
||||
curl -X POST ".../workers/assets/upload?base64=true" \
|
||||
-H "Authorization: Bearer $UPLOAD_JWT" \
|
||||
-F '08f1dfda4574284ab3c21666d1ee8c7d4=<BASE64_CONTENT>'
|
||||
# Returns: completion jwt
|
||||
```
|
||||
|
||||
**Multiple buckets:** Upload to all returned bucket URLs (typically 2 for redundancy) using same JWT and hash.
|
||||
|
||||
### 3. Deploy with Assets
|
||||
```bash
|
||||
curl -X PUT ".../scripts/$SCRIPT_NAME" \
|
||||
-F 'metadata={
|
||||
"main_module": "index.js",
|
||||
"assets": {"jwt": "<COMPLETION_TOKEN>"},
|
||||
"bindings": [{"type": "assets", "name": "ASSETS"}]
|
||||
};type=application/json' \
|
||||
-F 'index.js=export default {...};type=application/javascript+module'
|
||||
```
|
||||
|
||||
**Asset Isolation:** Assets shared across namespace by default. For customer isolation, salt hash: `sha256(customerId + fileContents).slice(0, 32)`
|
||||
|
||||
## Dispatch Workers
|
||||
|
||||
### Subdomain Routing
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const userWorkerName = new URL(request.url).hostname.split(".")[0];
|
||||
const userWorker = env.DISPATCHER.get(userWorkerName);
|
||||
return await userWorker.fetch(request);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Path Routing
|
||||
```typescript
|
||||
const pathParts = new URL(request.url).pathname.split("/").filter(Boolean);
|
||||
const userWorker = env.DISPATCHER.get(pathParts[0]);
|
||||
return await userWorker.fetch(request);
|
||||
```
|
||||
|
||||
### KV Routing
|
||||
```typescript
|
||||
const hostname = new URL(request.url).hostname;
|
||||
const userWorkerName = await env.ROUTING_KV.get(hostname);
|
||||
const userWorker = env.DISPATCHER.get(userWorkerName);
|
||||
return await userWorker.fetch(request);
|
||||
```
|
||||
|
||||
## Outbound Workers
|
||||
|
||||
Control external fetch from user Workers:
|
||||
|
||||
### Configure
|
||||
```typescript
|
||||
const userWorker = env.DISPATCHER.get(
|
||||
workerName, {},
|
||||
{ outbound: { customer_context: { customer_name: workerName, url: request.url } } }
|
||||
);
|
||||
```
|
||||
|
||||
### Implement
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const customerName = env.customer_name;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Block domains
|
||||
if (["malicious.com"].some(d => url.hostname.includes(d))) {
|
||||
return new Response("Blocked", { status: 403 });
|
||||
}
|
||||
|
||||
// Inject auth
|
||||
if (url.hostname === "api.example.com") {
|
||||
const headers = new Headers(request.headers);
|
||||
headers.set("Authorization", `Bearer ${generateJWT(customerName)}`);
|
||||
return fetch(new Request(request, { headers }));
|
||||
}
|
||||
|
||||
return fetch(request);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Note:** Doesn't intercept DO/mTLS fetch.
|
||||
|
||||
See [README.md](./README.md), [configuration.md](./configuration.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md)
|
||||
@@ -0,0 +1,167 @@
|
||||
# Configuration
|
||||
|
||||
## Dispatch Namespace Binding
|
||||
|
||||
### wrangler.jsonc
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"dispatch_namespaces": [{
|
||||
"binding": "DISPATCHER",
|
||||
"namespace": "production"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## Worker Isolation Mode
|
||||
|
||||
Workers in a namespace run in **untrusted mode** by default for security:
|
||||
- No access to `request.cf` object
|
||||
- Isolated cache per Worker (no shared cache)
|
||||
- `caches.default` disabled
|
||||
|
||||
### Enable Trusted Mode
|
||||
|
||||
For internal platforms where you control all code:
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/dispatch/namespaces/$NAMESPACE" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-d '{"name": "'$NAMESPACE'", "trusted_workers": true}'
|
||||
```
|
||||
|
||||
**Caveats:**
|
||||
- Workers share cache within namespace (use cache key prefixes: `customer-${id}:${key}`)
|
||||
- `request.cf` object accessible
|
||||
- Redeploy existing Workers after enabling trusted mode
|
||||
|
||||
**When to use:** Internal platforms, A/B testing platforms, need geolocation data
|
||||
|
||||
|
||||
### With Outbound Worker
|
||||
```jsonc
|
||||
{
|
||||
"dispatch_namespaces": [{
|
||||
"binding": "DISPATCHER",
|
||||
"namespace": "production",
|
||||
"outbound": {
|
||||
"service": "outbound-worker",
|
||||
"parameters": ["customer_context"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## Wrangler Commands
|
||||
|
||||
```bash
|
||||
wrangler dispatch-namespace list
|
||||
wrangler dispatch-namespace get production
|
||||
wrangler dispatch-namespace create production
|
||||
wrangler dispatch-namespace delete staging
|
||||
wrangler dispatch-namespace rename old new
|
||||
```
|
||||
|
||||
## Custom Limits
|
||||
|
||||
Set CPU time and subrequest limits per invocation:
|
||||
|
||||
```typescript
|
||||
const userWorker = env.DISPATCHER.get(
|
||||
workerName,
|
||||
{},
|
||||
{
|
||||
limits: {
|
||||
cpuMs: 10, // Max CPU ms
|
||||
subRequests: 5 // Max fetch() calls
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Handle limit violations:
|
||||
```typescript
|
||||
try {
|
||||
return await userWorker.fetch(request);
|
||||
} catch (e) {
|
||||
if (e.message.includes("CPU time limit")) {
|
||||
return new Response("CPU limit exceeded", { status: 429 });
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
```
|
||||
|
||||
## Static Assets
|
||||
|
||||
Deploy HTML/CSS/images with Workers. See [api.md](./api.md#static-assets) for upload process.
|
||||
|
||||
### Wrangler
|
||||
```jsonc
|
||||
{
|
||||
"name": "customer-site",
|
||||
"main": "./src/index.js",
|
||||
"assets": {
|
||||
"directory": "./public",
|
||||
"binding": "ASSETS"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
npx wrangler deploy --name customer-site --dispatch-namespace production
|
||||
```
|
||||
|
||||
### Dashboard Deployment
|
||||
|
||||
Alternative to CLI:
|
||||
|
||||
1. Upload Worker file in dashboard
|
||||
2. Add `--dispatch-namespace` flag: `wrangler deploy --dispatch-namespace production`
|
||||
3. Or configure in wrangler.jsonc under `dispatch_namespaces`
|
||||
|
||||
See [api.md](./api.md) for programmatic deployment via REST API or SDK.
|
||||
|
||||
## Tags
|
||||
|
||||
Organize/search Workers (max 8/script):
|
||||
|
||||
```bash
|
||||
# Set tags
|
||||
curl -X PUT ".../tags" -d '["customer-123", "pro", "production"]'
|
||||
|
||||
# Filter by tag
|
||||
curl ".../scripts?tags=production%3Ayes"
|
||||
|
||||
# Delete by tag
|
||||
curl -X DELETE ".../scripts?tags=customer-123%3Ayes"
|
||||
```
|
||||
|
||||
Common patterns: `customer-123`, `free|pro|enterprise`, `production|staging`
|
||||
|
||||
## Bindings
|
||||
|
||||
**Supported binding types:** 29 total including KV, D1, R2, Durable Objects, Analytics Engine, Service, Assets, Queue, Vectorize, Hyperdrive, Workflow, AI, Browser, and more.
|
||||
|
||||
Add via API metadata (see [api.md](./api.md#deploy-with-bindings)):
|
||||
```json
|
||||
{
|
||||
"bindings": [
|
||||
{"type": "kv_namespace", "name": "USER_KV", "namespace_id": "..."},
|
||||
{"type": "r2_bucket", "name": "STORAGE", "bucket_name": "..."},
|
||||
{"type": "d1", "name": "DB", "id": "..."}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Preserve existing bindings:
|
||||
```json
|
||||
{
|
||||
"bindings": [{"type": "r2_bucket", "name": "STORAGE", "bucket_name": "new"}],
|
||||
"keep_bindings": ["kv_namespace", "d1"] // Preserves existing bindings of these types
|
||||
}
|
||||
```
|
||||
|
||||
For complete binding type reference, see [bindings](../bindings/) documentation
|
||||
|
||||
See [README.md](./README.md), [api.md](./api.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md)
|
||||
@@ -0,0 +1,134 @@
|
||||
# Gotchas & Limits
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Worker not found"
|
||||
|
||||
**Cause:** Attempting to get Worker that doesn't exist in namespace
|
||||
**Solution:** Catch error and return 404:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const userWorker = env.DISPATCHER.get(workerName);
|
||||
return userWorker.fetch(request);
|
||||
} catch (e) {
|
||||
if (e.message.startsWith("Worker not found")) {
|
||||
return new Response("Worker not found", { status: 404 });
|
||||
}
|
||||
throw e; // Re-throw unexpected errors
|
||||
}
|
||||
```
|
||||
|
||||
### "CPU time limit exceeded"
|
||||
|
||||
**Cause:** User Worker exceeded configured CPU time limit
|
||||
**Solution:** Track violations in Analytics Engine and return 429 response; consider adjusting limits per customer tier
|
||||
|
||||
### "Hostname Routing Issues"
|
||||
|
||||
**Cause:** DNS proxy settings causing routing problems
|
||||
**Solution:** Use `*/*` wildcard route which works regardless of proxy settings for orange-to-orange routing
|
||||
|
||||
### "Bindings Lost on Update"
|
||||
|
||||
**Cause:** Not using `keep_bindings` flag when updating Worker
|
||||
**Solution:** Use `keep_bindings: true` in API requests to preserve existing bindings during updates
|
||||
|
||||
### "Tag Filtering Not Working"
|
||||
|
||||
**Cause:** Special characters not URL encoded in tag filters
|
||||
**Solution:** URL encode tags (e.g., `tags=production%3Ayes`) and avoid special chars like `,` and `&`
|
||||
|
||||
### "Deploy Failures with ES Modules"
|
||||
|
||||
**Cause:** Incorrect upload format for ES modules
|
||||
**Solution:** Use multipart form upload, specify `main_module` in metadata, and set file type to `application/javascript+module`
|
||||
|
||||
### "Static Asset Upload Failed"
|
||||
|
||||
**Cause:** Invalid hash format, expired token, or incorrect encoding
|
||||
**Solution:** Hash must be first 16 bytes (32 hex chars) of SHA-256, upload within 1 hour of session creation, deploy within 1 hour of upload completion, and Base64 encode file contents
|
||||
|
||||
### "Outbound Worker Not Intercepting Calls"
|
||||
|
||||
**Cause:** Outbound Workers don't intercept Durable Object or mTLS binding fetch
|
||||
**Solution:** Plan egress control accordingly; not all fetch calls are intercepted
|
||||
|
||||
### "TCP Socket Connection Failed"
|
||||
|
||||
**Cause:** Outbound Worker enabled blocks `connect()` API for TCP sockets
|
||||
**Solution:** Outbound Workers only intercept `fetch()` calls; TCP socket connections unavailable when outbound configured. Remove outbound if TCP needed, or use proxy pattern.
|
||||
|
||||
### "API Rate Limit Exceeded"
|
||||
|
||||
**Cause:** Exceeded Cloudflare API rate limits (1200 requests per 5 minutes per account, 200 requests per second per IP)
|
||||
**Solution:** Implement exponential backoff:
|
||||
|
||||
```typescript
|
||||
async function deployWithBackoff(deploy: () => Promise<void>, maxRetries = 3) {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await deploy();
|
||||
} catch (e) {
|
||||
if (e.status === 429 && i < maxRetries - 1) {
|
||||
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### "Gradual Deployment Not Supported"
|
||||
|
||||
**Cause:** Attempted to use gradual deployments with user Workers
|
||||
**Solution:** Gradual deployments not supported for Workers in dispatch namespaces. Use all-at-once deployment with staged rollout via dispatch worker logic (feature flags, percentage-based routing).
|
||||
|
||||
### "Asset Session Expired"
|
||||
|
||||
**Cause:** Upload JWT expired (1 hour validity) or completion token expired (1 hour after upload)
|
||||
**Solution:** Complete asset upload within 1 hour of session creation, and deploy Worker within 1 hour of upload completion. For large uploads, batch files or increase upload parallelism.
|
||||
|
||||
## Platform Limits
|
||||
|
||||
| Limit | Value | Notes |
|
||||
|-------|-------|-------|
|
||||
| Workers per namespace | Unlimited | Unlike regular Workers (500 per account) |
|
||||
| Namespaces per account | Unlimited | Best practice: 1 production + 1 staging |
|
||||
| Max tags per Worker | 8 | For filtering and organization |
|
||||
| Worker mode | Untrusted (default) | No `request.cf` access unless trusted mode |
|
||||
| Cache isolation | Per-Worker (untrusted) | Shared in trusted mode with key prefixes |
|
||||
| Durable Object namespaces | Unlimited | No per-account limit for WfP |
|
||||
| Gradual Deployments | Not supported | All-at-once only |
|
||||
| `caches.default` | Disabled (untrusted) | Use Cache API with custom keys |
|
||||
|
||||
## Asset Upload Limits
|
||||
|
||||
| Limit | Value | Notes |
|
||||
|-------|-------|-------|
|
||||
| Upload session JWT validity | 1 hour | Must complete upload within this time |
|
||||
| Completion token validity | 1 hour | Must deploy within this time after upload |
|
||||
| Asset hash format | First 16 bytes SHA-256 | 32 hex characters |
|
||||
| Base64 encoding | Required | For binary files |
|
||||
|
||||
## API Rate Limits
|
||||
|
||||
| Limit Type | Value | Scope |
|
||||
|------------|-------|-------|
|
||||
| Client API | 1200 requests / 5 min | Per account |
|
||||
| Client API | 200 requests / sec | Per IP address |
|
||||
| GraphQL | Varies by query cost | Query complexity |
|
||||
|
||||
See [Cloudflare API Rate Limits](https://developers.cloudflare.com/fundamentals/api/reference/limits/) for details.
|
||||
|
||||
## Operational Limits
|
||||
|
||||
| Operation | Limit | Notes |
|
||||
|-----------|-------|-------|
|
||||
| CPU time (custom limits) | Up to Workers plan limit | Set per-invocation in dispatch worker |
|
||||
| Subrequests (custom limits) | Up to Workers plan limit | Set per-invocation in dispatch worker |
|
||||
| Outbound Worker subrequests | Not intercepted for DO/mTLS | Only regular fetch() calls |
|
||||
| TCP sockets with outbound | Disabled | `connect()` API unavailable |
|
||||
|
||||
See [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [patterns.md](./patterns.md)
|
||||
@@ -0,0 +1,188 @@
|
||||
# Multi-Tenant Patterns
|
||||
|
||||
## Billing by Plan
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
DISPATCHER: DispatchNamespace;
|
||||
CUSTOMERS_KV: KVNamespace;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const userWorkerName = new URL(request.url).hostname.split(".")[0];
|
||||
const customerPlan = await env.CUSTOMERS_KV.get(userWorkerName);
|
||||
|
||||
const plans = {
|
||||
enterprise: { cpuMs: 50, subRequests: 50 },
|
||||
pro: { cpuMs: 20, subRequests: 20 },
|
||||
free: { cpuMs: 10, subRequests: 5 },
|
||||
};
|
||||
const limits = plans[customerPlan as keyof typeof plans] || plans.free;
|
||||
|
||||
const userWorker = env.DISPATCHER.get(userWorkerName, {}, { limits });
|
||||
return await userWorker.fetch(request);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Resource Isolation
|
||||
|
||||
**Complete isolation:** Create unique resources per customer
|
||||
- KV namespace per customer
|
||||
- D1 database per customer
|
||||
- R2 bucket per customer
|
||||
|
||||
```typescript
|
||||
const bindings = [{
|
||||
type: "kv_namespace",
|
||||
name: "USER_KV",
|
||||
namespace_id: `customer-${customerId}-kv`
|
||||
}];
|
||||
```
|
||||
|
||||
## Hostname Routing
|
||||
|
||||
### Wildcard Route (Recommended)
|
||||
Configure `*/*` route on SaaS domain → dispatch Worker
|
||||
|
||||
**Benefits:**
|
||||
- Supports subdomains + custom vanity domains
|
||||
- No per-route limits (regular Workers limited to 100 routes)
|
||||
- Programmatic control
|
||||
- Works with any DNS proxy settings
|
||||
|
||||
**Setup:**
|
||||
1. Cloudflare for SaaS custom hostnames
|
||||
2. Fallback origin (dummy `A 192.0.2.0` if Worker is origin)
|
||||
3. DNS CNAME to SaaS domain
|
||||
4. `*/*` route → dispatch Worker
|
||||
5. Routing logic in dispatch Worker
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const hostname = new URL(request.url).hostname;
|
||||
const hostnameData = await env.ROUTING_KV.get(`hostname:${hostname}`, { type: "json" });
|
||||
|
||||
if (!hostnameData?.workerName) {
|
||||
return new Response("Hostname not configured", { status: 404 });
|
||||
}
|
||||
|
||||
const userWorker = env.DISPATCHER.get(hostnameData.workerName);
|
||||
return await userWorker.fetch(request);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Subdomain-Only
|
||||
1. Wildcard DNS: `*.saas.com` → origin
|
||||
2. Route: `*.saas.com/*` → dispatch Worker
|
||||
3. Extract subdomain for routing
|
||||
|
||||
### Orange-to-Orange (O2O) Behavior
|
||||
|
||||
When customers use Cloudflare and CNAME to your Workers domain:
|
||||
|
||||
| Scenario | Behavior | Route Pattern |
|
||||
|----------|----------|---------------|
|
||||
| Customer not on Cloudflare | Standard routing | `*/*` or `*.domain.com/*` |
|
||||
| Customer on Cloudflare (proxied CNAME) | Invokes Worker at edge | `*/*` required |
|
||||
| Customer on Cloudflare (DNS-only CNAME) | Standard routing | Any route works |
|
||||
|
||||
**Recommendation:** Always use `*/*` wildcard for consistent O2O behavior.
|
||||
|
||||
### Custom Metadata Routing
|
||||
|
||||
For Cloudflare for SaaS: Store worker name in custom hostname `custom_metadata`, retrieve in dispatch worker to route requests. Requires custom hostnames as subdomains of your domain.
|
||||
|
||||
## Observability
|
||||
|
||||
### Logpush
|
||||
- Enable on dispatch Worker → captures all user Worker logs
|
||||
- Filter by `Outcome` or `Script Name`
|
||||
|
||||
### Tail Workers
|
||||
- Real-time logs with custom formatting
|
||||
- Receives HTTP status, `console.log()`, exceptions, diagnostics
|
||||
|
||||
### Analytics Engine
|
||||
```typescript
|
||||
// Track violations
|
||||
env.ANALYTICS.writeDataPoint({
|
||||
indexes: [customerName],
|
||||
blobs: ["cpu_limit_exceeded"],
|
||||
});
|
||||
```
|
||||
|
||||
### GraphQL
|
||||
```graphql
|
||||
query {
|
||||
viewer {
|
||||
accounts(filter: {accountTag: $accountId}) {
|
||||
workersInvocationsAdaptive(filter: {dispatchNamespaceName: "production"}) {
|
||||
sum { requests errors cpuTime }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Use Case Implementations
|
||||
|
||||
### AI Code Execution
|
||||
```typescript
|
||||
async function deployGeneratedCode(name: string, code: string) {
|
||||
const file = new File([code], `${name}.mjs`, { type: "application/javascript+module" });
|
||||
await client.workersForPlatforms.dispatch.namespaces.scripts.update("production", name, {
|
||||
account_id: accountId,
|
||||
metadata: { main_module: `${name}.mjs`, tags: [name, "ai-generated"] },
|
||||
files: [file],
|
||||
});
|
||||
}
|
||||
|
||||
// Short limits for untrusted code
|
||||
const userWorker = env.DISPATCHER.get(sessionId, {}, { limits: { cpuMs: 5, subRequests: 3 } });
|
||||
```
|
||||
|
||||
**VibeSDK:** For AI-powered code generation + deployment platforms, see [VibeSDK](https://github.com/cloudflare/vibesdk) - handles AI generation, sandbox execution, live preview, and deployment.
|
||||
|
||||
Reference: [AI Vibe Coding Platform Architecture](https://developers.cloudflare.com/reference-architecture/diagrams/ai/ai-vibe-coding-platform/)
|
||||
|
||||
### Edge Functions Platform
|
||||
```typescript
|
||||
// Route: /customer-id/function-name
|
||||
const [customerId, functionName] = new URL(request.url).pathname.split("/").filter(Boolean);
|
||||
const workerName = `${customerId}-${functionName}`;
|
||||
const userWorker = env.DISPATCHER.get(workerName);
|
||||
```
|
||||
|
||||
### Website Builder
|
||||
- Deploy static assets + Worker code
|
||||
- See [api.md](./api.md#static-assets) for full implementation
|
||||
- Salt hashes for asset isolation
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Architecture
|
||||
- One namespace per environment (production, staging)
|
||||
- Platform logic in dispatch Worker (auth, rate limiting, validation)
|
||||
- Isolation automatic (no shared cache, untrusted mode)
|
||||
|
||||
### Routing
|
||||
- Use `*/*` wildcard routes
|
||||
- Store mappings in KV
|
||||
- Handle missing Workers gracefully
|
||||
|
||||
### Limits & Security
|
||||
- Set custom limits by plan
|
||||
- Track violations with Analytics Engine
|
||||
- Use outbound Workers for egress control
|
||||
- Sanitize responses
|
||||
|
||||
### Tags
|
||||
- Tag all Workers: customer ID, plan, environment
|
||||
- Enable bulk operations
|
||||
- Filter efficiently
|
||||
|
||||
See [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [gotchas.md](./gotchas.md)
|
||||
Reference in New Issue
Block a user