| name | multi-tenant |
| description | Design multi-tenant Cloudflare applications with tenant isolation, D1 row/database boundaries, Durable Object-per-tenant patterns, R2 key prefixes, KV cache keys, authz, noisy-neighbor controls, and migration paths. Use for SaaS and platform code.
|
| compatibility | Cloudflare Workers TypeScript projects using Wrangler; verify current Cloudflare APIs, limits, and pricing before production use. |
| metadata | {"source":"Architecting on Cloudflare plus official Cloudflare Developer Platform docs","generated":"2026-04-28"} |
Multi-Tenant Design
Use this skill when building SaaS or any application serving multiple tenants, workspaces, organizations, or customers.
Isolation ladder
Choose the lowest isolation level that satisfies security, compliance, scale, and operational needs.
- Row-level isolation: shared D1/database tables with
tenant_id on every row.
- Schema/table/database per tenant group: stronger blast-radius control and data movement options.
- Durable Object per tenant/entity: serialized coordination and private per-object state.
- Separate buckets/namespaces/resources: higher isolation and operational overhead.
- Separate Workers/accounts/platform model: strongest isolation, highest management complexity.
Mandatory tenant rules
- Every request must resolve a principal with
tenantId before data access.
- Every D1 table that stores tenant data must include
tenant_id unless physically isolated.
- Every query must include a tenant predicate or use a tenant-specific database/binding.
- Every R2 key should include a tenant prefix or tenant-isolated bucket.
- Every KV key must include tenant and schema/version dimensions.
- Durable Object names should include tenant ID for tenant-scoped coordination.
D1 query pattern
async function getProject(env: Env, tenantId: string, projectId: string) {
return env.DB.prepare(
`SELECT id, tenant_id, name, created_at
FROM projects
WHERE tenant_id = ? AND id = ?`
).bind(tenantId, projectId).first<Project>();
}
R2 key pattern
const key = `tenants/${tenantId}/files/${fileId}`;
Durable Object key pattern
const object = env.TENANT_COORDINATOR.getByName(`tenant:${tenantId}`);
Noisy-neighbor controls
- Rate-limit by tenant and user.
- Partition Durable Objects by entity, not just tenant, if a tenant can be very large.
- Track per-tenant storage, queue, workflow, and AI usage.
- Add quotas before enterprise customers demand them.
- Use separate resources for large/high-compliance tenants when required.
Review checklist
Anti-patterns
- Frontend hides other tenants but backend queries do not scope by tenant.
- KV key
settings shared across all tenants.
- R2 object key generated from user-supplied filename only.
- Agent or AI tool can search across tenants without hard filters.
- One Durable Object serializes all tenants.