Skip to main content

Real-World Patterns

Complete edgebase.config.ts examples for common application types.

Personal Productivity App

A note-taking or todo app where each user has their own private data.

import { defineConfig } from '@edgebase/shared';

export default defineConfig({
databases: {
// user:{id} DB block — each user gets their own isolated DO
user: {
access: {
access(auth, id) { return auth?.id === id },
},
tables: {
notes: {
schema: {
title: { type: 'string', required: true },
content: { type: 'text' },
tags: { type: 'string' }, // comma-separated
pinned: { type: 'boolean', default: false },
color: { type: 'string' },
},
fts: ['title', 'content'],
access: {
read(auth) { return auth !== null },
insert(auth) { return auth !== null },
update(auth) { return auth !== null },
delete(auth) { return auth !== null },
},
},
settings: {
schema: {
theme: { type: 'string' },
language: { type: 'string' },
fontSize: { type: 'number' },
},
access: {
read(auth) { return auth !== null },
insert(auth) { return auth !== null },
update(auth) { return auth !== null },
},
},
},
},
},
});

Why this works:

  • Each user gets their own DO → queries are instant (only searching their ~100-1000 notes)
  • FTS on title and content → full-text search across personal notes
  • User data deletion is trivial (delete the DO)

Scaling characteristics:

  • 1M users = 1M independent DOs, each handling its own traffic
  • Read performance: microseconds (searching a small personal database)
  • Write performance: irrelevant concern (one user can't generate 500 writes/sec)

B2B SaaS Platform

A project management tool where companies have workspaces with team members.

import { defineConfig } from '@edgebase/shared';

export default defineConfig({
databases: {
workspace: {
access: {
async access(auth, id, ctx) {
const m = await ctx.db.get('members', `${auth.id}:${id}`);
return m !== null;
},
},
tables: {
// ── Project management (all tables in same DB block → same DO, JOINs OK) ──
projects: {
schema: {
name: { type: 'string' },
description: { type: 'string' },
ownerId: { type: 'string' },
status: { type: 'string' }, // active | archived
},
access: {
read(auth) { return auth !== null },
insert(auth) { return auth !== null },
update(auth, row) { return auth?.id === row.ownerId || auth?.meta?.role === 'admin' },
delete(auth) { return auth?.meta?.role === 'admin' },
},
},
tasks: {
schema: {
title: { type: 'string' },
projectId: { type: 'string' },
assigneeId: { type: 'string' },
status: { type: 'string' }, // todo | in_progress | done
priority: { type: 'number' },
dueDate: { type: 'string' },
},
fts: ['title'],
access: {
read(auth) { return auth !== null },
insert(auth) { return auth !== null },
update(auth) { return auth !== null },
delete(auth, row) { return auth?.id === row.assigneeId || auth?.meta?.role === 'admin' },
},
},

// ── Documents (separate DO — independent scaling) ──
documents: {
schema: {
title: { type: 'string' },
content: { type: 'string' },
projectId: { type: 'string' },
authorId: { type: 'string' },
},
fts: ['title', 'content'],
access: {
read(auth) { return auth !== null },
insert(auth) { return auth !== null },
update(auth, row) { return auth?.id === row.authorId || auth?.meta?.role === 'admin' },
delete(auth) { return auth?.meta?.role === 'admin' },
},
},

members: {
schema: {
userId: { type: 'string', required: true },
role: { type: 'string', default: 'member' },
},
},
},
},
},
});

Why this structure?

  • projects, tasks, and documents are all in the same workspace DB block → they share a DO per workspace instance, so you can query "all tasks for project X" efficiently with JOINs

Scaling characteristics:

  • 10,000 companies = 10,000 independent project management DOs
  • Each company's data is physically isolated (GDPR compliance is trivial)
  • Onboarding a new company = zero infrastructure change

Social Platform

A social app where posts are public but personal data is private.

import { defineConfig } from '@edgebase/shared';

export default defineConfig({
databases: {
// ── Public content (shared DO — everyone reads) ──
shared: {
tables: {
posts: {
schema: {
content: { type: 'string' },
authorId: { type: 'string' },
authorName: { type: 'string' },
likes: { type: 'number' },
imageUrl: { type: 'string' },
},
fts: ['content'],
access: {
read() { return true }, // Anyone can browse
insert(auth) { return auth !== null }, // Must be signed in
update(auth, row) { return auth?.id === row.authorId }, // Only author
delete(auth, row) { return auth?.id === row.authorId || auth?.role === 'admin' },
},
},
comments: {
schema: {
postId: { type: 'string' },
content: { type: 'string' },
authorId: { type: 'string' },
authorName: { type: 'string' },
},
access: {
read() { return true },
insert(auth) { return auth !== null },
delete(auth, row) { return auth?.id === row.authorId || auth?.role === 'admin' },
},
},
},
},

// ── Private data (per-user isolation) ──
user: {
access: { access(auth, id) { return auth?.id === id } },
tables: {
bookmarks: {
schema: { postId: { type: 'string' }, savedAt: { type: 'string' } },
access: {
read(auth) { return auth !== null },
insert(auth) { return auth !== null },
delete(auth) { return auth !== null },
},
},
drafts: {
schema: { content: { type: 'string' }, imageUrl: { type: 'string' } },
access: {
read(auth) { return auth !== null },
insert(auth) { return auth !== null },
update(auth) { return auth !== null },
delete(auth) { return auth !== null },
},
},
},
},
},
});

Design insight:

  • posts and comments are not isolated — they're public, shared data that everyone reads
  • bookmarks and drafts are isolated per user — private data that only one user accesses
  • This is the right split: use access rules for authorization, DB blocks for data ownership

Why it scales:

  • Public feeds are read-heavy → a single-instance shared block is a good fit
  • If write volume becomes an issue on posts, partition by category namespace

Marketplace

A platform where vendors sell products and manage orders.

import { defineConfig } from '@edgebase/shared';

export default defineConfig({
databases: {
// ── Per-vendor data (physically isolated) ──
vendor: {
access: {
async access(auth, id, ctx) {
const m = await ctx.db.get('members', `${auth.id}:${id}`);
return m !== null || auth?.meta?.isPublicBrowser === true;
},
},
tables: {
products: {
schema: {
name: { type: 'string' },
description: { type: 'string' },
price: { type: 'number' },
currency: { type: 'string' },
stock: { type: 'number' },
category: { type: 'string' },
imageUrl: { type: 'string' },
active: { type: 'boolean' },
},
fts: ['name', 'description'],
access: {
read() { return true }, // Anyone can browse
insert(auth) { return auth?.meta?.role === 'vendor' }, // Only the vendor
update(auth) { return auth?.meta?.role === 'vendor' },
delete(auth) { return auth?.meta?.role === 'vendor' },
},
},
orders: {
schema: {
productId: { type: 'string' },
buyerId: { type: 'string' },
quantity: { type: 'number' },
totalPrice: { type: 'number' },
status: { type: 'string' }, // pending | confirmed | shipped | delivered
},
access: {
read(auth, row) { return auth?.meta?.role === 'vendor' || auth?.id === row.buyerId },
insert(auth) { return auth !== null },
update(auth) { return auth?.meta?.role === 'vendor' },
},
},
members: {
schema: {
userId: { type: 'string', required: true },
role: { type: 'string', default: 'member' },
},
},
},
},

// ── Buyer's private data ──
user: {
access: { access(auth, id) { return auth?.id === id } },
tables: {
cart: {
schema: {
productId: { type: 'string' },
vendorId: { type: 'string' },
quantity: { type: 'number' },
},
access: {
read(auth) { return auth !== null },
insert(auth) { return auth !== null },
update(auth) { return auth !== null },
delete(auth) { return auth !== null },
},
},
},
},
},
});

Why this pattern?

  • products and orders are in the same vendor DB block → share a DO per vendor, enabling JOINs like "products with their orders"
  • Each vendor is physically isolated → vendor A's traffic spike doesn't affect vendor B
  • Buyer's cart is per-user → instant, private

Chat / Messaging App

A real-time messaging app with channels and direct messages.

import { defineConfig } from '@edgebase/shared';

export default defineConfig({
databases: {
// ── Channel messages (isolated per channel) ──
channel: {
tables: {
messages: {
schema: {
content: { type: 'string' },
authorId: { type: 'string' },
authorName: { type: 'string' },
type: { type: 'string' }, // text | image | file
attachmentUrl: { type: 'string' },
},
access: {
read(auth) { return auth !== null },
insert(auth) { return auth !== null },
delete(auth, row) { return auth?.id === row.authorId || auth?.role === 'admin' },
},
},
},
},

// ── Channel metadata + user prefs ──
shared: {
tables: {
channels: {
schema: {
name: { type: 'string' },
description: { type: 'string' },
createdBy: { type: 'string' },
memberCount: { type: 'number' },
isPrivate: { type: 'boolean' },
},
access: {
read(auth) { return auth !== null },
insert(auth) { return auth !== null },
update(auth) { return auth?.role === 'admin' },
},
},
},
},

// ── User preferences (per-user) ──
user: {
access: { access(auth, id) { return auth?.id === id } },
tables: {
userPrefs: {
schema: {
mutedChannels: { type: 'string' }, // JSON array
notificationLevel: { type: 'string' },
},
access: {
read(auth) { return auth !== null },
insert(auth) { return auth !== null },
update(auth) { return auth !== null },
},
},
},
},
},
});

Why channel DB block namespace?

  • Each channel's messages are in their own DO → 10,000 channels = 10,000 independent message stores
  • High-volume channels (thousands of messages/sec) don't affect quiet channels
  • Combined with EdgeBase Realtime, each channel gets efficient WebSocket broadcasting

Summary: Choosing Your Pattern

QuestionAnswer → Pattern
Does data belong to one user?databases: { user: { ... } }
Does data belong to a team/org?databases: { workspace: { ... } }
Do tables need JOINs?Put them in the same DB block
Is it public, read-heavy data?databases: { shared: { ... } } + access rules
Is it high-traffic shared data?Find a natural partition namespace
Is it global, low-write data?databases: { shared: { ... } }, single-instance DB is fine