Skip to main content
Introducing packages.sweber.dev
Documentation menuPostgres

Postgres

Store events in Postgres, Neon or Supabase.

import { createAuditLog } from "@sweberdev/logarithm"
import { migratePostgres, postgresSchema, postgresStore } from "@sweberdev/logarithm/postgres"
import { Pool } from "pg"

const pool = new Pool({ connectionString: process.env.DATABASE_URL })
await migratePostgres({ client: pool })
export const audit = createAuditLog({ store: postgresStore({ client: pool }) })

client is anything with a query(text, values) method that returns { rows }: pg.Pool, pg.Client, @neondatabase/serverless, PGlite, or a small wrapper around your driver.

Migrations

migratePostgres runs CREATE TABLE IF NOT EXISTS and creates the indexes. If you manage migrations with Drizzle, Prisma or Flyway, print the SQL once and add it to a migration:

console.log(postgresSchema({ table: "audit_events", schema: "audit" }))

Table

ColumnTypeContent
idtextULID, sorts by creation time
occurred_attimestamptzWhen it happened
tenant_idtextCustomer organisation, or NULL
actiontexte.g. project.updated
actor_idtextCopy of actor.id for fast filters
actor, targets, changes, context, metadatajsonbEvent data
searchtextLower-case text for search

Indexes cover tenant + time, actor + time, action prefix and targets (GIN).

Transactions

Pass the transaction's client so the event commits or rolls back together with the change:

const client = await pool.connect()
try {
  await client.query("BEGIN")
  await client.query("UPDATE projects SET name = $1 WHERE id = $2", [name, id])
  await createAuditLog({ store: postgresStore({ client }) }).record({ ... })
  await client.query("COMMIT")
} finally {
  client.release()
}

Protect the table

Give your application role only INSERT and SELECT on the table, and run retention with a separate role. That way a bug or an injection cannot rewrite history. Logarithm Pro adds a hash chain that makes changes visible even to someone with full database access.