startTracevault es async y devuelve un TracevaultApp. Configurá scopes (nombre lógico → tableName), defaultScope, enmascarado y defaults sync/async. El scope default es el que usan audit.emit y audit.query.
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,
})Las tablas se eligen al iniciar en scopes — no por evento. Usá audit.getScope("users") para escribir en otra tabla. Cada scope no-default tiene su propia cola async en el 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" },
})Pasá pool y readPool opcionales para que bootstrap y queries runtime reutilicen tu config SSL. Tracevault solo cierra pools que creó; cerrá los inyectados vos después de 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()Solo event es obligatorio. Todo lo demás es opcional — incluyendo correlationId / requestId para trazar entre 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",
})Exportados desde 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" },
})Claves opcionales dentro de data populan columnas generadas (outcome, error.code → error_code, severity). No las valida la librería — son patrones para 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" },
},
})Diff superficial a nivel top entre before y after. Sigue siendo azúcar sobre 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 espera el insert. async usa una FIFO in-process — eventos bufferizados se pierden si el proceso termina antes de flush(). Al apagar: await audit.flush() luego await audit.close().
Opciones de schema en Install. lecturas en Query columnas en Modelo de datos.