npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@stateledger/prisma

v0.1.0

Published

Prisma + Postgres adapter for stateledger — persisted, audited, concurrency-safe state machines.

Readme

@stateledger/prisma

Prisma + Postgres adapter for stateledger.

npm

Wires @stateledger/core to Postgres via Prisma. Pessimistic concurrency by default (advisory locks), transactional after-callbacks, immutable audit trail.

Install

pnpm add @stateledger/core @stateledger/prisma
# Peer:
pnpm add @prisma/client

Postgres only. The adapter relies on pg_advisory_xact_lock and a partial unique index — both Postgres-specific.

Add the schema

The adapter writes to a single table, stateledger_transitions. Add it via a Prisma migration:

pnpm prisma migrate dev --create-only --name add_stateledger

Then paste the contents of STATELEDGER_SCHEMA_SQL into the generated migration.sql:

CREATE TABLE IF NOT EXISTS "stateledger_transitions" (
  id               UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
  machine          TEXT         NOT NULL,
  subject_id       TEXT         NOT NULL,
  from_state       TEXT,
  to_state         TEXT         NOT NULL,
  sort_key         INTEGER      NOT NULL,
  most_recent      BOOLEAN      NOT NULL DEFAULT TRUE,
  actor_id         TEXT,
  actor_type       TEXT,
  metadata         JSONB        NOT NULL DEFAULT '{}'::jsonb,
  machine_version  INTEGER      NOT NULL DEFAULT 1,
  created_at       TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

CREATE UNIQUE INDEX IF NOT EXISTS "stateledger_transitions_subject_sort_key"
  ON "stateledger_transitions" (machine, subject_id, sort_key);

-- Partial unique: exactly one mostRecent row per subject. Load-bearing.
CREATE UNIQUE INDEX IF NOT EXISTS "stateledger_transitions_one_most_recent"
  ON "stateledger_transitions" (machine, subject_id)
  WHERE most_recent = TRUE;

CREATE INDEX IF NOT EXISTS "stateledger_transitions_history"
  ON "stateledger_transitions" (machine, subject_id, sort_key DESC);

Then pnpm prisma migrate dev to apply.

Why raw SQL and not a Prisma model? Prisma's schema language can't express the partial unique index on most_recent, and that index is the correctness invariant the library leans on. Shipping the schema as raw SQL keeps the storage layer identical to the future Drizzle and TypeORM adapters.

If you want a Prisma model on top for type-safe reads from your own code, you can add one alongside — see the test schema for an example. It's optional.

Gotcha if you add the model. Use @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid on the id field — NOT @default(uuid()). The adapter writes rows via raw INSERT, bypassing Prisma's client-side UUID generator. The default has to live in the Postgres column, not on Prisma's side.

Use it

import { defineMachine } from "@stateledger/core";
import { createPrismaAdapter } from "@stateledger/prisma";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();
const adapter = createPrismaAdapter(prisma);

const PaymentMachine = defineMachine({
  name: "payment",
  states: ["pending", "authorized", "captured", "failed"],
  initialState: "pending",
  transitions: [
    { from: "pending",    to: "authorized" },
    { from: "pending",    to: "failed" },
    { from: "authorized", to: "captured" },
  ],
} as const);

const machine = PaymentMachine.for(paymentId, {
  adapter,
  actor: { id: userId, type: "USER" },
});

await machine.transitionTo("pending");      // bootstrap
await machine.transitionTo("authorized");   // locked, validated, audited
await machine.history();                    // full timeline

Joining an existing transaction

If you're already inside a prisma.$transaction(...), hand the TransactionClient to the adapter and the transition will join that transaction instead of opening a new one:

await prisma.$transaction(async (tx) => {
  await tx.invoice.create({ data: { ... } });

  const adapter = createPrismaAdapter(tx);
  const machine = PaymentMachine.for(paymentId, { adapter, actor });
  await machine.transitionTo("authorized");

  // Both the invoice insert and the transition roll back together if
  // anything throws inside this block.
});

Options

createPrismaAdapter(prisma, {
  // Postgres table name. Override if you've namespaced it. Must be a
  // valid SQL identifier (letters, digits, underscores).
  tableName: "stateledger_transitions",

  // "pessimistic" (default) takes a per-(machine, subjectId) advisory
  // lock — concurrent writers wait their turn.
  //
  // "optimistic" skips the lock and relies on the partial unique index
  // to detect lost races. The library raises `OptimisticConcurrencyError`
  // on the loser; the caller is expected to retry.
  locking: "pessimistic",
});

Locking model

By default, every transition takes a Postgres advisory lock keyed on (machine, subjectId) (pg_advisory_xact_lock(hashtextextended(...))). The lock is bound to the transaction's lifetime — it releases automatically on commit or rollback. There's no separate releaseLock call and no shared state to clean up if your process dies mid-transaction.

If you need to scale writes higher than one-per-subject at a time and can tolerate retries, switch to locking: "optimistic". The partial unique index on most_recent will reject the loser of any race with a constraint violation, which the adapter surfaces as the library's OptimisticConcurrencyError. Catch and retry from your application code.

Development

# Install deps + generate test client
pnpm install
pnpm --filter @stateledger/prisma prisma:generate

# Unit tests (no Docker required)
pnpm --filter @stateledger/prisma test

# Integration tests (boots a real Postgres in Docker via testcontainers)
pnpm --filter @stateledger/prisma test:integration

License

MIT