Act Framework
The event-sourcing framework for TypeScript.
Most business apps can be modeled with just three primitives: Actions → {State} ← Reactions. Act wires them together with Zod schemas, an immutable event log, and a built-in pipeline that turns reactions into observable workflows. Drop in Postgres for production, SQLite for embedded, or run in-memory for tests; no external message broker required.
What you get
- Simplicity — focus on state, actions, and reactions without boilerplate or code generation
- Type safety — TypeScript and Zod for compile-time guarantees and runtime validation
- Composability — build complex workflows by composing small, testable state machines
- Auditability — every state change is an event, enabling time-travel, debugging, and compliance
- Adaptability — swap storage backends, integrate external systems, scale from in-memory to production
Rules across streams
New in 2.0. A rule that spans streams, such as a unique email or a limited number of seats, needs no saga. An action loads the other stream with .loads, decides, and commits to every stream at once: all of it lands or none of it does. See Rules across streams, and Migrating to 2.x to upgrade.
See it move
Every Act app can be drawn and played from its own builders: who invokes each action, how the state checks it, which events commit, and which reactions and timers follow. Watch the WolfDesk ticket flows, browse all animations, or make one for your app.
Quick Start
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({ by: z.number() }) })
.patch({ Incremented: ({ data }, s) => ({ count: s.count + data.by }) })
.on({ increment: z.object({ by: z.number() }) })
.emit("Incremented")
.build();
const app = act().withState(Counter).build();
const actor = { id: "user1", name: "User" };
await app.do("increment", { stream: "counter1", actor }, { by: 5 });
const snapshot = await app.load(Counter, "counter1");
console.log(snapshot.state.count); // 5
Core Concepts
Actions → State ← Reactions
- Actions — commands that represent intent to change state
- State — domain entities modeled as immutable data with event-driven transitions
- Reactions — asynchronous responses to state changes that trigger workflows and integrations
Builders
state()— define state machines with actions, events, business rules (preconditions), invariants in the state schema, and snapshotsprojection()— read-model updaters that react to events (with optional.batch()for high-throughput replay)slice()— vertical feature modules grouping states, projections, and scoped reactionsact()— orchestrator that composes states, slices, and projections into an application
Port/Adapter Pattern
Infrastructure uses swappable adapters injected via log(), store(), and cache() port functions:
- Logger —
ConsoleLogger(default) orPinoLogger(@rotorsoft/act-pino) - Store —
InMemoryStore(default),PostgresStore(@rotorsoft/act-pg), orSqliteStore(@rotorsoft/act-sqlite) - Cache —
InMemoryCache(default, LRU) or custom adapters (e.g., Redis) - Disposal —
dispose()()cleans up all registered adapters on shutdown
Event Processing
- Correlation — dynamic stream discovery via reaction resolvers
- Drain — leasing-based reaction processing with dual-frontier strategy
- Settle — debounced, non-blocking correlate→drain loop for production
- Time-travel —
load()accepts anasOffilter to reconstruct historical state - Close the books —
app.close()archives, tombstones, or restarts streams
Deliberate choices & non-goals
Coming from Axon, Marten, MassTransit, or Temporal? Some patterns those frameworks ship as named primitives — a compensation stack, an upcaster chain, an exactly-once projection checkpoint — are deliberately absent here, replaced by simpler shapes on machinery Act already has. Deliberate choices & non-goals explains each call, how to reach the same outcome, and the one pattern that genuinely is still missing.
Packages
Core
| Package | Description |
|---|---|
@rotorsoft/act | The framework |
@rotorsoft/act-pg | PostgreSQL store adapter |
@rotorsoft/act-sqlite | SQLite (libSQL) store adapter |
@rotorsoft/act-patch | Immutable deep-merge patch utility |
Supporting
| Package | Description |
|---|---|
@rotorsoft/act-http | HTTP integrations — webhooks, auto-generated APIs, and the canonical sse subpath (@rotorsoft/act-http/sse) for incremental state broadcast |
@rotorsoft/act-pino | Pino logger adapter |
@rotorsoft/act-otel | Prometheus metrics bridge over the lifecycle events |
@rotorsoft/act-diagram | SVG diagram generator |
Requirements
- Node.js >= 22.23.4
- pnpm >= 12.18.0
Stability and support
Act follows SemVer. The Stability Charter is the published contract for what each version protects and how the project is supported:
- What semver covers — the builder API,
IActinterface,Store/Cache/Loggeradapter contracts, lifecycle events, and public type exports. Internals, performance characteristics, and log formats are explicitly out of scope. - Support window — the latest major is actively maintained; the previous major gets security and critical-fix maintenance for at least 6 months after the next major ships. These windows have been in effect since the 1.0 release.
- Deprecation policy — public surface is deprecated in a minor release and removed no earlier than the next major. Events on disk are never broken; they evolve through versioned event names (
Foo→Foo_v2), with a startup advisory enumerating deprecated versions. - Security fixes — report privately via a GitHub security advisory; fixes land on the active major and any previous major still inside its maintenance window.
FAQ
Q: Do I need to use Postgres? No. Start with the in-memory store and switch to Postgres or another backend when needed.
Q: Is Act only for DDD experts? No. Act is designed to be approachable for all TypeScript developers, with a focus on simplicity and strong typing.