Getting started

Install stateledger + run your first transition

1. Install the packages

stateledger has one required package (@stateledger/core) and one per database adapter. For a Postgres app using Prisma:

bash
npm install @stateledger/core @stateledger/prisma
# or
yarn add @stateledger/core @stateledger/prisma
# or
pnpm add @stateledger/core @stateledger/prisma

Using Drizzle instead? Replace @stateledger/prisma with @stateledger/drizzle. Writing tests without a real database? Use @stateledger/memory. Need atomic side effects (Stripe, email, Kafka)? Add @stateledger/outbox — see the outbox docs.

2. Create the transitions table

stateledger stores every transition in one append-only table. The Prisma adapter ships with a schema SQL snippet you can run once:

typescript
import { STATELEDGER_SCHEMA_SQL } from "@stateledger/prisma";
import { prisma } from "./prisma";

// Run once, e.g. in a migration script:
await prisma.$executeRawUnsafe(STATELEDGER_SCHEMA_SQL);
Prefer versioned migrations? Copy the SQL into a Prisma migration (prisma migrate dev) or Drizzle migration file so it lives in your migration history.

3. Define your state machine

Machines are declared with defineMachine. Every state and legal transition is listed up front so TypeScript can narrow the union for you.

typescript
import { defineMachine } from "@stateledger/core";

export 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,
});

The as const is important | it lets stateledger infer the exact state and transition literals so transitionTo autocompletes only reachable targets.

4. Bind an adapter and transition

A machine definition is inert. Bind it to a specific subject id (the entity you want to transition) plus an adapter, and you get a live machine instance.

typescript
import { createPrismaAdapter } from "@stateledger/prisma";
import { prisma } from "./prisma";
import { invoiceMachine } from "./machines";

const adapter = createPrismaAdapter(prisma);

// Bind to invoice inv_0142:
const invoice = invoiceMachine.for("inv_0142", {
  adapter,
  actor: "user_abc",   // who's making the change (stored on every row)
});

// Move it from draft → sent:
await invoice.transitionTo("sent", {
  metadata: { sentBy: "email", to: "client@example.com" },
});

// Read where it is now:
const current = await invoice.readCurrent();
// → { state: "sent", actor: "user_abc", at: <Date>, metadata: {...} }

5. What happens under the hood

  • • Advisory lock acquired forsubjectIdso no other process can transition this entity while you're inside the call.
  • • Read current state from the transitions table.
  • • Validate the requested transition is legal from the current state.
  • • Insert a new row in the transitions table with the new state, timestamp, actor, and metadata.
  • • Release the lock.

Next steps