Skip to main content

Architecture Overview

How EdgeBase processes every request — from HTTP entry to the correct storage backend.

High-Level Architecture

Client SDK ──HTTP/WS──▶ Worker (Hono)

┌─────────────────┼─────────────────┬─────────────┐
│ │ │ │
DatabaseDO D1 DB Blocks D1 (AUTH_DB) D1 (CONTROL_DB) DatabaseLiveDO RoomsDO LogsDO
(dynamic SQLite) (single-instance) (auth data) (ops metadata) (DB subs & (rooms, (structured
broadcast) presence, logging)
broadcast ch.)

┌─────┴─────┐
R2 KV
(File storage) (Cache, OAuth,
Push Tokens)

Every request enters through a single Cloudflare Worker running Hono, which routes to the appropriate backend. Dynamic DB blocks use DatabaseDO with embedded SQLite, single-instance DB blocks default to D1, auth uses AUTH_DB, plugin/control-plane metadata uses CONTROL_DB, DB subscriptions and server broadcast use DatabaseLiveDO, rooms/presence/broadcast channels use RoomsDO, and structured logging uses LogsDO.

Request Lifecycle

Middleware Chain

Every HTTP request passes through a strict middleware chain before reaching any backend:

Request


Error Handler ─── Global error boundary, catches all exceptions


Logger ─────────── Request/response logging


CORS ───────────── Cross-origin resource sharing


Rate Limit ─────── 2-tier: software counter + Cloudflare Binding ceiling


Auth ───────────── JWT verification (local crypto, no DO call) [/api/*]


Rules ──────────── Declarative access rules (TypeScript functions, deny-by-default) [/api/db/*]


Internal Guard ─── Restricts /internal/* endpoints [/internal/*]


Route Handler ──── Routes to D1, DatabaseDO, PostgreSQL, DatabaseLiveDO, or RoomsDO

The ordering is intentional. Auth runs before Rules because access rules need the authenticated user ID to evaluate per-record permissions.

Request → DB Block Routing

The Worker determines which backend to contact based on the DB block configuration:

GET /api/db/shared/tables/notes?filter=...

Worker:
1. JWT verify (local crypto) → extract auth.id
2. Resolve backend + database identity:
- single-instance DB → D1 binding by default (e.g. DB_D1_SHARED)
- dynamic DB → DO name "user:{userId}" | "workspace:{wsId}"
- postgres/neon → Worker-side PostgreSQL handler
3. Route request to the resolved backend

Durable Object Types

DatabaseDO

The workhorse for dynamic DB blocks. Each instance owns an embedded SQLite database.

ResponsibilityDetails
CRUD operationsCreate, read, update, delete with filtering, sorting, pagination
Schema managementLazy table creation on first request, automatic migrations
Full-text searchFTS5 powered, configured per table
Access rulesEvaluated per record using TypeScript functions
Event emissionNotifies DatabaseLiveDO on data changes

Naming convention: {namespace} (static, e.g. shared) or {namespace}:{id} (dynamic, e.g. user:abc123, workspace:ws_456)

A single DatabaseDO processes requests serially (single-threaded). This eliminates concurrency bugs but bounds throughput to ~200-1,000 writes/sec per instance. The DB-block pattern (user:{id}, workspace:{id}, etc.) distributes load across many independent DO instances.

Auth (D1-First, DO-Coordinated OAuth)

Durable auth data is handled by D1. Only short-lived OAuth callback authority uses a key-sharded Durable Object:

D1 (AUTH_DB) ── All auth data (users, sessions, OAuth, MFA, passkeys)
D1 (CONTROL_DB) ── Internal control-plane metadata (plugin versions, cleanup state)
AUTH Durable Objects ── Five-minute OAuth state and one-time link continuations

The Worker routes user/session/identity operations directly to D1. OAuth start and callback routes use the AUTH binding to atomically consume their random state key; new flows never use eventually consistent KV as callback authority.

Key design decision: Ordinary data requests verify JWT locally and then go straight to the configured DB backend. They do not pay the OAuth coordination hop.

DatabaseLiveDO

Manages real-time DB subscription streaming and server-side broadcast via Cloudflare's Hibernation API:

ResponsibilityDetails
SubscriptionsCollection change notifications with server-side filters
Server broadcastHTTP-based broadcast via /api/db/broadcast
HibernationIdle connections cost $0, wake on message

RoomsDO

Server-authoritative real-time state channel for multiplayer games, collaborative editing, and live dashboards:

ResponsibilityDetails
Room stateShared, player, and server state areas
MembersOnline/offline user tracking per room (replaces presence)
SignalsArbitrary message passing between connected clients
HibernationIdle rooms cost $0, state persisted to DO Storage

Auxiliary Storage

ServiceRoleExamples
D1Auth database (AUTH_DB)All auth data: users, sessions, OAuth, email tokens, MFA, passkeys, public profiles
D1Internal control plane (CONTROL_DB)Plugin versions and other internal operational metadata
DOAtomic OAuth coordinatorKey-sharded, five-minute sign-in/link state and one-minute browser continuations
KVEphemeral state cacheShort-lived WebSocket pending gates, multipart upload tracking, email caches, push tokens/logs, legacy OAuth migration mirror
R2File storageUser uploads, signed URLs, multipart uploads

Most KV usage in EdgeBase is ephemeral or cache-like. Persistent, strongly consistent state should live in D1, Durable Objects, or a database provider instead.

Three Deployment Modes

The same codebase runs identically across three environments because Cloudflare's workerd runtime is open source:

ModeCommandRuntimeBest For
Edgenpx edgebase deployCloudflare WorkersGlobal, ~0ms cold start
Dockernpx edgebase docker runworkerd in containerSelf-hosted, full control
Node.jsnpx edgebase devworkerd via MiniflareDevelopment, VPS

All state persists in a single /data directory (Docker) or Cloudflare's infrastructure (Edge). No external database server is required.

Next Steps