Quickstart
Get a working event-sourced application running in under 5 minutes.
Installation
npm install @iwentsourcing/core @iwentsourcing/cli1 · Define a Realm
A Realm is the authentication namespace for your application. Every stream and aggregation belongs to a realm.
// state/realm.ts
import { defineRealm } from '@iwentsourcing/core/realm/define-realm.js';
import { jwtStore } from '@iwentsourcing/core/authentication/jwt-store.js';
export const todoRealm = defineRealm('todo', {
providers: {
custom: jwtStore({
issuer: 'my-app',
secret: 'change-me-in-production',
audience: 'my-app',
}),
},
});2 · Declare State and Events
State is a typed key-value snapshot derived from events. Events are the typed actions that mutate it.
// state/todo/state.ts
import { declareState, defineState } from '@iwentsourcing/core/storage/declare-state.js';
import { string, boolean, date } from '@iwentsourcing/core/typing/index.js';
export const Todo = declareState(
'todos',
{
id: string(),
title: string(),
completed: boolean(),
createdAt: date(),
},
['id'],
).index('date', ['createdAt']);
export const todoState = defineState([Todo]);// state/todo/events.ts
import { declareEvent, defineEvents } from '@iwentsourcing/core/stream/index.js';
import { string } from '@iwentsourcing/core/typing/index.js';
export const CreateTodoEvent = declareEvent('create-todo', {
title: string(),
});
export const CompleteTodoEvent = declareEvent('complete-todo', {
id: string(),
});
export const todoEvents = defineEvents([CreateTodoEvent, CompleteTodoEvent]);3 · Define a Stream
A Stream is an append-only event log with an associated in-stream state snapshot. The handle map defines how each event mutates state.
// state/todo/stream.ts
import { defineStream } from '@iwentsourcing/core/stream/define-stream.js';
import { todoRealm } from '../realm.js';
import { todoState } from './state.js';
import { todoEvents } from './events.js';
export const todoStream = defineStream(todoRealm, {
pattern: 'personal-$(auth.sub)', // one stream per authenticated user
events: todoEvents,
state: todoState,
outputs: {},
auth: { allowAnonymous: false },
handle: {
'create-todo': async (evt, ctx) => {
await ctx.state('todos').insert({
id: ctx.metadata.id,
title: evt.title,
completed: false,
createdAt: new Date(),
});
},
'complete-todo': async (evt, ctx) => {
await ctx.state('todos').patch({ id: evt.id, completed: true });
},
},
});The pattern is the key formula for stream instances. $(auth.sub) is an auth context variable that resolves to the user's subject claim — so every authenticated user gets their own isolated instance. See the Stream → Pattern docs for all available variables.
4 · (Optional) Define an Aggregation
An Aggregation is a read model that subscribes to events from one or more streams. Use it when you need cross-stream queries or a different projection of the same data.
// state/aggregation/all-todos.ts
import { defineAggregation } from '@iwentsourcing/core/aggregation/define-aggregation.js';
import { injectEvent } from '@iwentsourcing/core/aggregation/inject-event.js';
import { todoRealm } from '../realm.js';
import { todoStream } from '../todo/stream.js';
import { aggState } from './state.js';
export const allTodosAggregation = defineAggregation(todoRealm, {
pattern: 'personal-$(auth.sub)',
state: aggState,
auth: { allowAnonymous: false },
events: {
'create-todo': injectEvent(todoStream, 'create-todo'),
'complete-todo': injectEvent(todoStream, 'complete-todo'),
},
handle: {
'create-todo': async (evt, ctx) => {
await ctx.state('todos').insert({
id: ctx.metadata.id,
stream: ctx.stream.name,
title: evt.title,
completed: false,
createdAt: new Date(),
});
},
'complete-todo': async (evt, ctx) => {
await ctx.state('todos').patch({
id: evt.id,
stream: ctx.stream.name,
completed: true,
});
},
},
});5 · Add Seed Data
Seed data lets you pre-populate a development environment with deterministic events.
// state/seed.ts
import { defineSeed } from '@iwentsourcing/core/seed/define-seed.js';
import { todoStream } from './todo/stream.js';
export const todoSeed = defineSeed(todoStream, { seed: 42 })
.add('create-todo', { title: 'Buy groceries' })
.add('create-todo', { title: 'Walk the dog' })
.add('complete-todo', (ctx) => ({ id: ctx.eventId(-2) }));6 · Start the Dev Server
Point the CLI at your entry file (which exports your stream, aggregation, and seed):
npx iwentsourcing serve src/state/export.tsThis starts an HTTP API on http://localhost:3333, watches your source files for changes, and hot-reloads definitions automatically.
Entry file
// src/state/export.ts
export { todoStream } from './todo/stream.js';
export { allTodosAggregation } from './aggregation/all-todos.js';
export { todoSeed } from './seed.js';7 · Use the Stream from a Browser
Install the client and browser packages:
npm install @iwentsourcing/client @iwentsourcing/browserUse createRemoteManagement to connect to the running API. It handles auth token storage, refresh, and all communication with the server:
import { createRemoteManagement } from '@iwentsourcing/client/management/create-remote-management.js';
import { createRemoteStream } from '@iwentsourcing/client/management/remote-stream.js';
import { createOAuthStorage } from '@iwentsourcing/client/oauth/oauth-storage.js';
import { createNavigatorLock } from '@iwentsourcing/browser/create-navigator-lock.js';
import { createLocalSession } from '@iwentsourcing/browser/create-local-session.js';
import { createBrowserEnv } from '@iwentsourcing/browser/create-browser-env.js';
import { createConsoleTelemetry } from '@iwentsourcing/core/tracer/create-console-telemetry.js';
import { todoRealm } from './state/realm.js';
import { todoStream } from './state/todo/stream.js';
const controller = new AbortController();
const remote = createRemoteManagement({
url: 'http://localhost:3333',
tenant: 'default',
session: createOAuthStorage(todoRealm, {
sessionStorageFactory: createLocalSession,
transferStorageFactory: createLocalSession,
lock: createNavigatorLock(),
}),
env: createBrowserEnv(),
signal: controller.signal,
telemetry: createConsoleTelemetry(),
});createOAuthStorage persists the session token in localStorage and handles automatic token refresh. Listen to auth state changes to redirect to a login page when needed:
remote.session.on((evt) => {
if (evt.type === 'unauthenticated') {
window.location.href = '/login';
}
}, controller.signal);Log in with your realm's provider (the 'custom' key defined in step 1):
await remote.session.login('custom', 'user@example.com', 'password', controller.signal);Resolve the stream for the current authenticated user and push an event:
// createRemoteStream extracts the pattern, version, and schema from the definition
const stream = remote.getStream(createRemoteStream(todoStream));
await stream.push(
'create-todo',
{ title: 'Learn iwentsourcing' },
{ signal: controller.signal },
);Observing State (live updates)
observe returns an async generator that emits the current result set whenever the state changes — useful for keeping the UI in sync without polling:
for await (const todos of stream.observe('todos').index('date').observeRange(null, {
take: 10,
dir: 'prev',
signal: controller.signal,
})) {
console.log(todos.map(t => t.data.title));
// re-emits on every insert, patch, or delete
}One-time Snapshot Read
const todos = await stream.snapshot('todos').range(null, { signal: controller.signal });
console.log(todos.map(t => t.data.title));
// → ['Learn iwentsourcing']Next Steps
- Core Concepts — understand the full data-flow model
- Stream — deep-dive into stream options and the handle context
- Aggregation — cross-stream read models
- State — indexes, queries, and state operations
- CLI — serve — all CLI options