Back to Blog
Architecture Deep Dive 15 min read

Multi-Tenant SaaS Architecture: Isolation Without Servers

How Cadences achieves total data isolation, coordinated migrations and edge-native performance — without managing a single server.

G
Gonzalo Monzón
Server infrastructure and network architecture

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
Fundamentals

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.

Simple to implement
Easy to start quickly
Risk of data leakage if a query forgets the filter
One bad query degrades everyone
Harder to comply with data deletion regulations

🏗️ Database per Tenant Cadences

Each organization gets its own isolated database. Total physical separation.

Impossible to leak data between tenants
Each client has independent performance
Delete = drop database (instant GDPR compliance)
Requires migration coordination
More complex provisioning

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.

Infrastructure

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.

Implementation

Tenant Resolution

When a request arrives, the first thing Cadences does is determine which organization owns it. This process is called tenant resolution:

tenant-resolver.ts (simplified)
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.

Security

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.

Operations

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:

1

Declare

The new migration is deployed in code with a version number (e.g., v47).

2

Check on connect

When a tenant connects, the system checks their current version (stored in KV). If it's < v47, migrations are applied.

3

Atomic lock

The Durable Object ensures only one migration runs at a time per tenant. Other requests wait (50–200ms typically).

4

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.

Economics

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.

Comparison

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)
Retrospective

4 Lessons Learned

1

"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.

2

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.

3

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.

4

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 Platform
Final Thoughts

Conclusion

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.

G
Gonzalo Monzón
Building the future of business management
Share: