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
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({ amount: z.number() }) })
.patch({
Incremented: ({ data }, state) => ({ count: state.count + data.amount }),
})
.on({ increment: z.object({ by: z.number() }) })
.emit((action) => ["Incremented", { amount: action.by }])
.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, invariants, 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
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-sse | Server-Sent Events for incremental state broadcast |
@rotorsoft/act-pino | Pino logger adapter |
@rotorsoft/act-diagram | SVG diagram generator |
Requirementsβ
- Node.js >= 22.18.0
- pnpm >= 10.32.1
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.