Install
yarn add @stateledger/core @stateledger/prismaSet up the transitions table
stateledger persists every transition in one append-only table. Run the shipped SQL once, either against your database directly or inside a Prisma migration.
import { STATELEDGER_SCHEMA_SQL } from "@stateledger/prisma";
import { prisma } from "./prisma";
// One-off setup script
await prisma.$executeRawUnsafe(STATELEDGER_SCHEMA_SQL);Prefer versioned migrations? Copy the SQL from STATELEDGER_SCHEMA_SQL into a new Prisma migration file, then run prisma migrate deploy as usual.
Create the adapter
import { PrismaClient } from "@prisma/client";
import { createPrismaAdapter } from "@stateledger/prisma";
const prisma = new PrismaClient();
const adapter = createPrismaAdapter(prisma);
// Use it with any machine:
const invoice = invoiceMachine.for("inv_0142", {
adapter,
actor: "user_abc",
});The adapter accepts either a top-level PrismaClient or a Prisma.TransactionClient(see “Inside a transaction” below).
Configuration options
createPrismaAdapter(prisma, {
// Custom table name (default: "state_transitions")
tableName: "state_transitions",
// Locking strategy. "pessimistic" uses advisory locks (default);
// "optimistic" uses a unique-constraint retry pattern.
locking: "pessimistic",
});Inside a transaction
The most common use case: transition state atomically with your business change. Wrap everything in a Prisma transaction, then hand that transaction client to stateledger.
await prisma.$transaction(async (tx) => {
// 1. Update your business row
await tx.invoice.update({
where: { id: "inv_0142" },
data: { paidAt: new Date(), paymentIntentId: pi.id },
});
// 2. Transition the state machine in the SAME transaction
const invoice = invoiceMachine.for("inv_0142", {
adapter: createPrismaAdapter(tx),
actor: "stripe:webhook",
});
await invoice.transitionTo("paid", {
metadata: { paymentIntent: pi.id },
});
});If either write fails, both roll back. Your business row and the state ledger can never drift out of sync.
Reading history
const invoice = invoiceMachine.for("inv_0142", { adapter });
// Latest state:
const current = await invoice.readCurrent();
// Full history, oldest first:
const history = await invoice.readHistory();
// → [
// { state: "draft", actor: "user_abc", at: <Date>, ... },
// { state: "sent", actor: "user_abc", at: <Date>, ... },
// { state: "paid", actor: "stripe:webhook", at: <Date>, ... },
// ]Error handling
The adapter surfaces a small set of typed errors you can catch:
- •
InvalidTransition— the requested target isn’t reachable from the current state. - •
OptimisticConcurrencyError— another process transitioned this subject while you were mid-flight (only inoptimisticlocking mode). - •
GuardRejected— a guard function you provided returned false. - •
AdapterError— the underlying database call failed. Wraps the original Prisma error.