Skip to main content

Scaling & Data Isolation

How EdgeBase scales infinitely with zero configuration — and why physical data isolation is a natural consequence.

The Single Database Bottleneck

Every traditional BaaS funnels all traffic through a single database:

┌─────────────────────────────────────┐
│ Traditional BaaS │
│ │
│ User A ───┐ │
│ User B ───┤── Single Database │
│ User C ───┘ (bottleneck) │
│ │
│ Scale up: replicas, pooling, │
│ sharding, capacity planning... │
└─────────────────────────────────────┘

This creates compounding problems as you grow:

  • Scaling requires manual intervention — read replicas, connection pooling, database sharding
  • One tenant's heavy query slows down everyone else (noisy neighbor)
  • A single SQL injection can expose all tenants' data
  • Deleting a tenant means DELETE FROM ... WHERE tenant_id = ? across every table

Serverless DB Blocks — Scale and Isolation by Default

EdgeBase eliminates the single database bottleneck entirely. Each user, workspace, or tenant gets its own Durable Object with embedded SQLite — a natural consequence of building on serverless edge infrastructure:

┌──────────────────────────────────────────┐
│ EdgeBase │
│ │
│ Tenant A → DO (SQLite) ─── isolated │
│ Tenant B → DO (SQLite) ─── isolated │
│ Tenant C → DO (SQLite) ─── isolated │
│ │
│ Separate processes. │
│ No shared memory or storage. │
│ Data leakage is structurally │
│ impossible. │
└──────────────────────────────────────────┘

In EdgeBase, you declare database blocks in your config. Each block defines a namespace, and optionally an instance ID that the client supplies at runtime:

export default defineConfig({
databases: {
// Static DB — single-instance, shared by all users (D1 by default)
shared: {
tables: {
posts: {
schema: { title: 'string', body: 'text', authorId: 'string' },
access: {
read(auth, row) { return row.status === 'published' || auth?.id === row.authorId },
insert(auth) { return auth !== null },
update(auth, row) { return auth?.id === row.authorId },
delete(auth, row) { return auth?.role === 'admin' },
},
},
},
},

// Dynamic DB — one DO per (namespace, id) pair
user: {
instance: true,
access: {
canCreate(auth, id) { return auth?.id === id }, // only create your own DB
access(auth, id) { return auth?.id === id }, // only access your own DB
},
tables: {
notes: { schema: { title: 'string', body: 'text' } },
settings: { schema: { theme: 'string', lang: 'string' } },
},
},
},
})

DB Block Types

Single-Instance DB (shared)

One logical database, shared by all users. By default it routes to D1 unless you explicitly set provider: 'do'. Best for global data with low write volume or data that doesn't belong to a single owner.

shared: {
tables: {
announcements: { schema: { ... } },
leaderboard: { schema: { ... } },
},
}

Client usage:

const posts = await client.db('shared').table('posts').getList();

Default backend: D1 (DB_D1_SHARED)

Per-User Isolation

Each user gets their own isolated DO. 10 million users → 10 million independent SQLite databases.

'user:{id}': {
access: {
canCreate(auth, id) { return auth?.id === id },
access(auth, id) { return auth?.id === id },
},
tables: {
notes: { schema: { title: 'string', body: 'text' } },
settings: { schema: { theme: 'string', lang: 'string' } },
},
}

Client usage:

const notes = await client.db('user', userId).table('notes').getList();

DO name: user:{userId}

The user ID comes from the JWT sub claim, verified by the access rule. There is no implicit header injection — the client explicitly passes the ID.

Per-Workspace Isolation (B2B SaaS)

Each workspace is a physically isolated silo.

'workspace:{id}': {
access: {
canCreate(auth) { return auth?.custom?.plan === 'pro' },
async access(auth, id, ctx) {
const row = await ctx.db.get('workspace_members', `${auth.id}:${id}`);
return row?.active === true;
},
delete(auth, id) { return auth?.role === 'admin' },
},
tables: {
documents: { schema: { title: 'string', authorId: 'string' } },
invoices: { schema: { amount: 'number', status: 'string' } },
},
}

Client usage:

const docs = await client.db('workspace', 'ws-456').table('documents').getList();

DO name: workspace:ws-456

The access rule queries a membership table on every request — no implicit caching, no token-level claims that can lag. Revoke membership in the DB and the next request is blocked instantly.

Per-Tenant Isolation (Multi-tenant SaaS)

'tenant:{id}': {
access: {
async access(auth, id, ctx) {
const member = await ctx.db.get('tenant_members', `${auth.id}:${id}`);
return member?.active === true;
},
},
tables: {
crm: { schema: { ... } },
invoices: { schema: { ... } },
},
}

Client usage:

const crm = await client.db('tenant', tenantId).table('crm').getList();

Namespace Naming

The namespace name in a DB block (the part before :{id}) is fully customizable — you can use any string, not just the four examples shown above. Use whatever name makes sense for your domain:

// Game with per-guild databases
'guild:{id}': { tables: { members: { ... }, events: { ... } } }

// IoT with per-device databases
'device:{id}': { tables: { readings: { ... }, config: { ... } } }

// Education with per-classroom databases
'classroom:{id}': { tables: { students: { ... }, assignments: { ... } } }

// E-commerce with per-store databases
'store:{id}': { tables: { products: { ... }, orders: { ... } } }

The only requirements are:

  • Static DBs use a plain name (e.g., shared, global, public)
  • Dynamic DBs use the name:{id} pattern where {id} is supplied by the client at runtime
  • The instance ID must not contain the : character (used as a delimiter internally)

DB-Level Rules

Every dynamic DB block supports three access callbacks:

RuleWhen calledSignature
canCreateFirst access (DO doesn't exist yet)(auth, id) => boolean
accessEvery request to an existing DO(auth, id, ctx?) => boolean | Promise<boolean>
deleteAdmin DO deletion(auth, id) => boolean

canCreate defaults to deny when undefined — you must explicitly opt in to allow new DB creation. This prevents unbounded DO creation by malicious clients.

Infinite Horizontal Scaling — Zero Configuration

Horizontal scaling is the primary architectural advantage of DB blocks. Traditional BaaS platforms require manual intervention to scale — read replicas, connection pooling, database sharding. With DB blocks, scaling is automatic: every new user, workspace, or tenant creates a new independent instance. There is no configuration change, no migration, and no downtime. 10 users and 10 million users run on the same architecture — the only difference is the number of DO instances.

Since each DB instance is a separate Durable Object:

Active InstancesWrites/sec per DOTotal Writes/sec
1,000500500,000
100,00050050,000,000

No shared locks, no connection pooling, no contention. Each instance handles only its own data.

Each DO has a 10 GB SQLite storage limit:

  • Per-user: 10 GB per user (more than enough for most apps)
  • Per-workspace: 10 GB per workspace
  • Per-tenant: 10 GB per tenant

Total platform storage = 10 GB × number of instances = practically unlimited.

GDPR and Data Deletion

Deleting a tenant's data is trivial with physical isolation:

Traditional BaaS:
DELETE FROM posts WHERE tenant_id = 'acme'
DELETE FROM comments WHERE tenant_id = 'acme'
DELETE FROM files WHERE tenant_id = 'acme'
... (every table, hope you didn't miss one)

EdgeBase:
Delete DO "tenant:acme"
→ All data gone. Nothing to miss.

Design Decisions

Why Not Just RLS?

AspectRLS (Logical)DB Block (Physical)
Isolation levelQuery filterSeparate process + storage
SQL injection riskExposes all tenantsOnly one tenant accessible
Noisy neighborShared DB = shared performanceIndependent performance
Data deletionMulti-table DELETEDelete the DO
GDPR proofMust audit query pathsStructural guarantee
ComplexityDeveloper must write RLS rulesExplicit access() function

When to Use shared vs Dynamic Blocks

Data typeRecommended block
Global data (announcements, leaderboard)shared
Personal data (notes, settings, feeds)user:{id}
Team/workspace dataworkspace:{id}
Enterprise tenant datatenant:{id}
Cross-tenant analyticsshared block with provider: 'neon' (PostgreSQL), or App Functions to aggregate across DOs

Next Steps

  • Data Modeling Guide — Decision flowchart for choosing DB block types, anti-patterns, and a quick reference table
  • Real-World Patterns — Complete config examples for SaaS, social, marketplace, and chat apps