
stateledger persists every transition, audits who / when / from / to, and prevents two processes from racing the same entity. Paired with the built-in transactional outbox for side effects (Stripe, email, Kafka) that ship atomically with the state change or not at all. Built for payments, orders, subscriptions, and any Node.js backend where you need to prove exactly what happened, in what order.
Same pattern GoCardless open-sourced as Statesman for Ruby — now purpose-built for Node.
Not XState — XState runs in memory for UI. stateledger persists to Postgres for backend transitions. Different jobs.
npm install @stateledger/core @stateledger/prisma
# or
yarn add @stateledger/core @stateledger/prisma
# or
pnpm add @stateledger/core @stateledger/prismaDeclare your states and legal transitions once. Every .transition() call writes to your database inside the same transaction as your business change | so the ledger can never drift from reality.
Behind the scenes: advisory locks per entity, transactional after-callbacks, and a complete audit row for every hop. Nothing to configure.
import { defineMachine } from "@stateledger/core";
import { createPrismaAdapter } from "@stateledger/prisma";
import { prisma } from "./prisma";
const invoiceMachine = defineMachine({
name: "invoice",
states: ["draft", "sent", "paid", "cancelled"],
initialState: "draft",
transitions: [
{ from: "draft", to: "sent" },
{ from: "draft", to: "cancelled" },
{ from: "sent", to: "paid" },
{ from: "sent", to: "cancelled" },
] as const,
});
const adapter = createPrismaAdapter(prisma);
// Bind the machine to a specific subject (e.g. invoice id):
const invoice = invoiceMachine.for("inv_0142", {
adapter,
actor: "user_abc",
});
// Concurrency-safe transition. Audits who / when / from / to.
await invoice.transitionTo("paid", {
metadata: { reason: "Stripe webhook confirmed" },
});
// Read current state:
const current = await invoice.readCurrent();
// Time-travel: what was the state at a given moment?
const past = await invoice.stateAt(new Date("2026-06-01"));Every transition writes to your database in the same transaction as your business change. If the app crashes mid-flow, state stays consistent | no lost updates, no phantom retries.
PostgreSQL advisory locks prevent two processes from racing the same entity. No double-transitions, no lost writes, no re-inventing pessimistic locking in every service.
Who transitioned this entity, when, from which state, to which state, and why. Persisted forever, queryable without extra infrastructure. Compliance-ready out of the box.
Ask what state any entity was in at any past timestamp. Useful for compliance replays, dispute investigation, and reproducing exactly what a user saw when they filed a ticket.
Bring your own storage adapter. First-party support for Prisma, Drizzle, and an in-memory adapter for tests. Adapters are ~100 lines of code | rolling your own for TypeORM or Kysely is trivial.
@stateledger/outbox writes side-effect intents (Stripe, email, Kafka) inside the same transaction as the state change. A worker delivers them at-least-once with backoff and a dead-letter queue. Ship your state and your side effects atomically.
The finite state machine kind, not the blockchain kind. Zero cryptography, zero consensus, zero P2P. Just correct, auditable transitions for payments, orders, and workflows.
Wherever a database record moves between well-defined states and someone downstream will ask “when did that change, and who did it?”
Model the lifecycle of a charge, invoice, or refund. stateledger keeps Stripe/webhook races from double-charging by holding an advisory lock while it transitions.
Track carts → paid → picked → packed → shipped → delivered → returned with a full ledger of who moved each order and when.
trialing → active → past_due → cancelled with recovery paths. Every renewal attempt writes an audit row, so failed dunning is debuggable months later.
unverified → pending → approved → suspended. Regulators want to know who approved what and when. stateledger gives you that answer in one query.
submitted → in_review → published → flagged → removed. Concurrency-safe means two moderators never accidentally act on the same item.
Any business process where an entity moves between well-defined states and you need to prove the audit trail later. Contracts, expense reports, medical records, disbursements.
XState is brilliant for UI state and orchestration in a single process. stateledger complements it: use XState in the client and stateledger for the server-side persisted history. The two are not competitors.
Event sourcing replays events to derive current state. stateledger stores current state directly plus an append-only transitions log. Much less machinery, no snapshot cadence to tune, but you lose the ability to reshape past events. Fair tradeoff for most CRUD-shaped businesses.
You end up rebuilding advisory locks, audit rows, transition validation, and time-travel every time. stateledger is what you'd have written on your fifth attempt, distilled into 4 npm packages.
Two examples ship in the monorepo. Both spin up Postgres in Docker, apply the schema, and run scripted scenarios end-to-end.
Six scenarios: happy path, invalid transition, guard rejection, concurrent webhooks racing to authorize, and an in-process outbox worker delivering a receipt email atomically enqueued with the capture.
pnpm db:setup && pnpm simulateA trial → active → past_due → canceled → reactivated lifecycle, an optimistic concurrency demo, a time-travel query for end-of-month billing, and a 10-in-parallel billing run.
pnpm db:setup && pnpm simulatestateledger is open source and free forever. But if you'd rather have the team that wrote it embed it into your Node.js backend | schema design, migration planning, first machines, adapter for a bespoke ORM, or a Postgres tuning pass | we do fixed-scope consulting. Typical engagement: 1 to 3 weeks.
stateledger is a database-backed state machine library for Node.js and TypeScript. It gives you a finite state machine engine that persists every transition to your database, provides advisory-lock concurrency safety, keeps a full audit trail, and lets you query historical state at any past timestamp.
stateledger has first-party adapters for PostgreSQL via Prisma and Drizzle. The core is ORM-agnostic, so any database your ORM supports works. An in-memory adapter is included for tests. Adapters for other ORMs (Kysely, TypeORM, MikroORM) are ~100 lines to build.
XState is a client-side and orchestration state-machine library. stateledger is a server-side, persisted, audit-friendly state machine designed for business entities like invoices, orders, and subscriptions that live in your database. Use both together if you need orchestration in the UI plus a persistent ledger in the backend.
No. stateledger stores current state directly on your entity, plus an append-only transitions log. This gives you audit history without the operational complexity of full event sourcing (snapshots, replays, projections).
Yes. Every transition acquires a PostgreSQL advisory lock scoped to the entity ID, ensuring only one process can move an entity between states at a time. Combined with transactional writes, this eliminates the classic double-transition race that most hand-rolled state machines suffer from.
@stateledger/outbox implements the transactional outbox pattern. Instead of firing the side effect directly (which risks either a phantom action or state drift on rollback), you enqueue an intent into an outbox table inside the same transaction as the state change. A worker process claims rows with FOR UPDATE SKIP LOCKED, dispatches your handler, and marks the row delivered — with exponential backoff on failure and a dead-letter queue after N attempts. Ships with v1.0.
Yes. stateledger is at v1.0.0 as of the outbox helper release. The core state-machine engine and the Prisma / Drizzle adapters are stable and used in production. The outbox helper ships with 26 real-Postgres integration tests covering atomicity, retry, dead-letter, concurrency, and stuck-row reclaim. MIT licensed.
Install @stateledger/core and an adapter. Fifty lines of setup, one migration, and every business transition on your Node.js platform is persisted, audited, and race-safe.