Skip to main content
Version: Current

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​

  1. Actions — commands that represent intent to change state
  2. State — domain entities modeled as immutable data with event-driven transitions
  3. 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 snapshots
  • projection() — read-model updaters that react to events (with optional .batch() for high-throughput replay)
  • slice() — vertical feature modules grouping states, projections, and scoped reactions
  • act() — 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) or PinoLogger (@rotorsoft/act-pino)
  • Store — InMemoryStore (default), PostgresStore (@rotorsoft/act-pg), or SqliteStore (@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 an asOf filter 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​

PackageDescription
@rotorsoft/actThe framework
@rotorsoft/act-pgPostgreSQL store adapter
@rotorsoft/act-sqliteSQLite (libSQL) store adapter
@rotorsoft/act-patchImmutable deep-merge patch utility

Supporting​

PackageDescription
@rotorsoft/act-httpHTTP integrations — webhooks, auto-generated APIs, and the canonical sse subpath (@rotorsoft/act-http/sse) for incremental state broadcast
@rotorsoft/act-pinoPino logger adapter
@rotorsoft/act-otelPrometheus metrics bridge over the lifecycle events
@rotorsoft/act-diagramSVG 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, IAct interface, Store/Cache/Logger adapter 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.

License​

MIT