Configuration
betterAudit() options
Section titled “betterAudit() options”All configuration is passed to the betterAudit() factory. Only database and auditTables are required.
import { betterAudit } from "@usebetterdev/audit";import { drizzleAuditAdapter } from "@usebetterdev/audit/drizzle";
const audit = betterAudit({ database: drizzleAuditAdapter(db), auditTables: ["users", "orders", "payments"], asyncWrite: false, maxQueryLimit: 1000, retention: { days: 365 }, onError: (error) => logger.error("Audit write failed", error),});| Option | Type | Default | Description |
|---|---|---|---|
database | AuditDatabaseAdapter | — | Required. ORM adapter that handles writing and querying audit logs. |
auditTables | string[] | — | Required. SQL table names to audit. Events for unlisted tables are silently skipped. |
asyncWrite | boolean | false | When true, writes are fire-and-forget. Per-call asyncWrite overrides this. |
maxQueryLimit | number | 1000 | Hard upper-bound for query().limit(n). Throws if n exceeds this value. |
retention | RetentionPolicy | — | Retention policy for automatic purge. See Retention policy. |
onError | (error: unknown) => void | console.error | Called when an async write or afterLog hook fails. |
beforeLog | BeforeLogHook[] | [] | Hooks that run before each log is written. See Lifecycle hooks. |
afterLog | AfterLogHook[] | [] | Hooks that run after each log is written. See Lifecycle hooks. |
console | ConsoleRegistration | — | Console integration. See Console integration. |
Table filtering
Section titled “Table filtering”The auditTables array is an allowlist of SQL table names that should be audited. Any mutation on a table not in this list is silently ignored — no error, no log entry.
const audit = betterAudit({ database: drizzleAuditAdapter(db), auditTables: ["users", "orders"],});
// INSERT into "users" → captured// INSERT into "orders" → captured// INSERT into "sessions" → silently skippedEnrichment
Section titled “Enrichment”Enrichment adds human-readable labels, severity levels, compliance tags, and dynamic descriptions to audit entries. Register enrichment rules with audit.enrich(). See the Enrichment guide for a full walkthrough with examples.
import { betterAudit } from "@usebetterdev/audit";import { drizzleAuditAdapter } from "@usebetterdev/audit/drizzle";
const audit = betterAudit({ database: drizzleAuditAdapter(db), auditTables: ["users", "payments"],});
audit.enrich("users", "DELETE", { label: "User account deleted", severity: "critical", compliance: ["gdpr", "soc2"], redact: ["password", "ssn"],});
audit.enrich("payments", "*", { label: "Payment mutation", severity: "high", compliance: ["pci"],});Enrichment options
Section titled “Enrichment options”| Option | Type | Description |
|---|---|---|
label | string | Human-readable label for the audit entry. |
description | (context) => string | Dynamic description. Receives { before, after, diff, actorId, metadata }. |
severity | "low" | "medium" | "high" | "critical" | Severity level for the operation. |
compliance | string[] | Compliance tags (e.g., "gdpr", "soc2", "hipaa", "pci"). |
notify | boolean | Flag for downstream notification integrations. |
redact | string[] | Field names to remove from beforeData/afterData. Mutually exclusive with include. |
include | string[] | Field names to keep — all others are removed. Mutually exclusive with redact. |
Dynamic descriptions
Section titled “Dynamic descriptions”The description function receives context about the mutation:
audit.enrich("users", "UPDATE", { label: "User profile updated", severity: "medium", description: ({ before, after, diff, actorId }) => `Actor ${actorId} changed fields: ${diff?.changedFields.join(", ")}`,});Specificity tiers
Section titled “Specificity tiers”When multiple enrichment rules match an event, they are resolved from least to most specific. Scalar values (like label) are overwritten by more specific rules. Array values (like compliance) are concatenated and deduplicated.
| Tier | Pattern | Matches |
|---|---|---|
| 1 (least specific) | "*", "*" | All tables, all operations |
| 2 | "*", "DELETE" | All tables, specific operation |
| 3 | "users", "*" | Specific table, all operations |
| 4 (most specific) | "users", "DELETE" | Specific table, specific operation |
// Tier 1: global defaultaudit.enrich("*", "*", { severity: "low", compliance: ["soc2"],});
// Tier 4: specific overrideaudit.enrich("users", "DELETE", { severity: "critical", // overwrites "low" compliance: ["gdpr"], // merged → ["soc2", "gdpr"]});Field redaction
Section titled “Field redaction”Control which fields appear in beforeData and afterData snapshots. Two modes are available — use one or the other, not both.
Blocklist (redact)
Section titled “Blocklist (redact)”Remove specific fields. Everything else is kept:
audit.enrich("users", "*", { redact: ["password", "ssn", "secret_key"],});// beforeData: { id: "1", name: "Alice", password: "[REDACTED]" }// → stored as: { id: "1", name: "Alice" } (password removed)Allowlist (include)
Section titled “Allowlist (include)”Keep only the listed fields. Everything else is removed:
audit.enrich("users", "*", { include: ["id", "name", "email"],});// beforeData: { id: "1", name: "Alice", password: "hash", ssn: "123" }// → stored as: { id: "1", name: "Alice", email: "[email protected]" }When fields are redacted, the audit entry’s redactedFields column records which fields were removed — useful for compliance audits that need to prove sensitive data was excluded.
Retention policy
Section titled “Retention policy”Configure automatic purge of old audit entries by setting a retention window:
const audit = betterAudit({ database: drizzleAuditAdapter(db), auditTables: ["users", "orders"], retention: { days: 365 },});| Option | Type | Default | Description |
|---|---|---|---|
days | number | — | Required. Purge entries older than this many days. Must be a positive integer. |
tables | string[] | all tables | When set, only purge entries for these specific tables. |
See Retention Policies for table-scoped retention, automated purge scheduling, archiving strategies, and legal hold patterns.
Lifecycle hooks
Section titled “Lifecycle hooks”Hooks let you observe or transform audit entries at write time. There are two hook points: before the log is written and after.
Config-time hooks
Section titled “Config-time hooks”Pass hook arrays when creating the audit instance. These run for every audit entry:
import { betterAudit } from "@usebetterdev/audit";import { drizzleAuditAdapter } from "@usebetterdev/audit/drizzle";
const audit = betterAudit({ database: drizzleAuditAdapter(db), auditTables: ["users"], beforeLog: [ (log) => { // Mutate the log before it's written log.metadata = { ...log.metadata, environment: "production" }; }, ], afterLog: [ (log) => { // Observe the written log (read-only snapshot) metrics.increment("audit.entries", { table: log.tableName }); }, ],});Runtime hooks
Section titled “Runtime hooks”Register hooks dynamically after creation. Returns a dispose function to unregister:
// Registerconst dispose = audit.onBeforeLog((log) => { log.metadata = { ...log.metadata, requestId: getCurrentRequestId() };});
// Later: unregisterdispose();Hook behavior
Section titled “Hook behavior”| Hook | Can mutate? | Error behavior |
|---|---|---|
beforeLog | Yes | Errors abort the write — the entry is not stored. |
afterLog | No (read-only snapshot) | The entry is already written. In async mode, errors are passed to onError. In sync mode, errors propagate to the caller. |
Hooks run sequentially in registration order. beforeLog hooks see post-enrichment, post-redaction data.
Async writes
Section titled “Async writes”By default, captureLog() awaits the database write. Enable async mode for fire-and-forget behavior:
const audit = betterAudit({ database: drizzleAuditAdapter(db), auditTables: ["users"], asyncWrite: true,});When asyncWrite is true, captureLog() returns immediately without waiting for the write to complete.
Individual captureLog() calls can override the global setting:
// Global async, but force this specific write to be synchronousawait audit.captureLog({ tableName: "users", operation: "INSERT", recordId: "1", asyncWrite: false,});Error handling
Section titled “Error handling”The onError callback is called when an async write or an afterLog hook fails. If not set, errors are logged to console.error with sanitized output (no PII).
const audit = betterAudit({ database: drizzleAuditAdapter(db), auditTables: ["users"], onError: (error) => logger.error("Audit error", error),});In synchronous mode (asyncWrite: false), write errors and afterLog hook errors propagate to the caller as thrown exceptions. onError is only invoked for errors that cannot be thrown — async writes and async afterLog failures.
Manual context
Section titled “Manual context”The framework middleware (Hono/Express) automatically injects the actor from the HTTP request. For code that runs outside a request — background jobs, cron tasks, CLI scripts — use audit.withContext() to set actor identity manually. You can also enrich context mid-request with mergeAuditContext() and read it with getAuditContext().
See the Actor Context guide for full details, examples, and troubleshooting.
ORM adapter options
Section titled “ORM adapter options”The ORM proxy (Drizzle) and extension (Prisma) accept additional options beyond the core betterAudit() config.
import { betterAudit } from "@usebetterdev/audit";import { drizzleAuditAdapter, withAuditProxy } from "@usebetterdev/audit/drizzle";
const audit = betterAudit({ database: drizzleAuditAdapter(db), auditTables: ["users", "orders"],});
const auditedDb = withAuditProxy(db, audit.captureLog, { primaryKey: "id", onError: (error) => logger.error("Audit proxy error", error), onMissingRecordId: "warn", skipBeforeState: ["large_events"], maxBeforeStateRows: 1000,});| Option | Type | Default | Description |
|---|---|---|---|
primaryKey | string | "id" | Fallback primary key column name for record ID extraction. |
onError | (error: unknown) => void | console.error | Called when audit capture fails. |
onMissingRecordId | "warn" | "skip" | "throw" | "warn" | What to do when the record ID cannot be determined. |
skipBeforeState | string[] | [] | Table names to skip before-state SELECT for (improves performance for large tables). |
maxBeforeStateRows | number | 1000 | Safety limit for before-state SELECT queries. |
import { PrismaClient } from "./generated/prisma/client.js";import { betterAudit } from "@usebetterdev/audit";import { prismaAuditAdapter, withAuditExtension } from "@usebetterdev/audit/prisma";
const prisma = new PrismaClient();
const audit = betterAudit({ database: prismaAuditAdapter(prisma), auditTables: ["users", "orders"],});
const auditedPrisma = withAuditExtension(prisma, audit.captureLog, { bulkMode: "per-row", onError: (error) => logger.error("Audit extension error", error), skipBeforeCapture: ["large_events"], maxBeforeStateRows: 100, tableNameTransform: (modelName) => modelName.toLowerCase() + "s",});| Option | Type | Default | Description |
|---|---|---|---|
bulkMode | "per-row" | "bulk" | "per-row" | How createMany/updateMany/deleteMany are logged. "per-row" creates one entry per row; "bulk" creates one entry for the whole operation. |
onError | (error: unknown) => void | console.error | Called when audit capture fails. |
metadata | Record<string, unknown> | — | Extra metadata merged into every log entry from this extension. |
tableNameTransform | (modelName: string) => string | auto-detect | Maps Prisma model name to SQL table name. Overrides auto-detection from @@map. |
skipBeforeCapture | string[] | [] | SQL table names to skip before-state capture for (no extra findUnique/findMany). |
maxBeforeStateRows | number | 100 | Max rows fetched by before-state findMany. |
Console integration
Section titled “Console integration”Connect Better Audit to UseBetter Console for a web-based audit dashboard:
import { betterConsole } from "@usebetterdev/console";import { betterAudit } from "@usebetterdev/audit";import { drizzleAuditAdapter } from "@usebetterdev/audit/drizzle";
const consoleInstance = betterConsole({ connectionTokenHash: process.env.BETTER_CONSOLE_TOKEN_HASH ?? "", sessions: { autoApprove: process.env.NODE_ENV === "development" },});
const audit = betterAudit({ database: drizzleAuditAdapter(db), auditTables: ["users"], console: consoleInstance,});This registers audit dashboard endpoints with the console automatically.
Next steps
Section titled “Next steps”- Actor Context — how actor identity propagates through your request lifecycle
- Adapters — adapter-specific setup, API reference, and error handling
- Quick Start — working example with ORM + framework middleware in one page