Skip to main content
v1.x · TypeScript-first event sourcing

Act

Fluent event sourcing for TypeScript

Build robust, auditable, and reactive systems with composable state machines, pure functions, and zero runtime bloat.

Actions → State ← ReactionsPostgres · SQLite · In-MemorytRPC · REST · OpenAPIZod-typed

Three steps to your first event

From install to your first persisted event in under a minute.

1Install
npm install @rotorsoft/act zod
2Define a state and run an action
import { act, state } from "@rotorsoft/act";
import { z } from "zod";

const Counter = state({ Counter: z.object({ count: z.number() }) })
.init(() => ({ count: 0 }))
.emits({ Incremented: z.object({ amount: z.number() }) })
.patch({ Incremented: ({ data }, s) => ({ count: s.count + data.amount }) })
.on({ increment: z.object({ by: z.number() }) })
.emit((action) => ["Incremented", { amount: action.by }])
.build();

const app = act().withState(Counter).build();

await app.do("increment",
{ stream: "counter1", actor: { id: "1", name: "User" } },
{ by: 1 }
);
console.log(await app.load(Counter, "counter1"));
3Run & explore

Run your app and see the output in your terminal. Then dive into reactions, projections, and slices.

Read the full guide

Advanced composition patterns

Five primitives compose every Act application — slices, projections, partial states, invariants, and the orchestrator.

Group states and reactions into feature modules

Each slice owns a set of partial states and their scoped reactions. Handlers receive the full IAct interface for cross-state coordination.

const TicketCreationSlice = slice()
.withState(TicketCreation) // partial state
.withState(TicketOperations) // another partial state

.on("TicketOpened")
.do(async function assign(event, _stream, app) {
const agent = assignAgent(event.stream, event.data.supportCategoryId);
await app.do("AssignTicket",
{ stream: event.stream, actor: { id: randomUUID(), name: "assign" } },
agent, event
);
})
.build();

WolfDesk uses 3 slices: Creation, Operations, and Messaging — each a self-contained feature boundary.

From registry to wire

A built Act is a typed registry of actions. @rotorsoft/act-http walks it once and emits tRPC, Hono REST, and an OpenAPI 3.1 document — same actor and stream seams across every transport, same error envelope, no codegen step.

import { trpc } from "@rotorsoft/act-http/trpc";
import { hono } from "@rotorsoft/act-http/hono";
import { openapi } from "@rotorsoft/act-http/openapi";

// One Act, three transports — same actor / stream seams,
// same ApiError envelope, no codegen.
const tRouter = trpc(app, { actor, stream });
const restApi = hono(app, { actor, stream });
const apiDoc = openapi(app, {
info: { title: "Wolfdesk API", version: "1.0.0" },
});
tRPC

Typed React/Node client via typeof router. Mount with fetchRequestHandler on any fetch-shaped runtime.

Hono REST

POST /api/actions/<name> per action. Runs unchanged on Node, Bun, Cloudflare Workers, Vercel Edge, AWS Lambda.

OpenAPI 3.1

Pure-data emitter. Describes the live REST surface; pair with Scalar or Redoc for an interactive explorer.

Read the API guide

Built for serious systems

Type-safe primitives and production-ready adapters, with the smallest API surface that gets the job done.

Functional Event Sourcing

Every state change is a pure function of previous state and events. Immutability and replayability by design.

Composable State Machines

Model your domain as composable, strongly-typed state machines. No classes, just functions and data.

TypeScript Native

Type safety and inference everywhere. Catch errors at compile time, not at runtime.

Reactive by Default

Reactions let you build event-driven flows and side effects with ease — with built-in correlation, drain, and dual-frontier processing.

Production Adapters Included

Postgres for scale, SQLite for embedded, in-memory for tests. Switch between them with a single line of code.

Minimal Footprint

Minimal and dependency-light. No codegen, no runtime bloat, and a tiny bundle size.

Ready to act?

Spend a minute on the quickstart, an afternoon on the Calculator example, or a weekend on WolfDesk — every step has a working sandbox.