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

@pgshift/state

v1.0.0

Published

PgShift state — state machines, data normalization, audit logs, and consensus gates via PostgreSQL triggers.

Downloads

19

Readme

@pgshift/state

State machines, data normalization, audit logs, and consensus gates for PostgreSQL. Enforced at the database level via triggers.

npm version license downloads


Why?

Every application has business rules. Most of them live in the API layer.

The problem is that the API is not the only path into your database. Scripts bypass it. Migrations bypass it. Admins with psql bypass it. Internal workers with their own database connections bypass it. Every one of those paths is an opportunity for data to end up in an invalid state.

@pgshift/state moves the rules down into the database itself via triggers. No matter who writes to the table, the rules are enforced on every write, from every source, unconditionally.


Install

npm install @pgshift/state

Quick start

import { createClient, normalizers } from '@pgshift/state'

const db = createClient({ url: process.env.DATABASE_URL })

await db.state('loans')
  .define({
    field: 'status',
    states: ['pending', 'approved', 'rejected', 'paid'],
    transitions: {
      pending:  ['approved', 'rejected'],
      approved: ['paid'],
      rejected: [],
      paid:     [],
    },
    initial: 'pending',
  })
  .normalize({ amount: 'ABS({value})' })
  .audit({ track: ['status', 'amount'] })
  .consensus({
    transition: 'approved',
    require: 2,
    roles: ['finance', 'manager'],
    when: 'NEW.amount > 10000000',
  })

await db.destroy()

Features

  • State machines enforced via BEFORE UPDATE triggers
  • Data normalization via BEFORE INSERT OR UPDATE triggers
  • Immutable audit logs via AFTER INSERT OR UPDATE triggers
  • Consensus gates requiring N approvals before a transition
  • Each method is independent — use only what you need, in any order
  • Built-in normalizers for common cases
  • TypeScript types for all inputs and outputs

Architecture

Application (or script, migration, admin, worker)
     |
PostgreSQL
 |-- BEFORE UPDATE trigger    (.define)
 |-- BEFORE INSERT OR UPDATE  (.normalize)
 |-- AFTER INSERT OR UPDATE   (.audit)
 |-- BEFORE UPDATE trigger    (.consensus)

Four independent capabilities

.define() — State machine

Installs a trigger that enforces valid state transitions on every write.

await db.state('loans').define({
  field: 'status',
  states: ['pending', 'approved', 'rejected', 'paid'],
  transitions: {
    pending:  ['approved', 'rejected'],
    approved: ['paid'],
    rejected: [],
    paid:     [],
  },
  initial: 'pending',
})

Invalid transitions are rejected with a clear error:

ERROR: [PgShift] Invalid state transition on table "loans": "paid" -> "pending" is not allowed.

.normalize() — Data normalization

Installs a trigger that normalizes field values on every INSERT or UPDATE.

import { normalizers } from '@pgshift/state'

await db.state('users').normalize({
  email: normalizers.email,   // LOWER(TRIM(value))
  name:  normalizers.name,    // TRIM + collapse spaces
  phone: normalizers.phone,   // remove non-digits
})

Built-in normalizers: normalizers.email, normalizers.name, normalizers.phone, normalizers.trim, normalizers.lowercase, normalizers.uppercase.

Custom SQL expressions using {value} as placeholder:

await db.state('products').normalize({
  slug: "LOWER(REGEXP_REPLACE(TRIM({value}), '[^a-z0-9]+', '-', 'g'))",
})

.audit() — Immutable audit log

Installs a trigger that writes an immutable entry to _pgshift_state_audit for every change.

await db.state('loans').audit({
  track: ['status', 'amount'],  // omit to track all columns
})

const history = await db.state('loans').history('loan-123')
// [{ field: 'status', fromValue: 'pending', toValue: 'approved', changedAt: Date }]

.consensus() — Consensus gate

Installs a trigger that blocks a specific transition until the required number of approvals are recorded.

await db.state('loans').consensus({
  transition: 'approved',
  require: 2,
  roles: ['finance', 'manager'],
  when: 'NEW.amount > 10000000',
})

await db.state('loans').approve('loan-123', { by: 'alice', role: 'finance' })
await db.state('loans').approve('loan-123', { by: 'bob',   role: 'manager' })

// Now the transition is allowed
await pool.query(`UPDATE loans SET status = 'approved' WHERE id = 'loan-123'`)

The when condition is evaluated inside the trigger with access to NEW.*. When false, the consensus check is skipped and the transition proceeds normally.


API

createClient(options)

const db = createClient({
  url: process.env.DATABASE_URL,
  max: 10,
  ssl: { rejectUnauthorized: false },
})

db.state(table).define(config)

| Option | Type | Description | |---|---|---| | field | string | Column that holds the state value | | states | string[] | All valid state values | | transitions | Record<string, string[]> | Allowed transitions per state | | initial | string | Default value set on INSERT if the field is null |


db.state(table).normalize(config)

Map of field name to SQL expression. Use {value} as placeholder for the field value.


db.state(table).audit(config?)

| Option | Type | Default | Description | |---|---|---|---| | track | string[] | all columns | Fields to include in the audit log |


db.state(table).consensus(config)

| Option | Type | Description | |---|---|---| | transition | string | Target state that requires consensus | | require | number | Number of approvals required | | roles | string[] | Optional. Roles allowed to approve. | | when | string | Optional SQL condition evaluated inside the trigger |


db.state(table).approve(entityId, options)

Records an approval for a given entity.

await db.state('loans').approve('loan-123', { by: 'alice', role: 'finance' })

db.state(table).history(entityId)

Returns the audit log for a given entity.

const history = await db.state('loans').history('loan-123')

db.state(table).pendingApprovals(entityId)

Returns all recorded approvals for a given entity.

const approvals = await db.state('loans').pendingApprovals('loan-123')

db.destroy()

Drains the connection pool. Always call on process exit.

process.on('SIGTERM', async () => {
  await db.destroy()
  process.exit(0)
})