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