diff --git a/src/assets/images/cloudflare-for-platforms/tenant-architecture.png b/src/assets/images/cloudflare-for-platforms/tenant-architecture.png new file mode 100644 index 000000000000000..e2fe2ec14da325a Binary files /dev/null and b/src/assets/images/cloudflare-for-platforms/tenant-architecture.png differ diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/tenant/bindings.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/tenant/bindings.mdx new file mode 100644 index 000000000000000..fafc5084fe93fab --- /dev/null +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/tenant/bindings.mdx @@ -0,0 +1,86 @@ +--- +title: Tenant binding +pcx_content_type: how-to +sidebar: + order: 5 +--- + +The tenant binding eliminates the need to configure individual bindings for each resource your customers use. + +The tenant binding gives Workers dynamic access to all resources within their tenant. Instead of configuring individual bindings for each resource, a single `MY_RESOURCES` binding gives your Worker the ability to discover and access KV namespaces, D1 databases, R2 buckets, and other Workers within the same tenant. + +Resources become available immediately after creation—your Worker can access newly created resources without redeployment. + +## Configuration + +Add a tenant binding to your Worker configuration: + +```json +{ + "main": "./src/worker.ts", + "bindings": [ + { + "type": "tenant", + "binding": "MY_RESOURCES" + } + ] +} +``` + +## Usage + +The tenant binding supports two methods for each resource type: + +- **`get(name: string)`** - Get a specific resource by name +- **`list()`** - List all resources of that type in the tenant + +## Resource access + +### KV namespaces + +```javascript +// Get a specific KV namespace +const userStore = env.MY_RESOURCES.kv.get("user-store"); +if (userStore) { + await userStore.put("key", "value"); + const value = await userStore.get("key"); +} + +// List all KV namespaces +const allKV = await env.MY_RESOURCES.kv.list(); +``` + +### D1 databases + +```javascript +// Get a specific database +const userDB = env.MY_RESOURCES.d1.get("user-database"); +if (userDB) { + const result = await userDB.prepare("SELECT * FROM users").all(); +} + +// List all databases +const allDB = await env.MY_RESOURCES.d1.list(); +``` + +## Error handling + +Always check if resources exist before using them: + +```javascript +export default { + async fetch(request, env, ctx) { + const database = env.MY_RESOURCES.d1.get("user-database"); + if (!database) { + return new Response("Database not configured", { status: 503 }); + } + + try { + const result = await database.prepare("SELECT COUNT(*) as count FROM users").first(); + return Response.json({ userCount: result.count }); + } catch (error) { + return new Response("Database error", { status: 500 }); + } + }, +}; +``` diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/tenant/dispatch-workers.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/tenant/dispatch-workers.mdx new file mode 100644 index 000000000000000..ac48beea9078a68 --- /dev/null +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/tenant/dispatch-workers.mdx @@ -0,0 +1,103 @@ +--- +title: Dispatch Worker +pcx_content_type: how-to +sidebar: + order: 3 +--- + +The Dispatch Worker routes incoming requests to the appropriate tenant resources. They act as the entry point to your platform, handling request routing, tenant identification, and resource discovery. With the platform binding, dispatch workers can dynamically access any tenant and their resources. + +## Platform Binding + +The `platform` binding gives dispatch workers access to all tenants and their resources within your platform. + +### Configuration + +Configure your dispatch worker with a platform binding: + +```json +{ + "main": "./src/dispatch-worker.ts", + "bindings": [ + { + "type": "platform", + "binding": "DISPATCHER" + } + ] +} +``` + +### Basic Usage + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + // Get a tenant by name + const tenant = env.DISPATCHER.get("acme-corp"); + if (!tenant) { + return new Response("Tenant not found", { status: 404 }); + } + + // Get a worker within the tenant + const worker = await tenant.workers.get("api-handler"); + if (!worker) { + return new Response("Worker not found", { status: 404 }); + } + + // Forward the request to the tenant's worker + return await worker.fetch(request); + }, +}; +``` + +## Request routing + +### Subdomain-based routing + +Route requests based on subdomain to identify the tenant: + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + const subdomain = url.hostname.split('.')[0]; + + // Map subdomain to tenant name + const tenantName = subdomain; // e.g., "acme-corp.yourplatform.com" + + const tenant = env.DISPATCHER.get(tenantName); + if (!tenant) { + return new Response("Tenant not found", { status: 404 }); + } + + // Route to the tenant's main worker + const mainWorker = await tenant.workers.get("main"); + if (mainWorker) { + return await mainWorker.fetch(request); + } + + return new Response("Service unavailable", { status: 503 }); + }, +}; +``` + + +## Deployment + +Deploy your dispatch worker to handle platform routing: + +```bash +curl -X PUT \ + "https://api.cloudflare.com/client/v4/accounts/{account-id}/platforms/production/dispatch-worker" \ + -H "Authorization: Bearer {api-token}" \ + -H "Content-Type: application/json" \ + -d '{ + "script": "export default { async fetch(request, env) { /* your dispatch logic */ } }", + "bindings": [ + { + "type": "platform", + "binding": "DISPATCHER" + } + ] + }' +``` \ No newline at end of file diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/tenant/getting-started.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/tenant/getting-started.mdx new file mode 100644 index 000000000000000..aa44112acaf6569 --- /dev/null +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/tenant/getting-started.mdx @@ -0,0 +1,161 @@ +--- +title: Getting started +pcx_content_type: tutorial +sidebar: + order: 2 +--- + +This guide walks you through setting up your first platform with tenants, deploying customer resources, and implementing request routing. + +## Prerequisites + +Before you begin, ensure you have: + +- A Workers for Platforms subscription + +## Step 1: Create a platform + +Create a platform to contain all your tenants: + +```bash +curl -X POST \ + "https://api.cloudflare.com/client/v4/accounts/{account-id}/platforms" \ + -H "Authorization: Bearer {api-token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my-platform" + }' +``` + +## Step 2: Create your first tenant + +Create a tenant for your first customer: + +```bash +curl -X POST \ + "https://api.cloudflare.com/client/v4/accounts/{account-id}/platforms/my-platform/tenants" \ + -H "Authorization: Bearer {api-token}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "acme-corp" + }' +``` + +## Step 3: Create a KV namespace in the tenant + +Create a KV namespace for the customer to store data: + +```bash +curl -X POST \ + "https://api.cloudflare.com/client/v4/accounts/{account-id}/platforms/my-platform/tenant/acme-corp/kv/namespaces" \ + -H "Authorization: Bearer {api-token}" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "user-cache" + }' +``` + +## Step 4: Create a tenant worker + +Deploy a worker that accesses the KV namespace: + +```bash +curl -X PUT \ + "https://api.cloudflare.com/client/v4/accounts/{account-id}/platforms/my-platform/tenant/acme-corp/workers/api-worker" \ + -H "Authorization: Bearer {api-token}" \ + -H "Content-Type: application/json" \ + -d '{ + "script": "export default {\n async fetch(request, env, ctx) {\n const cache = env.TENANT.kv.get(\"user-cache\");\n await cache.put(\"last-visit\", new Date().toISOString());\n const lastVisit = await cache.get(\"last-visit\");\n return new Response(`Last visit: ${lastVisit}`);\n }\n};", + "bindings": [ + { + "type": "tenant", + "binding": "TENANT" + } + ] + }' +``` + +## Step 5: Create a dispatch worker + +Create a dispatch worker to route requests to the appropriate tenant: + +```bash +curl -X PUT \ + "https://api.cloudflare.com/client/v4/accounts/{account-id}/platforms/my-platform/dispatch-worker" \ + -H "Authorization: Bearer {api-token}" \ + -H "Content-Type: application/json" \ + -d '{ + "script": "export default {\n async fetch(request, env) {\n const url = new URL(request.url);\n const subdomain = url.hostname.split(\".\")[0];\n \n const tenant = env.DISPATCHER.get(subdomain);\n if (!tenant) {\n return new Response(\"Tenant not found\", { status: 404 });\n }\n \n const worker = await tenant.workers.get(\"api-worker\");\n if (!worker) {\n return new Response(\"Service unavailable\", { status: 503 });\n }\n \n return await worker.fetch(request);\n }\n};", + "bindings": [ + { + "type": "platform", + "binding": "DISPATCHER" + } + ] + }' +``` + +## Step 6: Test your setup + +Test routing through your dispatch worker: + +```bash +# Assuming your dispatch worker is accessible and you've configured DNS +# to route acme-corp.{your-domain}.com to it + +curl "https://acme-corp.{your-domain}.com/" + +# Response: Shows the last visit timestamp +``` + +## Using the TypeScript SDK + +For easier development, use the Cloudflare TypeScript SDK: + +```bash +npm install cloudflare +``` + +```typescript +import Cloudflare from 'cloudflare'; + +const cloudflare = new Cloudflare({ + apiToken: process.env.CLOUDFLARE_API_TOKEN, +}); + +// Get the platform +const platform = await cloudflare.platforms.get({ name: 'my-platform' }); + +// Create a tenant +const tenant = await platform.tenants.create({ + name: 'new-customer', + environment_variables: { + CUSTOMER_NAME: 'New Customer Inc' + } +}); + +// Deploy worker +await tenant.workers.update({ + name: 'hello-worker', + content: { + script: ` + export default { + async fetch(request, env, ctx) { + const customerName = env.CUSTOMER_NAME || 'Unknown'; + return new Response('Hello from ' + customerName + '!'); + } + }; + `, + bindings: [{ type: 'tenant', binding: 'TENANT' }] + } +}); +``` + +## Next steps + +Now that you have a basic platform running, explore these guides: + +- [Tenant Bindings](/cloudflare-for-platforms/workers-for-platforms/tenant/bindings/) - Learn about dynamic resource access +- [Dispatch Workers](/cloudflare-for-platforms/workers-for-platforms/tenant/dispatch-workers/) - Advanced routing patterns +- [API Reference](/cloudflare-for-platforms/workers-for-platforms/tenant/api-reference/) - Complete API documentation +- [Migration Guide](/cloudflare-for-platforms/workers-for-platforms/tenant/migration/) - Move existing Workers to the tenant model \ No newline at end of file diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/tenant/index.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/tenant/index.mdx new file mode 100644 index 000000000000000..b41e01c5e8ab8b0 --- /dev/null +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/tenant/index.mdx @@ -0,0 +1,98 @@ +--- +title: Tenant Model +pcx_content_type: concept +sidebar: + order: 1 +--- + +The tenant model provides clearer isolation and simpler resource management for multi-tenant platforms on Cloudflare. + +Each tenant is a dedicated boundary containing all of a customer's resources — their Workers, databases, storage, and configuration. Resources can only be created within a tenant boundary, eliminating the possibility of accidentally exposing one customer's resources to another. + +![Tenant architecture diagram](~/assets/images/cloudflare-for-platforms/tenant-architecture.png) + +## Key improvements + +- **Dynamic bindings** — Workers can dynamically access any resource in the tenant without managing individual bindings for every resource +- **Consistent naming** — Create resources with the same name (e.g. "my-database") across tenants. This allows you to reference resources consistently in code without prefix logic. +- **Same APIs** — Uses standard Cloudflare management APIs, just scoped to tenant paths +- **Clearer isolation** — Hard tenant boundaries prevent cross-customer resource access +- **Less error-prone** — Resources must be created within a tenant, eliminating misconfigurations +- **Automatic cleanup** — Deleting a tenant removes all associated resources + +## Additional tenant-level controls + +- **Per-customer resource limits** — Apply different quotas and limits at the tenant level for different customer tiers +- **Customer-specific monitoring** — Get telemetry and logs scoped to individual tenants +- **Simplified billing and metering** — Track resource usage per tenant for customer billing + +## How it works + +Your platform contains multiple tenants, each with their own isolated resources. Think of each tenant as a dedicated mini-account for one customer. + +``` +Your Platform (e.g., "production-platform") +├── Tenant (e.g., "tenant-a") +│ ├── Customer's Workers +│ ├── Customer's D1 database +│ ├── Customer's KV namespaces +│ └── Customer's environment variables and secrets +├── Tenant (e.g., "tenant-b") +│ ├── Customer's Workers +│ ├── Customer's D1 database +│ ├── Customer's KV namespaces +│ └── Customer's environment variables and secrets +└── Platform Infrastructure + ├── Dispatch Worker (routes requests to tenants) + └── Outbound Worker (handles external calls) +``` + +### Request flow + +1. Client request arrives at your Dispatch Worker +2. Dispatch Worker determines which tenant to route to (based on domain, token, etc.) +3. Customer's Worker executes within their tenant boundary and dynamically accesses their resources + +```typescript +// Dispatch Worker routes to tenant +const tenant = env.DISPATCHER.get("tenant-a"); +const worker = await tenant.workers.get("api-worker"); +return worker.fetch(request); + +// Customer's Worker accesses resources dynamically +const db = env.TENANT.d1.get("my-database"); +const kv = env.TENANT.kv.get("my-kv-store"); +``` + +## Managing tenants via the REST API + +### API structure + +The `/platforms` API endpoint acts as a scoped proxy to other resource APIs (Workers, KV, D1, etc.). This means you use the exact same API structure, parameters, and requests as the standard Cloudflare APIs. + +The path is simply scoped to your platform and the specific tenant you want to manage. + +**Tenant-Scoped Resources:** + +``` +/accounts//platforms//tenant//workers/ +/accounts//platforms//tenant//kv/ +/accounts//platforms//tenant//d1/ +/accounts//platforms//tenant//...etc +``` + +**Platform-Level Infrastructure:** + +``` +/accounts//platforms//dispatch-worker +/accounts//platforms//outbound-worker +``` + +## Next steps + +Ready to get started? Follow our step-by-step guides: + +- [Getting started](/cloudflare-for-platforms/workers-for-platforms/tenant/getting-started/) - Create your first tenant and deploy customer resources +- [Dispatch workers](/cloudflare-for-platforms/workers-for-platforms/tenant/dispatch-workers/) - Route requests to the right customer +- [User workers](/cloudflare-for-platforms/workers-for-platforms/tenant/user-workers/) - Deploy customer code within tenant boundaries +- [Tenant bindings](/cloudflare-for-platforms/workers-for-platforms/tenant/bindings/) - Use the tenant binding to access customer resources \ No newline at end of file diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/tenant/user-workers.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/tenant/user-workers.mdx new file mode 100644 index 000000000000000..fa593a9db393046 --- /dev/null +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/tenant/user-workers.mdx @@ -0,0 +1,98 @@ +--- +title: User workers +pcx_content_type: concept +sidebar: + order: 4 +--- + +User workers are Workers written by your end users (customers) that contain their specific business logic and application code. In the tenant model, user workers are deployed within tenant boundaries, giving them isolated access to their tenant's resources. + +## What are user workers + +User workers contain code written by your customers or generated by your platform that runs within a tenant boundary. Each user worker automatically gets access to its tenant's resources through a tenant binding. + +## Examples + +### Basic user worker + +```typescript +export default { + async fetch(request, env, ctx) { + return new Response("Hello from my worker!"); + } +}; +``` + +### Accessing tenant resources + +```typescript +export default { + async fetch(request, env, ctx) { + const database = env.MY_RESOURCES.d1.get("my-database"); + const result = await database + .prepare("SELECT * FROM posts LIMIT 5") + .all(); + + return Response.json(result); + } +}; +``` + +## Best practices + +### Use consistent resource names + +Reference resources by consistent names that work across all tenants: + +```typescript +const db = env.MY_RESOURCES.d1.get("user-database"); +const cache = env.MY_RESOURCES.kv.get("session-cache"); +``` + +This means you can deploy the same code template to every customer without customization. + +### Configure the resources binding + +Always configure user workers with a resources binding: + +```json +{ + "bindings": [ + { + "type": "tenant", + "binding": "MY_RESOURCES" + } + ] +} +``` + +This lets customer code discover and access any resource in their tenant without adding new bindings for each resource. + +## Deploying user workers + +### Using the REST API + +```bash +curl -X PUT \ + "https://api.cloudflare.com/client/v4/accounts/{account-id}/platforms/{platform}/tenant/{tenant}/workers/customer-app" \ + -H "Authorization: Bearer {api-token}" \ + -H "Content-Type: application/json" \ + -d '{ + "script": "export default { async fetch(request, env) { /* customer code */ } }", + "bindings": [{"type": "tenant", "binding": "MY_RESOURCES"}] + }' +``` + +### Using the SDK + +```typescript +const tenant = await platform.tenants.get({ name: 'customer-123' }); + +await tenant.workers.update({ + name: 'customer-app', + content: { + script: customerProvidedCode, + bindings: [{ type: 'tenant', binding: 'MY_RESOURCES' }] + } +}); +```