Skip to content

Quickstart ​

Get a working event-sourced application running in under 5 minutes.

Installation ​

bash
npm install @iwentsourcing/core @iwentsourcing/cli

1 · Define a Realm ​

A Realm is the authentication namespace for your application. Every stream and aggregation belongs to a realm.

ts
// 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.

ts
// 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]);
ts
// 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.

ts
// 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.

ts
// 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.

ts
// 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):

bash
npx iwentsourcing serve src/state/export.ts

This starts an HTTP API on http://localhost:3333, watches your source files for changes, and hot-reloads definitions automatically.

Entry file ​

ts
// 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:

bash
npm install @iwentsourcing/client @iwentsourcing/browser

Use createRemoteManagement to connect to the running API. It handles auth token storage, refresh, and all communication with the server:

ts
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:

ts
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):

ts
await remote.session.login('custom', 'user@example.com', 'password', controller.signal);

Resolve the stream for the current authenticated user and push an event:

ts
// 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:

ts
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 ​

ts
const todos = await stream.snapshot('todos').range(null, { signal: controller.signal });
console.log(todos.map(t => t.data.title));
// → ['Learn iwentsourcing']

Next Steps ​