Skip to content

Cookbook

Copy-paste recipes for common tasks.


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.toml
export 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 });
}
}

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 facts
account.fact('deposit', {
amount: schemaBuilders.number().positive().required(),
source: schemaBuilders.string(),
});
account.fact('withdrawal', {
amount: schemaBuilders.number().positive().required(),
destination: schemaBuilders.string(),
});
// Register entity
registerEntity(account);
// Generate manifest
const manifest = buildManifest({ domain: 'banking', version: '1.0.0' });
console.log(manifest);

Task: Define domain model in YAML (no TypeScript needed).

domain.yaml
name: banking
version: 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 manifest
import { 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 entities
const 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 subclass
import { 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: JavaScript
const 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 EventSource
const 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();
};

Task: Aggregate facts into derived state without coding.

projections.yaml
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_day
import { ProjectionEngine } from '@z0-app/sdk';
const engine = new ProjectionEngine(projectionYaml);
// Process fact
const 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.yaml
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: reject
import { MeterEngine } from '@z0-app/sdk';
const meter = new MeterEngine(meterYaml);
// Check budget before processing
const 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 });
}

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

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

Task: Prevent duplicate facts from retries.

import { LedgerClient } from '@z0-app/sdk';
const client = new LedgerClient(env.LEDGER);
// Use idempotency key
const 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 ignored

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

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 now
const 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 resource
const releaseFact = buildReleaseFact(
'resource_pool_123',
'+15551234567',
{ reason: 'user_requested' }
);
await client.emit('resource_pool_123', releaseFact.type, releaseFact.data);
// Check active binding
const binding = await getActiveBinding(client, 'resource_pool_123', '+15551234567');
console.log(binding); // { resource, entity_id: 'user_456', assigned_at, ... }

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 },
// ]
// }

Task: Detect when metrics cross thresholds (with debounce).

import { ThresholdMonitor } from '@z0-app/sdk';
const monitor = new ThresholdMonitor();
// Track balance crossing $500 threshold
const 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 $500
const 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...
}

Task: Anonymize entity data for GDPR compliance.

import { LedgerClient } from '@z0-app/sdk';
const client = new LedgerClient(env.ACCOUNT_LEDGER);
// Request anonymization
await 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 anonymized
const entity = await client.get('user_123');
console.log(entity.anonymized_at); // timestamp
console.log(entity.data.email); // null (cleared)

Task: Evolve fact schemas over time with automatic migration.

import { registerSchema, migrateFact, z } from '@z0-app/sdk';
// Version 1
registerSchema('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 latest
const migrated = migrateFact(oldFact);
console.log(migrated.data); // { amount: 100, currency: 'USD' }

Task: Define REST API routes in YAML manifest.

manifest.yaml
name: analytics-api
version: 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: public

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.

Terminal window
# Generate gateway Worker
npx z0 generate gateway --manifest manifest.yaml --output src/generated
# Generate OpenAPI spec
npx z0 generate openapi --manifest manifest.yaml --output openapi.json
# Validate manifest
npx z0 check --manifest manifest.yaml

Generated code structure:

src/generated/gateway.ts
import { GatewayWorker, parseManifest } from '@z0-app/sdk';
// ... routes registered from manifest
// ... auth middleware for protected routes
// ... rate limit middleware
// ... /health endpoint

Use generated code:

src/index.ts
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 request
const 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 request
const 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-safe
console.log(listRes.hasMore); // Boolean

Task: Handle standard API responses.

// Success response envelope
interface SuccessResponse<T> {
data: T;
meta: {
request_id: string;
timestamp: number;
};
}
// RFC 7807 error response
interface ErrorResponse {
type: string;
title: string;
status: number;
detail: string;
instance: string;
errors?: Array<{ field: string; code: string; message: string }>;
}
// Parse response
const 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}`);
});
}
}

RecipeUse Case
1Create first ledger
2Use LedgerClient
3Schema builders
4YAML manifest
5Hierarchical entities
6WebSocket subscriptions
7SSE subscriptions
8Projections from YAML
9Budget enforcement
10Webhook delivery
11Incoming webhook validation
12Idempotent append
13Circuit breaker
14Temporal resource binding
15Tiered pricing
16Threshold monitoring
17Batch operations
18Health checks
19GDPR anonymization
20Schema versioning
21Create API Gateway with routes
22Extend GatewayWorker
23Generate gateway code
24oRPC type-safe contracts
25Gateway response envelopes