Adapters

@stateledger/prisma — Prisma adapter for Postgres

Install

bash
yarn add @stateledger/core @stateledger/prisma
The Prisma adapter is Postgres-only. It relies on Postgres advisory locks and partial unique indexes that don’t exist in MySQL or SQLite.

Set 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.

typescript
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

typescript
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

typescript
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.

typescript
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

typescript
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 in optimistic locking mode).
  • • GuardRejected — a guard function you provided returned false.
  • • AdapterError — the underlying database call failed. Wraps the original Prisma error.