Skip to content

Getting Started with TypeScript

This guide walks you through building a client application using the TypeScript SDK.

  • Node.js 20.0 or later
  • pnpm 9.0 or later
  • An EvidentSource server instance with deployed state changes and state views

If you’re building state changes and state views (not just a client), use the project template:

Terminal window
# Clone the SDKs repository
git clone https://github.com/evidentsystems/evidentsource-sdks
cd evidentsource-sdks/typescript/packages/evidentsource-functions/templates
# Create a new project
./create-project.sh my-app my-database open-account active-accounts

This creates a complete project with:

  • domain/ - Shared types for events and commands
  • state-changes/ - Example state change component
  • state-views/ - Example state view component
  • build_all.sh - Build all WASM components
  • install.sh - Deploy components to EvidentSource
  • CLAUDE.md - AI assistant context for development
Terminal window
cd my-app
pnpm install
pnpm build
./build_all.sh # Build WASM components
./install.sh --all # Deploy to server

See the Template Guide for full documentation.

For building client applications that connect to existing state changes/views:

Terminal window
mkdir my-banking-client
cd my-banking-client
pnpm init
Terminal window
# Install from git (until published to npm)
pnpm add github:evidentsystems/evidentsource-sdks#main:typescript/packages/evidentsource-core
pnpm add github:evidentsystems/evidentsource-sdks#main:typescript/packages/evidentsource-client

Or clone and link locally:

Terminal window
git clone https://github.com/evidentsystems/evidentsource-sdks.git
cd evidentsource-sdks/typescript
pnpm install
pnpm build
# In your project
pnpm link ../evidentsource-sdks/typescript/packages/evidentsource-core
pnpm link ../evidentsource-sdks/typescript/packages/evidentsource-client

Create tsconfig.json:

{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src/**/*"]
}

Create src/index.ts:

import {
EvidentSource,
databaseName,
stateChangeName,
stateViewName,
jsonCommandRequest,
stateViewContentAsJson,
} from "@evidentsource/client";
interface AccountSummary {
id: string;
name: string;
balance: number;
isOpen: boolean;
}
async function main() {
// Connect to the server
const es = EvidentSource.connect("http://localhost:50051");

For production deployments, you’ll need to authenticate with a JWT token. See Authentication for details on the trust model.

import { EvidentSource, Credentials } from "@evidentsource/client";
async function main() {
// Bearer token authentication (production)
const token = process.env.EVS_TOKEN!;
const es = EvidentSource.connect(
"api.example.com:50051",
Credentials.bearerToken(token)
);
// DevMode for local development (no auth required)
const es = EvidentSource.connect("http://localhost:50051");
try {
// Connect to a database
const dbName = databaseName("banking");
const conn = await es.connectDatabase(dbName);
console.log(`Connected at revision ${conn.revision()}`);
// Execute a state change to open an account
const db = await conn.executeStateChange(
stateChangeName("open-account"),
1,
jsonCommandRequest({
accountId: "acct-001",
customerName: "Alice Smith",
initialDeposit: 1000.0,
})
);
console.log(`New revision: ${db.revision()}`);
// Query the account summary
const view = await db.viewState(
stateViewName("account-summary"),
1,
{ account_id: "acct-001" }
);
if (view) {
const account = stateViewContentAsJson<AccountSummary>(view);
console.log(`Account: ${account.name}`);
console.log(`Balance: $${account.balance.toFixed(2)}`);
}
// Clean up
await conn.close();
} finally {
es.close();
}
}
main().catch(console.error);
import { EventSelector, streamName, eventType } from "@evidentsource/core";
// Query events by stream
const events = await db.queryEvents({
selector: EventSelector.streamEquals(streamName("accounts")),
limit: 100,
});
for (const event of events) {
console.log(`${event.type}: ${JSON.stringify(event.data)}`);
}
// AND combination
const selector = EventSelector.and(
EventSelector.subjectEquals(subject("acct-001")),
EventSelector.typeStartsWith(eventType("com.banking."))
);
// OR combination
const selector = EventSelector.or(
EventSelector.typeEquals(eventType("com.banking.account.credited")),
EventSelector.typeEquals(eventType("com.banking.account.debited"))
);

State changes are server-side WASM components that handle commands:

// Execute with JSON command
const db = await conn.executeStateChange(
stateChangeName("deposit"),
1,
jsonCommandRequest({
accountId: "acct-001",
amount: 500.0,
description: "Paycheck deposit",
})
);
// Handle errors
try {
await conn.executeStateChange(
stateChangeName("withdraw"),
1,
jsonCommandRequest({
accountId: "acct-001",
amount: 10000.0, // More than balance
})
);
} catch (e) {
if (e instanceof StateChangeError) {
console.error(`Validation error: ${e.message}`);
}
}

State views are materialized read models:

// Query with parameters
const view = await db.viewState(
stateViewName("account-summary"),
1,
{ account_id: "acct-001" }
);
// Parse as typed JSON
interface AccountSummary {
id: string;
name: string;
balance: number;
}
const account = stateViewContentAsJson<AccountSummary>(view);

Query historical state at a specific revision:

// Get database at a specific revision
const historicalDb = await conn.atRevision(500);
// Query state as it was at that revision
const pastView = await historicalDb.viewState(
stateViewName("account-summary"),
1,
{ account_id: "acct-001" }
);

Create an HTTP API using the SDK:

import { Hono } from "hono";
import { EvidentSource, databaseName, stateChangeName, stateViewName, jsonCommandRequest, stateViewContentAsJson } from "@evidentsource/client";
const app = new Hono();
const es = EvidentSource.connect("http://localhost:50051");
const dbName = databaseName("banking");
// Get account
app.get("/accounts/:id", async (c) => {
const conn = await es.connectDatabase(dbName);
try {
const db = await conn.latestDatabase();
const view = await db.viewState(
stateViewName("account-summary"),
1,
{ account_id: c.req.param("id") }
);
if (!view) {
return c.json({ error: "Account not found" }, 404);
}
return c.json(stateViewContentAsJson(view));
} finally {
await conn.close();
}
});
// Open account
app.post("/accounts", async (c) => {
const body = await c.req.json();
const conn = await es.connectDatabase(dbName);
try {
const db = await conn.executeStateChange(
stateChangeName("open-account"),
1,
jsonCommandRequest(body)
);
return c.json({ revision: db.revision() }, 201);
} catch (e) {
return c.json({ error: e.message }, 400);
} finally {
await conn.close();
}
});
// Deposit
app.post("/accounts/:id/deposit", async (c) => {
const body = await c.req.json();
const conn = await es.connectDatabase(dbName);
try {
const db = await conn.executeStateChange(
stateChangeName("deposit"),
1,
jsonCommandRequest({
accountId: c.req.param("id"),
...body,
})
);
return c.json({ revision: db.revision() });
} catch (e) {
return c.json({ error: e.message }, 400);
} finally {
await conn.close();
}
});
export default app;
import { DatabaseError, StateChangeError } from "@evidentsource/client";
try {
await conn.executeStateChange(/* ... */);
} catch (e) {
if (e instanceof StateChangeError) {
switch (e.code) {
case "VALIDATION":
console.error("Invalid input:", e.message);
break;
case "CONFLICT":
console.error("Conflict:", e.message);
break;
case "NOT_FOUND":
console.error("Resource not found:", e.message);
break;
default:
console.error("State change error:", e.message);
}
} else if (e instanceof DatabaseError) {
console.error("Database error:", e.message);
} else {
throw e;
}
}