Skip to content

Getting Started with z0

This guide walks you through building your first z0 application.

  • Node.js 18+
  • Cloudflare account (for deployment)
  • Basic understanding of TypeScript
Terminal window
npm install @z0-app/sdk
npm install -D wrangler

Create a domain manifest describing your entities:

src/domain/manifest.ts
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']
}
}
};

Extend EntityLedger to create domain-specific entities:

src/domain/entities/Account.ts
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);
}
}

In your main entry point:

src/index.ts
import { Hono } from 'hono';
import { LedgerRegistry, LedgerClient } from '@z0-app/sdk';
import { myDomainManifest } from './domain/manifest';
// Register domain manifest
LedgerRegistry.register(myDomainManifest);
const app = new Hono();
// Entity CRUD
app.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;
wrangler.toml
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 = "..."
Terminal window
wrangler deploy
  • 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.