z0 Platform SDK Architecture
Overview
Section titled “Overview”The z0 platform implements a three-layer architecture that separates domain-agnostic infrastructure (the SDK) from z0-specific business logic. This separation enables the platform to be extended for multiple domains while maintaining a clean, reusable foundation.
The Three Layers:
┌─────────────────────────────────────────────┐│ API Layer (src/api/) ││ - HTTP routing and middleware ││ - Request/response handling ││ - Authentication and authorization │└──────────────────┬──────────────────────────┘ │┌──────────────────▼──────────────────────────┐│ Domain Layer (src/domain/) ││ - z0-specific entity implementations ││ - Domain manifest and schema mappings ││ - Fact event handlers ││ - Business logic for Account, Asset, etc. │└──────────────────┬──────────────────────────┘ │┌──────────────────▼──────────────────────────┐│ z0 Layer (src/z0/) ││ - Generic EntityLedger base class ││ - LedgerRegistry for dynamic routing ││ - Core primitives (Entity, Fact, Config) ││ - Reusable utilities (auth, errors, id) ││ - Platform APIs (facts, configs) │└─────────────────────────────────────────────┘Core Primitives (domain-agnostic):
- Entity: Anything with identity participating in economic activity
- Fact: Immutable record of something that happened
- Config: Versioned setting governing behavior
Platform Layer (src/z0/)
Section titled “Platform Layer (src/z0/)”The platform layer provides domain-agnostic infrastructure that can be used for any product built on the Entity-Fact-Config model.
Core Components
Section titled “Core Components”EntityLedger Base Class
Section titled “EntityLedger Base Class”File: src/z0/ledgers/EntityLedger.ts
The foundational Durable Object class implementing the ledger pattern:
import { EntityLedger } from './z0/ledgers/EntityLedger';
export abstract class EntityLedger<TEnv extends EntityLedgerEnv = EntityLedgerEnv> extends DurableObject {
// Append-only fact storage protected async appendFact(fact: Partial<Fact>): Promise<Fact>
// Versioned config management protected async createConfig(config: Partial<Config>): Promise<Config> protected async updateConfig(id: string, updates: any): Promise<Config>
// Disposable cached state protected getCachedState<T>(key: string): T | null protected setCachedState<T>(key: string, value: T, factsThrough?: string): void protected deleteCachedState(key: string): void
// Entity CRUD protected getEntity(): Entity | null protected updateEntity(updates: Partial<Entity>): Promise<Entity>
// Facts query protected getFacts(filters?: { type?: string; since?: number }): Fact[]}Key Features:
- SQLite-backed durable storage
- Immutable fact ledger with append-only pattern
- Generic schema with index slots (
ix_s_*,ix_n_*) - Automatic replication to D1 (via queue)
- Idempotency support via
Idempotency-Keyheaders - Disposable cached state for performance
LedgerRegistry Pattern
Section titled “LedgerRegistry Pattern”File: src/z0/registry.ts
Central registry for mapping entity types to Durable Object classes:
import { LedgerRegistry } from './z0/registry';
// Register domain manifestLedgerRegistry.register(z0Manifest);
// In API routes - dynamically resolve namespaceconst namespace = LedgerRegistry.getNamespace(env, 'account');// Returns: env.ACCOUNT_LEDGER
const doId = namespace.idFromName(entityId);const stub = namespace.get(doId);Key Methods:
register(manifest)- Register domain manifest at startupgetNamespace(env, type)- Get DO namespace for entity typegetDefinition(type)- Get entity schema definitionregisterHandler(name, fn)- Register fact event handlers
Primitives Types
Section titled “Primitives Types”File: src/z0/types/primitives.ts
Core type definitions for Entity, Fact, and Config:
import type { Entity, Fact, Config } from './z0/types/primitives';
export interface Entity { id: string; type: string; tenant_id?: string; version: number; // Optimistic concurrency control metadata: Record<string, unknown>; created_at: number; updated_at: number; [key: string]: any; // Extension point for domain fields}
export interface Fact { id: string; type: string; subtype?: string; tenant_id: string; entity_id?: string; source_id?: string; trace_id?: string; timestamp: number; data: Record<string, unknown>; created_at: number;}
export interface Config { id: string; version: number; // Immutable version number type: string; scope_type: string; // global, tenant, account, etc. scope_id?: string; settings: Record<string, unknown>; status: string; // active, archived superseded_at?: number; created_at: number;}Core Utilities
Section titled “Core Utilities”Directory: src/z0/lib/
Domain-agnostic utility modules:
-
auth.ts: API key authentication with tenant extractionimport { authenticateRequest } from './z0/lib/auth';const auth = authenticateRequest(request);// auth.tenantId, auth.mode ('live', 'test', 'admin') -
errors.ts: Structured error handling (RFC 7807)import { EntityLedgerErrors as Errors } from './z0/lib/errors';throw Errors.badRequest('Missing required field: name');throw Errors.notFound('Entity', entityId);throw Errors.conflict('Version mismatch'); -
id.ts: Prefixed ID generation (ULID-based)import { generateId } from './z0/lib/id';const entityId = generateId('ent_'); // ent_01HQABCDEF...const factId = generateId('fct_');
Platform APIs
Section titled “Platform APIs”Directory: src/z0/api/
Generic REST APIs for primitives:
-
facts.ts: Fact append APIPOST /v1/facts- Append fact to entity ledgerGET /v1/facts/:id- Retrieve fact by ID
-
configs.ts: Config CRUD with versioningPOST /v1/configs- Create configPUT /v1/configs/:id- Update (creates new version)DELETE /v1/configs/:id- Soft delete (archive)GET /v1/configs/:id/versions- Version history
Domain Layer (src/domain/)
Section titled “Domain Layer (src/domain/)”The domain layer contains z0-specific implementations, business logic, and schema mappings.
Domain Manifest
Section titled “Domain Manifest”File: src/domain/manifest.ts
Declarative schema defining all z0 entity types and their properties:
import { z0Manifest } from './domain/manifest';
export const z0Manifest: DomainManifest = { name: 'z0', version: '1.1.0', entities: { account: { ledger: Account, // DO class description: 'Financial container for budgets and caps', fields: { status: { type: 'string', required: true, storage: 'ix_s_1' }, name: { type: 'string', required: true, storage: 'ix_s_2' }, subtype: { type: 'string', required: true, storage: 'ix_s_4' }, parent_id: { type: 'string', storage: 'ix_s_5' }, owner_id: { type: 'string', storage: 'ix_s_6' }, }, facts: ['deposit', 'charge', 'credit', 'payout', 'cost'], }, asset: { ledger: Asset, fields: { status: { type: 'string', required: true, storage: 'ix_s_1' }, name: { type: 'string', required: true, storage: 'ix_s_2' }, identifier: { type: 'string', required: true, storage: 'ix_s_3' }, subtype: { type: 'string', required: true, storage: 'ix_s_4' }, parent_id: { type: 'string', storage: 'ix_s_5' }, }, facts: ['invocation', 'outcome'], }, // ... contact, deal, campaign, tool, vendor, user }, subscriptions: [ { name: 'z0-no-answer-alert', patterns: ['outcome.no_answer'], handler: 'z0-no-answer-alert', } ]};Generic Schema Pattern
Section titled “Generic Schema Pattern”File: src/domain/schema.ts
The platform uses a generic entity schema with flexible index slots instead of hardcoded columns:
// Database schema (generic)CREATE TABLE entities ( id TEXT PRIMARY KEY, tenant_id TEXT, type TEXT NOT NULL, data TEXT NOT NULL DEFAULT '{}', -- Full JSON payload
-- Generic Index Slots (queryable fields) ix_s_1 TEXT, -- String slot 1 ix_s_2 TEXT, -- String slot 2 ix_s_3 TEXT, -- String slot 3 ... -- ix_s_4 through ix_s_10
ix_n_1 REAL, -- Number slot 1 ... -- ix_n_2 through ix_n_5
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);
// Domain mapping (z0-specific)export const Schema = { Asset: { Status: 'ix_s_1', // Asset.Status → ix_s_1 Name: 'ix_s_2', // Asset.Name → ix_s_2 Identifier: 'ix_s_3', // Asset.Identifier → ix_s_3 (phone, email, etc.) Subtype: 'ix_s_4', // Asset.Subtype → ix_s_4 ParentId: 'ix_s_5', // Asset.ParentId → ix_s_5 }, // ... other entity types};
// Usage in queriesconst query = ` SELECT * FROM entities WHERE type = 'asset' AND ${Schema.Asset.Status} = 'active' AND ${Schema.Asset.Identifier} = ?`;Benefits:
- No schema migrations when adding new entity types
- Multiple domains can coexist with different field mappings
- Full entity data stored in
dataJSON column - Queryable fields promoted to index slots for performance
Domain Entity Classes
Section titled “Domain Entity Classes”Directory: src/domain/entities/
z0-specific Durable Object implementations extending EntityLedger:
Account.ts
Section titled “Account.ts”import { Account } from './domain/entities/Account';
export class Account extends EntityLedger { // Domain-specific cached state getBudgetState(): BudgetState { const cached = this.getCachedState<BudgetState>('BudgetState'); if (cached) return cached; return this.recomputeBudgetState(); }
// Domain-specific fact recording async recordDeposit(amount: number, currency = 'USD'): Promise<Fact> { return this.appendFact({ type: 'deposit', subtype: 'manual', amount, currency, }); }
// Custom HTTP endpoints override async fetch(request: Request): Promise<Response> { await this.ensureInitialized(); const url = new URL(request.url);
if (url.pathname === '/budget' && request.method === 'GET') { return Response.json(this.getBudgetState()); }
if (url.pathname === '/deposit' && request.method === 'POST') { const body = await request.json(); const fact = await this.recordDeposit(body.amount, body.currency); return Response.json(fact, { status: 201 }); }
return super.fetch(request); }}Other Domain Entities:
Asset.ts- Marketing assets (phone numbers, URLs)Contact.ts- External contacts/leadsDeal.ts- Business opportunitiesCallLedger.ts- Twilio call state machine (product-specific)
API Layer (src/api/)
Section titled “API Layer (src/api/)”HTTP interface for the platform, using Hono framework.
Main Entry Point
Section titled “Main Entry Point”File: src/index.ts
import { Hono } from 'hono';import { authMiddleware } from './api/middleware/auth';import { LedgerRegistry } from './z0/registry';import { z0Manifest } from './domain/manifest';import { entities } from './api/routes/entities';import { facts } from './z0/api/facts';import { configs } from './z0/api/configs';
// Register domain manifestLedgerRegistry.register(z0Manifest);
const app = new Hono<HonoEnv>();
// Middlewareapp.use('*', authMiddleware);
// Platform routesapp.route('/v1/facts', facts);app.route('/v1/configs', configs);
// Application routesapp.route('/v1/entities', entities);
export default app;Entities API
Section titled “Entities API”File: src/api/routes/entities.ts
Generic entity CRUD with dynamic DO routing:
import { entities } from './api/routes/entities';
// POST /v1/entities - Create entityentities.post('/', async (c) => { const body = await c.req.json(); const entityId = body.id ?? generateId('ent_');
// Dynamic namespace resolution via LedgerRegistry const namespace = LedgerRegistry.getNamespace(c.env, body.type); if (!namespace) { throw Errors.badRequest(`Unknown entity type: ${body.type}`); }
const doId = namespace.idFromName(entityId); const stub = namespace.get(doId);
return stub.fetch(new Request('https://do/init', { method: 'POST', headers: { 'X-Tenant-ID': reqCtx.tenantId, 'Idempotency-Key': c.req.header('Idempotency-Key') ?? '' }, body: JSON.stringify({ ...body, id: entityId }) }));});
// GET /v1/entities/:id?entity_type=accountentities.get('/:id', async (c) => { const entityType = c.req.query('entity_type'); if (!entityType) throw Errors.badRequest('entity_type required');
const namespace = LedgerRegistry.getNamespace(c.env, entityType); const doId = namespace.idFromName(c.req.param('id')); const stub = namespace.get(doId);
return stub.fetch(new Request('https://do/', { headers: { 'X-Tenant-ID': reqCtx.tenantId } }));});Key Changes from Old Architecture:
- Entity type passed as query parameter (
entity_type=account) - LedgerRegistry dynamically resolves DO namespace
- No hardcoded switch statements for entity types
Middleware
Section titled “Middleware”Directory: src/api/middleware/
-
auth.ts: Authentication and tenant extractionimport { authMiddleware } from './api/middleware/auth';app.use('*', authMiddleware); -
errors.ts: Error handling wrapperimport { Errors } from './api/middleware/errors';throw Errors.notFound('Entity', id);
File Structure Reference
Section titled “File Structure Reference”Complete Directory Tree
Section titled “Complete Directory Tree”src/├── z0/ # Domain-agnostic SDK layer│ ├── ledgers/│ │ └── EntityLedger.ts # Base DO class│ ├── lib/│ │ ├── auth.ts # Authentication│ │ ├── errors.ts # Error handling│ │ ├── id.ts # ID generation│ │ └── router.ts # HTTP routing (deprecated)│ ├── types/│ │ ├── primitives.ts # Entity, Fact, Config types│ │ └── ledger.ts # Ledger-specific types│ ├── api/│ │ ├── facts.ts # Facts REST API│ │ └── configs.ts # Configs REST API│ └── registry.ts # LedgerRegistry pattern│├── domain/ # z0-specific implementations│ ├── entities/│ │ ├── Account.ts # Account DO│ │ ├── Asset.ts # Asset DO│ │ ├── Contact.ts # Contact DO│ │ ├── Deal.ts # Deal DO│ │ └── CallLedger.ts # Twilio call DO│ ├── lib/│ │ ├── twilio.ts # Twilio utilities│ │ └── validation.ts # z0-specific validation│ ├── schema.ts # Index slot mappings│ ├── manifest.ts # z0Manifest definition│ ├── handlers.ts # Fact event handlers│ └── types/│ └── index.ts # z0-specific types│├── api/ # HTTP interface layer│ ├── middleware/│ │ ├── auth.ts # Auth middleware│ │ ├── tenant.ts # Tenant middleware│ │ └── errors.ts # Error middleware│ ├── routes/│ │ ├── entities.ts # Entity CRUD│ │ └── voice.ts # Voice product API│ ├── webhooks/│ │ └── twilio.ts # Twilio webhooks│ └── router.ts # Main router (deprecated)│├── queues/ # Queue handlers│ ├── replication.ts # DO → D1 replication│ ├── webhooks.ts # Webhook delivery│ └── fact-events.ts # Event pub/sub│├── db/ # Database layer│ ├── schema.ts # D1 schema│ └── queries.ts # Query utilities│├── types/ # Global types│ ├── env.ts # Environment bindings│ └── index.ts # Type re-exports│└── index.ts # Main entry pointImport Path Changes
Section titled “Import Path Changes”Old paths (pre-refactoring):
import { EntityLedger } from './z0/ledgers/EntityLedger';import { Account } from './domain/entities/Account';import { Entity, Fact, Config } from './z0/types/primitives';import { authenticateRequest } from './z0/lib/auth';import { generateId } from './z0/lib/id';New paths (current):
import { EntityLedger } from './z0/ledgers/EntityLedger';import { Account } from './domain/entities/Account';import { Entity, Fact, Config } from './z0/types/primitives';import { authenticateRequest } from './z0/lib/auth';import { generateId } from './z0/lib/id';Environment Bindings
Section titled “Environment Bindings”wrangler.toml changes:
# Old bindings[[durable_objects.bindings]]name = "ACCOUNT_DO"class_name = "AccountDO"
# New bindings[[durable_objects.bindings]]name = "ACCOUNT_LEDGER"class_name = "Account"All DO Bindings:
ENTITY_LEDGER→ Generic EntityLedgerACCOUNT_LEDGER→ AccountASSET_LEDGER→ AssetCONTACT_LEDGER→ ContactDEAL_LEDGER→ DealCALL_LEDGER→ CallLedger (z0-specific)
Key Architectural Patterns
Section titled “Key Architectural Patterns”1. Ledger Pattern (EntityLedger)
Section titled “1. Ledger Pattern (EntityLedger)”All entities are implemented as Durable Objects extending EntityLedger:
export class MyEntity extends EntityLedger { // Cached state (disposable) getMyState(): MyState { const cached = this.getCachedState<MyState>('MyState'); if (cached) return cached; return this.recomputeMyState(); }
private recomputeMyState(): MyState { const facts = this.getFacts(); // Fold facts into state const state = facts.reduce(/* ... */); this.setCachedState('MyState', state, lastFactId); return state; }
// Fact recording async recordEvent(data: any): Promise<Fact> { return this.appendFact({ type: 'event', data }); }
// Cache invalidation on new facts protected override async updateCachedState(fact: Fact): Promise<void> { if (fact.type === 'event') { this.deleteCachedState('MyState'); } }}2. Registry Pattern (LedgerRegistry)
Section titled “2. Registry Pattern (LedgerRegistry)”Dynamic entity type → DO namespace resolution:
// 1. Define domain manifestconst manifest: DomainManifest = { name: 'my-domain', entities: { 'my-entity': { ledger: MyEntityDO, fields: { name: { type: 'string', storage: 'ix_s_2' } } } }};
// 2. Register at startupLedgerRegistry.register(manifest);
// 3. Resolve dynamically in API routesconst namespace = LedgerRegistry.getNamespace(env, 'my-entity');// Returns: env.MY_ENTITY_LEDGER3. Generic Schema Pattern
Section titled “3. Generic Schema Pattern”Store domain fields in index slots for queryability:
// Manifest definitionfields: { status: { type: 'string', storage: 'ix_s_1' }, email: { type: 'string', storage: 'ix_s_3' },}
// SQL query using schema mappingconst query = ` SELECT * FROM entities WHERE ${Schema.MyEntity.Status} = 'active' AND ${Schema.MyEntity.Email} = ?`;// Expands to: WHERE ix_s_1 = 'active' AND ix_s_3 = ?4. Tenant Isolation
Section titled “4. Tenant Isolation”All queries filter by tenant_id:
// Database query functionsexport async function getEntity( db: D1Database, id: string, tenantId: string // Mandatory parameter): Promise<EntityRow | null> { return db.prepare(` SELECT * FROM entities WHERE id = ? AND tenant_id = ? // Always filter `).bind(id, tenantId).first();}
// API routes extract tenant from authconst auth = c.get('auth');const entity = await getEntity(db, id, auth.tenantId);Migration Guide
Section titled “Migration Guide”From Old Architecture
Section titled “From Old Architecture”1. Update imports:
# Find/replace old import pathssed -i '' 's|from "./durable-objects/|from "./z0/ledgers/|g' **/*.tssed -i '' 's|from "./lib/|from "./z0/lib/|g' **/*.ts2. Update DO class names:
// Beforeexport class AccountDO extends EntityLedger { }
// Afterexport class Account extends EntityLedger { }3. Update environment bindings:
// Beforeenv.ACCOUNT_DO
// Afterenv.ACCOUNT_LEDGER4. Update type imports:
// Beforeimport type { EntityType, FactType } from './types';
// Afterimport type { z0EntityType, z0FactType } from './domain/types';// Or for generic types:import type { Entity, Fact, Config } from './z0/types/primitives';Adding a New Domain
Section titled “Adding a New Domain”To add a new domain (e.g., “CRM”):
1. Create domain manifest:
export const crmManifest: DomainManifest = { name: 'crm', version: '1.0.0', entities: { lead: { ledger: Lead, fields: { status: { type: 'string', storage: 'ix_s_1' }, name: { type: 'string', storage: 'ix_s_2' }, } } }};2. Create entity DO:
import { EntityLedger } from '../../z0/ledgers/EntityLedger';
export class Lead extends EntityLedger { // CRM-specific logic}3. Register manifest:
import { crmManifest } from './domains/crm/manifest';
LedgerRegistry.register(crmManifest);4. Add DO binding:
[[durable_objects.bindings]]name = "LEAD_LEDGER"class_name = "Lead"Dependency Graph
Section titled “Dependency Graph”index.ts (entry point)├── z0/registry.ts (LedgerRegistry)│ └── domain/manifest.ts (z0Manifest)│ └── domain/entities/*.ts (Account, Asset, etc.)│ └── z0/ledgers/EntityLedger.ts│ └── z0/types/primitives.ts├── z0/api/*.ts (facts, configs)│ └── z0/lib/*.ts (auth, errors, id)├── api/routes/*.ts (entities, voice)├── api/middleware/*.ts (auth, errors)└── queues/*.ts (replication, webhooks)Clean Separation:
- z0 layer has no knowledge of z0 domain
- Domain layer depends on platform abstractions
- API layer orchestrates both layers
- No circular dependencies
Testing Considerations
Section titled “Testing Considerations”Entity Tests:
// Before: Direct DO class instantiationconst stub = env.ACCOUNT_DO.get(id);
// After: Use LedgerRegistryconst namespace = LedgerRegistry.getNamespace(env, 'account');const stub = namespace.get(id);Test Environment:
// Update binding namesconst env = getMiniflareBindings();env.ACCOUNT_LEDGER // not env.ACCOUNT_DOPerformance Characteristics
Section titled “Performance Characteristics”LedgerRegistry Lookup: O(1) HashMap lookup, negligible overhead
Generic Schema:
- Full entity in
dataJSON column (no joins needed) - Indexed slots for fast queries (same as dedicated columns)
- Minimal storage overhead (~10 empty slots per row)
DO Initialization: Schema creation only on first access (lazy)
Cached State: In-memory after first computation, invalidated on relevant facts
Future Enhancements
Section titled “Future Enhancements”Planned Features:
- Cross-domain entity references (Account in z0 → Lead in CRM)
- Plugin system for domain registration
- OpenAPI schema generation from manifests
- SDK type generation from domain manifests
- Migration tooling for schema evolution
Document Version: 2.0 Last Updated: 2026-01-19 Reflects: SDK Separation Refactoring (feature/sdk-separation branch) Status: Complete