Getting Started with z0
This guide walks you through building your first z0 application.
Prerequisites
Section titled “Prerequisites”- Node.js 18+
- Cloudflare account (for deployment)
- Basic understanding of TypeScript
Installation
Section titled “Installation”npm install @z0-app/sdknpm install -D wranglerYour First Domain
Section titled “Your First Domain”1. Define Your Entity Types
Section titled “1. Define Your Entity Types”Create a domain manifest describing your entities:
import { DomainManifest } from '@z0-app/sdk';import { Account } from './entities/Account';import { Transaction } from './entities/Transaction';
export const myDomainManifest: DomainManifest = { name: 'my-banking-app', version: '1.0.0', entities: { account: { ledger: Account, description: 'Bank account', fields: { status: { type: 'string', required: true, storage: 'ix_s_1' }, name: { type: 'string', required: true, storage: 'ix_s_2' }, owner_id: { type: 'string', storage: 'ix_s_5' }, balance: { type: 'number', storage: 'ix_n_1' }, }, facts: ['deposit', 'withdrawal', 'transfer'] }, transaction: { ledger: Transaction, description: 'Financial transaction', fields: { status: { type: 'string', required: true, storage: 'ix_s_1' }, type: { type: 'string', required: true, storage: 'ix_s_4' }, }, facts: ['initiated', 'completed', 'failed'] } }};2. Implement Your Entity Classes
Section titled “2. Implement Your Entity Classes”Extend EntityLedger to create domain-specific entities:
import { EntityLedger, Fact } from '@z0-app/sdk';
export class Account extends EntityLedger { // Cached state (disposable, can be rebuilt) getBalance(): number { const cached = this.getCachedState<{ balance: number }>('BalanceState'); if (cached) return cached.balance; return this.recomputeBalance(); }
// Recompute balance from Facts private recomputeBalance(): number { const facts = this.getFacts({ type: ['deposit', 'withdrawal'] }); const balance = facts.reduce((sum, fact) => { if (fact.type === 'deposit') return sum + fact.data.amount; if (fact.type === 'withdrawal') return sum - fact.data.amount; return sum; }, 0);
this.setCachedState('BalanceState', { balance }, facts[facts.length - 1]?.id); return balance; }
// Domain-specific fact recording async recordDeposit(amount: number, currency = 'USD'): Promise<Fact> { if (amount <= 0) { throw new Error('Deposit amount must be positive'); }
return this.appendFact({ type: 'deposit', subtype: 'manual', data: { amount, currency } }); }
async recordWithdrawal(amount: number, currency = 'USD'): Promise<Fact> { const balance = this.getBalance(); if (amount > balance) { throw new Error('Insufficient funds'); }
return this.appendFact({ type: 'withdrawal', subtype: 'manual', data: { amount, currency } }); }
// Custom HTTP endpoints override async fetch(request: Request): Promise<Response> { await this.ensureInitialized(); const url = new URL(request.url);
if (url.pathname === '/balance' && request.method === 'GET') { return Response.json({ balance: this.getBalance() }); }
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 }); }
if (url.pathname === '/withdraw' && request.method === 'POST') { const body = await request.json(); const fact = await this.recordWithdrawal(body.amount, body.currency); return Response.json(fact, { status: 201 }); }
return super.fetch(request); }}3. Register Your Domain
Section titled “3. Register Your Domain”In your main entry point:
import { Hono } from 'hono';import { LedgerRegistry, LedgerClient } from '@z0-app/sdk';import { myDomainManifest } from './domain/manifest';
// Register domain manifestLedgerRegistry.register(myDomainManifest);
const app = new Hono();
// Entity CRUDapp.post('/v1/entities', async (c) => { const body = await c.req.json(); const namespace = LedgerRegistry.getNamespace(c.env, body.type); const client = new LedgerClient(namespace, body.tenant_id);
const entity = await client.stub(body.id).upsertEntity({ type: body.type, data: body.data });
return c.json(entity, 201);});
app.get('/v1/entities/:id', async (c) => { const entityType = c.req.query('entity_type'); const tenantId = c.req.header('X-Tenant-ID'); const namespace = LedgerRegistry.getNamespace(c.env, entityType!); const client = new LedgerClient(namespace, tenantId);
const entity = await client.get(c.req.param('id')); return c.json(entity);});
export default app;4. Configure Cloudflare Bindings
Section titled “4. Configure Cloudflare Bindings”name = "my-banking-app"main = "src/index.ts"compatibility_date = "2024-01-01"
[[durable_objects.bindings]]name = "ACCOUNT_LEDGER"class_name = "Account"script_name = "my-banking-app"
[[d1_databases]]binding = "DB"database_name = "my-banking-app-db"database_id = "..."5. Deploy
Section titled “5. Deploy”wrangler deployNext Steps
Section titled “Next Steps”- Core Concepts - Understand primitives, Facts, and Configs
- Building Your First Domain - Deep dive into domain design
- Patterns - Error handling, webhooks, multi-tenant isolation
- API Reference - REST API for Entity, Fact, Config
Example Applications
Section titled “Example Applications”- Banking - Accounts, deposits, withdrawals, transfers
- E-commerce - Products, orders, payments, refunds
- CRM - Contacts, deals, activities, notes
- SaaS - Users, subscriptions, usage, billing
The three primitives (Entity, Fact, Config) are flexible enough to model any domain.