Cookbook
Copy-paste recipes for common tasks.
Recipe 1: Create Your First Ledger
Section titled “Recipe 1: Create Your First Ledger”Task: Build a basic account ledger with balance tracking.
import { EntityLedger, type Fact, type EntityLedgerEnv } from '@z0-app/sdk';
export class AccountLedger extends EntityLedger<EntityLedgerEnv> { protected async updateCachedState(fact: Fact): Promise<void> { const balance = this.getCachedState<number>('balance') ?? 0;
switch (fact.type) { case 'deposit': this.setCachedState('balance', balance + (fact.data.amount as number)); break; case 'withdrawal': this.setCachedState('balance', balance - (fact.data.amount as number)); break; } }}
// wrangler.tomlexport default { durable_objects: { bindings: [ { name: "ACCOUNT_LEDGER", class_name: "AccountLedger", script_name: "worker" } ] }}Recipe 2: Use LedgerClient for Type-Safe Operations
Section titled “Recipe 2: Use LedgerClient for Type-Safe Operations”Task: Interact with a ledger from your Worker.
import { LedgerClient } from '@z0-app/sdk';
export default { async fetch(request: Request, env: { ACCOUNT_LEDGER: DurableObjectNamespace }) { const client = new LedgerClient(env.ACCOUNT_LEDGER);
// Append a fact await client.emit('user_123', 'deposit', { amount: 100.00 });
// Get entity state const entity = await client.get<{ balance: number }>('user_123');
return Response.json({ balance: entity.data.balance }); }}Recipe 3: Define Schema with Builders
Section titled “Recipe 3: Define Schema with Builders”Task: Type-safe entity definition with validation.
import { schemaBuilders, registerEntity, buildManifest } from '@z0-app/sdk';
const account = schemaBuilders.entity('account', { email: schemaBuilders.string().email().required(), balance: schemaBuilders.number().min(0).default(0), status: schemaBuilders.enum(['active', 'suspended', 'closed']).default('active'), created_by: schemaBuilders.string().required(),});
// Register factsaccount.fact('deposit', { amount: schemaBuilders.number().positive().required(), source: schemaBuilders.string(),});
account.fact('withdrawal', { amount: schemaBuilders.number().positive().required(), destination: schemaBuilders.string(),});
// Register entityregisterEntity(account);
// Generate manifestconst manifest = buildManifest({ domain: 'banking', version: '1.0.0' });console.log(manifest);Recipe 4: Use YAML Manifest Instead
Section titled “Recipe 4: Use YAML Manifest Instead”Task: Define domain model in YAML (no TypeScript needed).
name: bankingversion: 1.0.0
entities: account: description: Bank account fields: email: type: string required: true validation: email balance: type: number default: 0 validation: min: 0 status: type: enum values: [active, suspended, closed] default: active
facts: deposit: description: Deposit money data: amount: type: number required: true validation: min: 0.01 source: type: string
withdrawal: description: Withdraw money data: amount: type: number required: true validation: min: 0.01 destination: type: string// Load manifestimport { parseManifest, LedgerRegistry } from '@z0-app/sdk';import { readFileSync } from 'fs';
const yaml = readFileSync('domain.yaml', 'utf-8');const manifest = parseManifest(yaml);LedgerRegistry.register(manifest);Recipe 5: Hierarchical Entities (Parent-Child)
Section titled “Recipe 5: Hierarchical Entities (Parent-Child)”Task: Create organization → team → user hierarchy.
import { LedgerClient, validateHierarchyDepth } from '@z0-app/sdk';
const client = new LedgerClient(env.ENTITY_LEDGER);
// Create organization (root)await client.stub('org_abc').upsertEntity({ type: 'organization', data: { name: 'Acme Corp' }});
// Create team (child)await client.stub('team_xyz').upsertEntity({ type: 'team', parent_id: 'org_abc', data: { name: 'Engineering' }});
// Create user (grandchild)await client.stub('user_123').upsertEntity({ type: 'user', parent_id: 'team_xyz', data: { name: 'Alice', email: 'alice@example.com' }});
// Validate depth before creating deeper entitiesconst validation = await validateHierarchyDepth('team_xyz', 2, { maxDepth: 10 });if (!validation.valid) { throw new Error(`Hierarchy depth limit: ${validation.error}`);}Recipe 6: Real-Time Subscriptions (WebSocket)
Section titled “Recipe 6: Real-Time Subscriptions (WebSocket)”Task: Stream entity changes to client.
// Server-side: EntityLedger subclassimport { EntityLedger, type Fact } from '@z0-app/sdk';
export class DashboardLedger extends EntityLedger { protected async afterFactAppended(fact: Fact): Promise<void> { // Broadcast to WebSocket subscribers const subscribers = this.subscriptions.getSubscribers('facts'); for (const connId of subscribers) { const ws = this.subscriptions.getConnection(connId); ws?.send(JSON.stringify({ type: 'fact_appended', fact })); } }}
// Client-side: JavaScriptconst ws = new WebSocket('wss://your-worker.dev/entity/dashboard_123/ws');
ws.onopen = () => { ws.send(JSON.stringify({ type: 'subscribe', channels: ['facts', 'state'] }));};
ws.onmessage = (event) => { const message = JSON.parse(event.data); if (message.type === 'fact_appended') { console.log('New fact:', message.fact); }};Recipe 7: Real-Time Subscriptions (Server-Sent Events)
Section titled “Recipe 7: Real-Time Subscriptions (Server-Sent Events)”Task: Stream updates for simpler clients (no WebSocket support).
// Client-side: Browser EventSourceconst sse = new EventSource('/entity/dashboard_123/sse?channels=facts,state');
sse.addEventListener('fact_appended', (event) => { const fact = JSON.parse(event.data); console.log('New fact:', fact);});
sse.addEventListener('state_updated', (event) => { const state = JSON.parse(event.data); console.log('State updated:', state);});
sse.onerror = () => { console.error('SSE connection error'); sse.close();};Recipe 8: Projections from YAML
Section titled “Recipe 8: Projections from YAML”Task: Aggregate facts into derived state without coding.
projections: daily_revenue: description: Total revenue per day source_facts: - payment.completed aggregations: - function: sum field: data.amount output: total - function: count output: transaction_count time_window: duration: 1d alignment: calendar_dayimport { ProjectionEngine } from '@z0-app/sdk';
const engine = new ProjectionEngine(projectionYaml);
// Process factconst fact = { type: 'payment', subtype: 'completed', data: { amount: 99.00 } };const result = engine.processFact(fact);
console.log(result);// { daily_revenue: { total: 99.00, transaction_count: 1 } }Recipe 9: Budget Enforcement with MeterEngine
Section titled “Recipe 9: Budget Enforcement with MeterEngine”Task: Limit API calls per tenant with soft/hard limits.
meters: api_calls: description: API request count source_facts: - api.request aggregation: function: count window: duration: 1d budget: soft_limit: 1000 hard_limit: 1200 overage: strategy: rejectimport { MeterEngine } from '@z0-app/sdk';
const meter = new MeterEngine(meterYaml);
// Check budget before processingconst check = meter.checkBudget('api_calls', 1, { current_value: 999 });
if (check.allowed) { // Process request await processApiRequest();} else { return new Response('Rate limit exceeded', { status: 429 });}Recipe 10: Webhook Delivery with Retry
Section titled “Recipe 10: Webhook Delivery with Retry”Task: Send webhooks for fact events with automatic retry.
import { buildWebhookPayload, signPayload, calculateBackoff, isRetryableError} from '@z0-app/sdk';
async function deliverWebhook(fact: Fact, webhookUrl: string, secret: string) { const deliveryId = crypto.randomUUID(); const payload = buildWebhookPayload(fact, 'fact.appended', deliveryId, 1); const timestamp = Date.now(); const signature = await signPayload(JSON.stringify(payload), timestamp, secret);
let attempt = 0; const maxAttempts = 5;
while (attempt < maxAttempts) { try { const response = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Webhook-Signature': signature, 'X-Webhook-Timestamp': timestamp.toString(), 'X-Delivery-ID': deliveryId, }, body: JSON.stringify(payload), });
if (response.ok) { return { success: true }; }
if (!isRetryableError(response.status)) { return { success: false, error: 'Non-retryable error' }; } } catch (error) { // Network error, retry }
attempt++; const backoffMs = calculateBackoff(attempt, { base: 1000, max: 60000 }); await new Promise(resolve => setTimeout(resolve, backoffMs)); }
return { success: false, error: 'Max retries exceeded' };}Recipe 11: Validate Incoming Webhooks
Section titled “Recipe 11: Validate Incoming Webhooks”Task: Route Twilio/Stripe/SendGrid webhooks to correct tenant.
import { validateWebhookOwnership, createDefaultProviderRegistry, twilioParser, stripeParser} from '@z0-app/sdk';
const registry = createDefaultProviderRegistry();
export default { async fetch(request: Request, env: Env) { const url = new URL(request.url);
// Twilio webhook if (url.pathname === '/webhooks/twilio') { const result = await validateWebhookOwnership(request, 'twilio', registry, env);
if (!result.valid) { return new Response('Forbidden', { status: 403 }); }
// Route to tenant's ledger const client = new LedgerClient(env.TENANT_LEDGER); await client.emit(result.tenant_id!, 'call.received', result.parsed);
return new Response('OK'); } }}Recipe 12: Idempotent Fact Append
Section titled “Recipe 12: Idempotent Fact Append”Task: Prevent duplicate facts from retries.
import { LedgerClient } from '@z0-app/sdk';
const client = new LedgerClient(env.LEDGER);
// Use idempotency keyconst idempotencyKey = `payment_${request.headers.get('X-Idempotency-Key')}`;
await client.stub('user_123').appendFact({ type: 'payment', subtype: 'completed', data: { amount: 99.00 }, idempotency_key: idempotencyKey,});
// Duplicate requests with same key are ignoredRecipe 13: Circuit Breaker for Parent Calls
Section titled “Recipe 13: Circuit Breaker for Parent Calls”Task: Protect parent DO from cascading failures.
import { ParentDOClient } from '@z0-app/sdk';
export class ChildLedger extends EntityLedger { private parentClient?: ParentDOClient;
async fetch(request: Request): Promise<Response> { const entity = await this.getEntity();
if (entity.parent_id) { this.parentClient = new ParentDOClient( this.env.PARENT_LEDGER.get(this.env.PARENT_LEDGER.idFromName(entity.parent_id)), { timeout: 5000, maxRetries: 3 } );
try { const response = await this.parentClient.fetch('/budget/check', { method: 'POST', body: JSON.stringify({ amount: 100 }) });
const result = await response.json(); // Process result... } catch (error) { // Circuit is open, use fallback console.warn('Parent unavailable, using local budget'); } }
return super.fetch(request); }}Recipe 14: Temporal Resource Binding
Section titled “Recipe 14: Temporal Resource Binding”Task: Assign phone numbers to entities with release/quarantine.
import { canAssignResource, buildAssignmentFact, buildReleaseFact, getActiveBinding, PHONE_NUMBER_CONFIG} from '@z0-app/sdk';
// Check if phone number can be assigned nowconst canAssign = await canAssignResource( client, 'resource_pool_123', '+15551234567', 'user_456', Date.now(), PHONE_NUMBER_CONFIG);
if (canAssign) { // Build assignment fact const assignFact = buildAssignmentFact( 'resource_pool_123', '+15551234567', 'user_456', { purpose: 'customer_calls' } );
await client.emit('resource_pool_123', assignFact.type, assignFact.data);}
// Later: release the resourceconst releaseFact = buildReleaseFact( 'resource_pool_123', '+15551234567', { reason: 'user_requested' });
await client.emit('resource_pool_123', releaseFact.type, releaseFact.data);
// Check active bindingconst binding = await getActiveBinding(client, 'resource_pool_123', '+15551234567');console.log(binding); // { resource, entity_id: 'user_456', assigned_at, ... }Recipe 15: Tiered Pricing Calculation
Section titled “Recipe 15: Tiered Pricing Calculation”Task: Calculate graduated pricing (like Stripe).
import { calculateTieredCharge, type Tier } from '@z0-app/sdk';
const tiers: Tier[] = [ { ending_quantity: 100, unit_price_cents: 200 }, // $2.00/unit for 1-100 { ending_quantity: 1000, unit_price_cents: 100 }, // $1.00/unit for 101-1000 { ending_quantity: null, unit_price_cents: 50 }, // $0.50/unit for 1001+];
const result = calculateTieredCharge(1500, tiers);
console.log(result);// {// total_charge_cents: 135000, // $1,350.00// tier_breakdown: [// { tier: 0, quantity: 100, unit_price_cents: 200, charge_cents: 20000 },// { tier: 1, quantity: 900, unit_price_cents: 100, charge_cents: 90000 },// { tier: 2, quantity: 500, unit_price_cents: 50, charge_cents: 25000 },// ]// }Recipe 16: Threshold Monitoring
Section titled “Recipe 16: Threshold Monitoring”Task: Detect when metrics cross thresholds (with debounce).
import { ThresholdMonitor } from '@z0-app/sdk';
const monitor = new ThresholdMonitor();
// Track balance crossing $500 thresholdconst check = monitor.check('balance', 1000, 400, 500);
console.log(check);// 'crossed' (dropped from $1000 to $400, crossing $500)
// Subsequent checks won't trigger until balance goes back above $500const check2 = monitor.check('balance', 400, 350, 500);console.log(check2); // 'none' (already below, no new cross)Recipe 17: Batch Operations with Resumability
Section titled “Recipe 17: Batch Operations with Resumability”Task: Process large fact lists with automatic resumption.
import { BatchExecutor, type BatchProcessor } from '@z0-app/sdk';
const processor: BatchProcessor<Fact> = async (fact) => { // Process each fact await client.emit(fact.entity_id!, fact.type, fact.data);};
const executor = new BatchExecutor(processor, { batchSize: 100, parallel: 10, retryAttempts: 3,});
const facts = [...]; // Large array of facts
const result = await executor.execute(facts);
if (!result.completed) { // Save progress for later resumption await env.KV.put('batch_progress', JSON.stringify(result.progress));}Recipe 18: Health Check for Data Integrity
Section titled “Recipe 18: Health Check for Data Integrity”Task: Compare entity state with parent for corruption detection.
import { compareStates, createNumericCheck } from '@z0-app/sdk';
const childEntity = await client.get('child_123');const parentEntity = await client.get('parent_456');
const result = compareStates( { balance: childEntity.data.balance }, { balance: parentEntity.data.child_balance }, { balance: createNumericCheck('balance', 0.01), // Allow 1 cent tolerance });
if (!result.consistent) { console.error('Data divergence:', result.divergences); // Trigger reconciliation...}Recipe 19: GDPR Anonymization
Section titled “Recipe 19: GDPR Anonymization”Task: Anonymize entity data for GDPR compliance.
import { LedgerClient } from '@z0-app/sdk';
const client = new LedgerClient(env.ACCOUNT_LEDGER);
// Request anonymizationawait client.stub('user_123').anonymize({ reason: 'gdpr_request', request_id: 'gdpr_2024_001', fields_to_clear: ['email', 'phone', 'address'], fields_to_hash: ['account_id'],});
// Entity is now anonymizedconst entity = await client.get('user_123');console.log(entity.anonymized_at); // timestampconsole.log(entity.data.email); // null (cleared)Recipe 20: Fact Schema Versioning
Section titled “Recipe 20: Fact Schema Versioning”Task: Evolve fact schemas over time with automatic migration.
import { registerSchema, migrateFact, z } from '@z0-app/sdk';
// Version 1registerSchema('payment', 1, z.object({ amount: z.number(),}));
// Version 2 (added currency field)registerSchema('payment', 2, z.object({ amount: z.number(), currency: z.string(),}), { migrations: { 1: (data) => ({ ...data, currency: 'USD' }) // Backfill }});
// Old fact (version 1)const oldFact = { type: 'payment', data: { amount: 100 }, version: 1 };
// Migrate to latestconst migrated = migrateFact(oldFact);console.log(migrated.data); // { amount: 100, currency: 'USD' }Recipe 21: Create API Gateway with Routes
Section titled “Recipe 21: Create API Gateway with Routes”Task: Define REST API routes in YAML manifest.
name: analytics-apiversion: 1.0.0
entities: pageview: description: Page view event fields: url: { type: string, indexed: true } duration_ms: { type: number } facts: - viewed
routes: - path: /v1/track method: POST entity: pageview action: emit factType: viewed auth: api_key rateLimit: { rpm: 1000, scope: tenant }
- path: /v1/pageviews method: GET entity: pageview action: list auth: api_key
- path: /health method: GET entity: system action: get auth: publicRecipe 22: Extend GatewayWorker
Section titled “Recipe 22: Extend GatewayWorker”Task: Create custom gateway with additional routes.
import { GatewayWorker, type GatewayWorkerEnv, parseManifest } from '@z0-app/sdk';import manifestYaml from './manifest.yaml';
const { manifest } = parseManifest(manifestYaml);
export class AnalyticsGateway extends GatewayWorker<GatewayWorkerEnv> { constructor(state: DurableObjectState, env: GatewayWorkerEnv) { super(state, env, manifest);
// Add custom batch endpoint this.router.post('/v1/batch', async (req) => { const events = await req.json(); const results = await Promise.all( events.map(e => this.client.emit(e.entityId, e.factType, e.data)) ); return Response.json({ data: { processed: results.length }, meta: { request_id: crypto.randomUUID(), timestamp: Date.now() } }); }); }
// Lifecycle hooks protected async beforeRoute(request: Request): Promise<void> { console.log(`[${new Date().toISOString()}] ${request.method} ${new URL(request.url).pathname}`); }
protected async afterRoute(request: Request, response: Response): Promise<Response> { const headers = new Headers(response.headers); headers.set('X-API-Version', '1.0.0'); return new Response(response.body, { status: response.status, headers }); }}Recipe 23: Generate Gateway Code from Manifest
Section titled “Recipe 23: Generate Gateway Code from Manifest”Task: Auto-generate Worker code from YAML manifest.
# Generate gateway Workernpx z0 generate gateway --manifest manifest.yaml --output src/generated
# Generate OpenAPI specnpx z0 generate openapi --manifest manifest.yaml --output openapi.json
# Validate manifestnpx z0 check --manifest manifest.yamlGenerated code structure:
import { GatewayWorker, parseManifest } from '@z0-app/sdk';
// ... routes registered from manifest// ... auth middleware for protected routes// ... rate limit middleware// ... /health endpointUse generated code:
export { GeneratedGateway as default } from './generated/gateway';Recipe 24: Use oRPC Contracts for Type Safety
Section titled “Recipe 24: Use oRPC Contracts for Type Safety”Task: Type-safe API requests with oRPC contracts.
import type { EmitRequest, EmitResponse, GetRequest, GetResponse, ListRequest, ListResponse,} from '@z0-app/sdk';
// Type-safe emit requestconst emitReq: EmitRequest = { entityId: 'pv_abc123', factType: 'viewed', data: { url: 'https://example.com', duration_ms: 5000 }};
const emitRes: EmitResponse = await fetch('/v1/track', { method: 'POST', headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify(emitReq)}).then(r => r.json());
console.log(emitRes.factId); // TypeScript knows this exists
// Type-safe list requestconst listReq: ListRequest = { entityType: 'pageview', limit: 100, cursor: 'cursor_abc'};
const listRes: ListResponse = await fetch('/v1/pageviews', { method: 'GET', headers: { 'X-API-Key': apiKey }}).then(r => r.json());
console.log(listRes.items.length); // Type-safeconsole.log(listRes.hasMore); // BooleanRecipe 25: Gateway Response Envelopes
Section titled “Recipe 25: Gateway Response Envelopes”Task: Handle standard API responses.
// Success response envelopeinterface SuccessResponse<T> { data: T; meta: { request_id: string; timestamp: number; };}
// RFC 7807 error responseinterface ErrorResponse { type: string; title: string; status: number; detail: string; instance: string; errors?: Array<{ field: string; code: string; message: string }>;}
// Parse responseconst response = await fetch('/v1/track', { /* ... */ });
if (response.ok) { const success: SuccessResponse<EmitResponse> = await response.json(); console.log(success.data.factId); console.log(success.meta.request_id); // For tracing} else { const error: ErrorResponse = await response.json(); console.error(error.detail); console.error(error.instance); // Contains request ID
// Field-level validation errors (400) if (error.errors) { error.errors.forEach(e => { console.error(`${e.field}: ${e.message}`); }); }}Summary
Section titled “Summary”| Recipe | Use Case |
|---|---|
| 1 | Create first ledger |
| 2 | Use LedgerClient |
| 3 | Schema builders |
| 4 | YAML manifest |
| 5 | Hierarchical entities |
| 6 | WebSocket subscriptions |
| 7 | SSE subscriptions |
| 8 | Projections from YAML |
| 9 | Budget enforcement |
| 10 | Webhook delivery |
| 11 | Incoming webhook validation |
| 12 | Idempotent append |
| 13 | Circuit breaker |
| 14 | Temporal resource binding |
| 15 | Tiered pricing |
| 16 | Threshold monitoring |
| 17 | Batch operations |
| 18 | Health checks |
| 19 | GDPR anonymization |
| 20 | Schema versioning |
| 21 | Create API Gateway with routes |
| 22 | Extend GatewayWorker |
| 23 | Generate gateway code |
| 24 | oRPC type-safe contracts |
| 25 | Gateway response envelopes |