@imola-solutions/audit-trail
v0.1.0
Published
Compliance-grade audit trail sink for @imola-solutions/* onAudit events. Buffers, batches, and persists to Supabase, HTTP, or console.
Readme
@imola-solutions/audit-trail
Compliance-grade audit trail sink. Wire it once; pass auditTrail.record as the onAudit prop to every other @imola-solutions/* package. It buffers, batches, and persists to Supabase (or HTTP or console).
Install
Normally installed via npx @imola-solutions/setup. To install manually:
npm install @imola-solutions/audit-trailUsage
import { createAuditTrail } from "@imola-solutions/audit-trail";
import { createChatService } from "@imola-solutions/chat";
import { client } from "./lib/imola-client";
// One-time wiring
const auditTrail = createAuditTrail({
apiClient: client, // Supabase-compatible
actorResolver: async () => {
const { data: { user } } = await client.auth.getUser();
return { id: user?.id ?? "unknown", name: user?.email };
},
transport: "supabase", // or "console" | "http"
bufferSize: 50,
flushIntervalMs: 5000,
tags: ["bopc", "prod"], // stamped on every event
});
// Then feed .record to every service that emits audit events
const chatService = createChatService({
apiClient: client,
currentUser: { id: "USR-1", name: "Alice" },
onAudit: auditTrail.record,
});API
createAuditTrail(options)
Returns { record, flush, shutdown, stats }.
Options:
apiClient(required fortransport: "supabase") — Supabase-compatible clientactorResolver(required) —async () => ({ id, name? }). Called at record-time so the actor is snapshotted at the moment the event happened.transport—"supabase"(default) |"console"|"http"httpEndpoint(required fortransport: "http") — POSTs{ events: [...] }bufferSize(default50) — flush trigger; events accumulate in memory until this many bufferedflushIntervalMs(default5000) — periodic flush interval (ignored forconsole)onAudit(optional) — receives self-audit events (audit-trail.record,audit-trail.flush,audit-trail.flush-error) so you can monitor the trail itselftags(default[]) — string tags attached to every event (product/env)
Methods
record(event)— Non-blocking. Buffers (or writes inline for console). Never awaits. Never throws.flush()— Force a flush now. Returns a promise. Safe to call anytime.shutdown()— Stops the periodic timer + flushes. Call before your process exits.stats— Live counters:{ buffered, flushed, failed, lastFlushAt, lastError }.
Resilience
- Non-blocking:
record()never blocks the calling flow. Domain services can safelyawaittheir own logic without ever waiting on the audit trail. - Retention on failure: Flush errors keep the events in the buffer for the next attempt. No silent data loss.
- Self-audit events: every buffered event, every flush success, every flush error emits a
self-auditso consumers can see when the trail itself misbehaves. - Broken meta-logger safe: if the
onAuditcallback (used for self-audit) throws, it's swallowed — never breaks the primary trail.
Event shape written to audit_events
{
"id": "uuid",
"action": "chat.message.send",
"actor_id": "USR-1",
"actor_name": "Alice",
"target_type": "message",
"target_id": "MSG-42",
"metadata": { "threadId": "T-1", "contentLength": 22 },
"success": true,
"tags": ["bopc", "prod"],
"timestamp": "2026-07-06T04:20:00Z"
}SQL
Applied automatically by npx @imola-solutions/setup. Files:
sql/init.sql— table + indexessql/rls.sql— RLS policies (writers insert, readers see own events)
Peer dependencies
None hard peer deps. If using transport: "supabase" you'll want @supabase/supabase-js for typing purposes but the factory itself only requires that the injected apiClient exposes .from(table).insert(rows).
