startTracevault is async and returns a TracevaultApp. Configure scopes (logical name → tableName), defaultScope, masking, and sync/async defaults. The default scope is what audit.emit and audit.query target.
import { startTracevault } from "tracevault"
const audit = await startTracevault({
driver: "postgres",
connectionString: process.env.DATABASE_URL_WRITE!,
readConnectionString: process.env.DATABASE_URL_READ,
defaultScope: "default",
scopes: {
default: { tableName: "audit_logs" },
users: { tableName: "audit_user_events" },
},
bootstrap: { ensureSchema: true },
maskFields: ["password", "token", "pin"],
defaultMode: "sync",
environment: process.env.NODE_ENV,
})Tables are chosen at startup in scopes — not per event. Use audit.getScope("users") for writes to another table. Each non-default scope has its own async queue on the write path.
const userAudit = audit.getScope("users")
await userAudit.emit({
event: "user.profile.updated",
actor: { id: "user_123", type: "user" },
target: { id: "user_123", type: "user" },
data: { field: "phone" },
})
await audit.getScope("default").emit({
event: "payment.intent.created",
actor: { id: "merchant_42", type: "merchant" },
target: { id: "payment_987", type: "payment" },
data: { amount: 1200, currency: "UYU" },
})Pass optional pool and readPool so bootstrap and runtime queries reuse your SSL configuration. Tracevault ends only pools it created; close injected pools yourself after await audit.close().
import { startTracevault } from "tracevault"
import { Pool } from "pg"
const pool = new Pool({
connectionString: process.env.DATABASE_URL!,
ssl: { rejectUnauthorized: true },
})
const audit = await startTracevault({
driver: "postgres",
connectionString: process.env.DATABASE_URL!,
pool,
defaultScope: "default",
scopes: { default: { tableName: "audit_logs" } },
})
// End injected pools yourself after await audit.close()Only event is required. Everything else is optional — including correlationId / requestId for tracing across emits.
await audit.emit({
event: "auth.login.succeeded",
actor: { id: "user_123", type: "user" },
meta: { ip: "127.0.0.1", userAgent: "curl/8" },
correlationId: "req_abc",
})Exported from tracevault: randomCorrelationId, readCorrelationIdHeader, resolveCorrelationId.
import {
randomCorrelationId,
readCorrelationIdHeader,
resolveCorrelationId,
} from "tracevault"
const correlationId = resolveCorrelationId(
readCorrelationIdHeader((name) => req.headers.get(name)),
randomCorrelationId,
)
await audit.emit({
event: "checkout.started",
correlationId,
data: { cartId: "cart_1" },
})Optional keys inside data populate generated columns (outcome, error.code → error_code, severity). Not validated by the library — patterns for dashboards.
// Optional keys inside data — mirrored to generated columns after migrations 002–003:
await audit.emit({
event: "auth.login.failed",
actor: { id: "user_123", type: "user" },
data: {
outcome: "failure",
severity: "warning",
error: { code: "AUTH_INVALID_CREDENTIALS", stage: "credential_check" },
},
})Shallow top-level diff between before and after. Still sugar around emit.
await audit.emitDiff({
event: "product.updated",
actor: { id: "user_123", type: "user" },
target: { id: "product_456", type: "product" },
before: { name: "Café", price: 120 },
after: { name: "Café", price: 150 },
})
// Persisted data:
// {
// "before": { "name": "Café", "price": 120 },
// "after": { "name": "Café", "price": 150 },
// "diff": { "price": { "before": 120, "after": 150 } }
// }sync awaits the insert. async uses an in-process FIFO — buffered events are lost if the process exits before flush(). On shutdown: await audit.flush() then await audit.close().
Schema options on Install. reads on Query columns on Data model.