Multi-Tenant SaaS Architecture: Isolation Without Servers
How Cadences achieves total data isolation, coordinated migrations and edge-native performance — without managing a single server.
When you build a SaaS, one of the first architectural decisions is: how do I keep each client's data separate, secure and performant? The answer to that question shapes everything else: cost, scalability, maintenance and compliance.
This article explains in detail how Cadences solves the multi-tenancy problem using a database-per-tenant approach at the edge with Cloudflare's infrastructure.
🏗️ What you'll learn in this article
- Database per Tenant vs. Shared Database: trade-offs and why we chose isolation
- The Cadences Stack: Workers, Durable Objects, D1, R2 and KV
- Tenant resolution, isolation guarantees and coordinated migrations
- Real cost model: $0.12/org/month and how it scales
Database per Tenant vs. Shared Database
There are two classic approaches to multi-tenancy:
🏢 Shared Database
All clients share one database. A tenant_id column separates rows.
🏗️ Database per Tenant Cadences
Each organization gets its own isolated database. Total physical separation.
Traditionally, database-per-tenant was expensive because each database meant a server (or at least a container). But with Cloudflare D1, creating a SQLite database is a millisecond API call, and costs only cents per month.
The Cadences Stack
The entire platform runs on four Cloudflare primitives:
Workers
Application logic. Each request runs in an isolated V8 isolate at the nearest edge location. No servers, no containers, no cold starts.
Durable Objects
Strongly consistent state per tenant. Each organization has its own Durable Object that acts as a coordinator: manages sessions, transaction locks and workflow queues.
D1 (SQLite at the edge)
One database per organization. Full SQL. Automatic replication. Each DB is very small (usually <100 MB) and incredibly fast. Creating a new DB takes <50ms.
R2 + KV
R2 for file storage (attachments, exports, backups). KV for global caches (configurations, feature flags, rate limiting). Zero egress costs.
Tenant Resolution
When a request arrives, the first thing Cadences does is determine which organization owns it. This process is called tenant resolution:
async function resolveTenant(request: Request): Promise<TenantContext> {
// 1. Extract JWT from header or cookie
const token = extractToken(request);
const claims = await verifyJWT(token);
// 2. Look up the org in the global registry (KV)
const orgMeta = await KV.get(`org:${claims.org_id}`);
// 3. Get a handle to the tenant's D1 database
const db = env.DB.get(orgMeta.d1_database_id);
// 4. Get a handle to the Durable Object (coordinator)
const coordinator = env.TENANT_DO.get(
env.TENANT_DO.idFromName(claims.org_id)
);
return { db, coordinator, org: orgMeta, user: claims };
} After resolution, all queries in that request run against that specific D1. There's no way a query "leaks" into another org's database because the database literally doesn't exist in the context.
Isolation Guarantees
Data Isolation
Each org has its own D1 database. No shared tables, no shared rows. Zero risk of cross-tenant leakage.
Performance Isolation
A heavy query from Org A has zero impact on Org B. Each database has independent IOPS.
Schema Isolation
Each database can have its own schema version. Migrations roll out progressively with zero downtime.
Deletion Isolation
Deleting an org = DROP DATABASE + delete R2 prefix. Full GDPR "right to erasure" in milliseconds.
Coordinated Migrations
The biggest challenge with database-per-tenant is: how do you apply schema changes to 10,000 databases at once?
Our answer: don't. We use a lazy migration strategy:
Declare
The new migration is deployed in code with a version number (e.g., v47).
Check on connect
When a tenant connects, the system checks their current version (stored in KV). If it's < v47, migrations are applied.
Atomic lock
The Durable Object ensures only one migration runs at a time per tenant. Other requests wait (50–200ms typically).
Background sweep
A Cron Worker iterates over all DBs and applies pending migrations to inactive tenants so they're ready when they return.
Result: zero downtime, no migration windows, each tenant migrates at its own pace without affecting others.
Real Cost Model
One of the biggest advantages of the Cloudflare stack is cost predictability. Here's the real breakdown per organization per month:
| Component | Cost/org/month | Notes |
|---|---|---|
| D1 Database | ~$0.04 | 50 MB average × reads/writes |
| Workers (compute) | ~$0.03 | Average 5K requests/org/month |
| Durable Objects | ~$0.02 | Walls-clock + storage |
| R2 + KV | ~$0.03 | Files + cache |
| Total | ~$0.12 | Per org per month |
Compare this with a traditional setup: a dedicated PostgreSQL instance would cost $15–$50/month minimum per tenant. A shared RDS with proper isolation can reach $2–5/org. The edge-native model with D1 is 10-100× cheaper.
Edge-Native vs. Traditional Cloud
| Aspect | Traditional (AWS/GCP) | Edge-Native (Cadences) |
|---|---|---|
| Cold start | 200ms–2s (Lambda/Cloud Run) | 0ms (V8 isolate) |
| Latency | 50–200ms (single region) | <20ms (nearest PoP) |
| DB location | Fixed region | Edge replicated |
| Scaling | Auto-scaling groups, 30-60s | Instant, per-request |
| Cost at 10K orgs | $20K–50K/month | ~$1,200/month |
| Ops needed | DevOps/SRE team | Zero (fully managed) |
4 Lessons Learned
"Database per tenant" is viable if the db is lightweight
D1/SQLite databases are files, not processes. Creating one takes milliseconds, and the cost is proportional to actual usage, not to reservation.
Lazy migrations beat "big bang" every time
Instead of migrating everything at once (and risking collective downtime), each tenant migrates independently when it connects. The risk is isolated.
Durable Objects are the missing piece
Without a per-tenant coordinator, the edge model breaks down with race conditions. Durable Objects solve exactly this: single-threaded, persistent, globally addressable.
GDPR compliance becomes trivial
"Delete all my data" = DROP DATABASE + DELETE R2 prefix. No need to audit every query to see if a tenant_id was left behind.
Want to build on this architecture?
Cadences is not just a SaaS — it's a platform you can extend. If you have a use case that requires multi-tenant isolation, performance and low cost, talk to us.
Explore the PlatformConclusion
Multi-tenant architecture doesn't have to be a nightmare of WHERE tenant_id = ? in every query. With database per tenant at the edge, isolation is physical, performance is local, and cost is proportional to real usage.
The combination of Workers + Durable Objects + D1 + R2 gives us what traditional cloud SaaS couldn't: server-level isolation at serverless cost. And the icing on the cake: GDPR compliance becomes trivial when deletion means dropping a database.
The best architecture is the one that works for you without you having to think about it.