Skip to content

z0 Platform SDK Architecture

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

The platform layer provides domain-agnostic infrastructure that can be used for any product built on the Entity-Fact-Config model.

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-Key headers
  • Disposable cached state for performance

File: src/z0/registry.ts

Central registry for mapping entity types to Durable Object classes:

import { LedgerRegistry } from './z0/registry';
// Register domain manifest
LedgerRegistry.register(z0Manifest);
// In API routes - dynamically resolve namespace
const 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 startup
  • getNamespace(env, type) - Get DO namespace for entity type
  • getDefinition(type) - Get entity schema definition
  • registerHandler(name, fn) - Register fact event handlers

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;
}

Directory: src/z0/lib/

Domain-agnostic utility modules:

  • auth.ts: API key authentication with tenant extraction

    import { 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_');

Directory: src/z0/api/

Generic REST APIs for primitives:

  • facts.ts: Fact append API

    • POST /v1/facts - Append fact to entity ledger
    • GET /v1/facts/:id - Retrieve fact by ID
  • configs.ts: Config CRUD with versioning

    • POST /v1/configs - Create config
    • PUT /v1/configs/:id - Update (creates new version)
    • DELETE /v1/configs/:id - Soft delete (archive)
    • GET /v1/configs/:id/versions - Version history

The domain layer contains z0-specific implementations, business logic, and schema mappings.

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',
}
]
};

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 queries
const 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 data JSON column
  • Queryable fields promoted to index slots for performance

Directory: src/domain/entities/

z0-specific Durable Object implementations extending EntityLedger:

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/leads
  • Deal.ts - Business opportunities
  • CallLedger.ts - Twilio call state machine (product-specific)

HTTP interface for the platform, using Hono framework.

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 manifest
LedgerRegistry.register(z0Manifest);
const app = new Hono<HonoEnv>();
// Middleware
app.use('*', authMiddleware);
// Platform routes
app.route('/v1/facts', facts);
app.route('/v1/configs', configs);
// Application routes
app.route('/v1/entities', entities);
export default app;

File: src/api/routes/entities.ts

Generic entity CRUD with dynamic DO routing:

import { entities } from './api/routes/entities';
// POST /v1/entities - Create entity
entities.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=account
entities.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

Directory: src/api/middleware/

  • auth.ts: Authentication and tenant extraction

    import { authMiddleware } from './api/middleware/auth';
    app.use('*', authMiddleware);
  • errors.ts: Error handling wrapper

    import { Errors } from './api/middleware/errors';
    throw Errors.notFound('Entity', id);

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 point

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';

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 EntityLedger
  • ACCOUNT_LEDGER → Account
  • ASSET_LEDGER → Asset
  • CONTACT_LEDGER → Contact
  • DEAL_LEDGER → Deal
  • CALL_LEDGER → CallLedger (z0-specific)

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');
}
}
}

Dynamic entity type → DO namespace resolution:

// 1. Define domain manifest
const manifest: DomainManifest = {
name: 'my-domain',
entities: {
'my-entity': {
ledger: MyEntityDO,
fields: { name: { type: 'string', storage: 'ix_s_2' } }
}
}
};
// 2. Register at startup
LedgerRegistry.register(manifest);
// 3. Resolve dynamically in API routes
const namespace = LedgerRegistry.getNamespace(env, 'my-entity');
// Returns: env.MY_ENTITY_LEDGER

Store domain fields in index slots for queryability:

// Manifest definition
fields: {
status: { type: 'string', storage: 'ix_s_1' },
email: { type: 'string', storage: 'ix_s_3' },
}
// SQL query using schema mapping
const query = `
SELECT * FROM entities
WHERE ${Schema.MyEntity.Status} = 'active'
AND ${Schema.MyEntity.Email} = ?
`;
// Expands to: WHERE ix_s_1 = 'active' AND ix_s_3 = ?

All queries filter by tenant_id:

// Database query functions
export 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 auth
const auth = c.get('auth');
const entity = await getEntity(db, id, auth.tenantId);

1. Update imports:

Terminal window
# Find/replace old import paths
sed -i '' 's|from "./durable-objects/|from "./z0/ledgers/|g' **/*.ts
sed -i '' 's|from "./lib/|from "./z0/lib/|g' **/*.ts

2. Update DO class names:

// Before
export class AccountDO extends EntityLedger { }
// After
export class Account extends EntityLedger { }

3. Update environment bindings:

// Before
env.ACCOUNT_DO
// After
env.ACCOUNT_LEDGER

4. Update type imports:

// Before
import type { EntityType, FactType } from './types';
// After
import type { z0EntityType, z0FactType } from './domain/types';
// Or for generic types:
import type { Entity, Fact, Config } from './z0/types/primitives';

To add a new domain (e.g., “CRM”):

1. Create domain manifest:

src/domains/crm/manifest.ts
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:

src/domains/crm/entities/Lead.ts
import { EntityLedger } from '../../z0/ledgers/EntityLedger';
export class Lead extends EntityLedger {
// CRM-specific logic
}

3. Register manifest:

src/index.ts
import { crmManifest } from './domains/crm/manifest';
LedgerRegistry.register(crmManifest);

4. Add DO binding:

wrangler.toml
[[durable_objects.bindings]]
name = "LEAD_LEDGER"
class_name = "Lead"

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

Entity Tests:

// Before: Direct DO class instantiation
const stub = env.ACCOUNT_DO.get(id);
// After: Use LedgerRegistry
const namespace = LedgerRegistry.getNamespace(env, 'account');
const stub = namespace.get(id);

Test Environment:

// Update binding names
const env = getMiniflareBindings();
env.ACCOUNT_LEDGER // not env.ACCOUNT_DO

LedgerRegistry Lookup: O(1) HashMap lookup, negligible overhead

Generic Schema:

  • Full entity in data JSON 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


Planned Features:

  1. Cross-domain entity references (Account in z0 → Lead in CRM)
  2. Plugin system for domain registration
  3. OpenAPI schema generation from manifests
  4. SDK type generation from domain manifests
  5. Migration tooling for schema evolution

Document Version: 2.0 Last Updated: 2026-01-19 Reflects: SDK Separation Refactoring (feature/sdk-separation branch) Status: Complete