WeaveKit Docs
Guides

Audit

The audit subsystem records immutable events for every data mutation (and every read denial) at one uniform boundary: the data-access layer.

The audit subsystem records immutable events for every data mutation (and every read denial) at one uniform boundary: the data-access layer. REST, MCP, and any interface you add later inherit it automatically, with zero per-interface code.

Audit is an optional subsystem. It is disabled by default, and when it is off it isn't loaded at all — no import, no table, no queries.

Enable

// weavekit.config.ts
export default {
  // ...
  subsystems: {
    audit: {
      enabled: true,
      retention: '90d',                          // placeholder (auto-cleanup later)
      batch: { batchSize: 50, flushMs: 100 },    // optional; defaults shown
    },
  },
};

What gets audited automatically

Because events are recorded at the data-access boundary, every create / update / delete through REST, MCP tools, or a host call produces an event — including failures:

EventWhen
createrecord created (success)
create (isError)validation failed, e.g. data.field.required
updaterecord updated (success)
update (isError)RBAC denial (rbac.denied.update / rbac.denied.field) or record not found
deleterecord deleted (success)
delete (isError)RBAC denial or record not found
read (isError)read denied (rbac.denied.read)

Failed and denied operations are audit highlights — they are exactly the attempts you want to review.

Event model

interface AuditEvent {
  actorType: 'user' | 'system' | 'agent' | 'anonymous';  // AUDIT_ACTOR_TYPES
  actorId: string;          // subject.id, or 'system', or agent key
  action: string;           // DATA_ACTIONS (create/update/delete/read) or '<prefix>.<detail>'
  objectName?: string;      // object name
  objectId?: string;        // record id
  changes?: unknown;        // write payload / change summary
  isError?: boolean;
  errorCode?: string;       // engine SchemaError code, e.g. 'rbac.denied.field'
  meta?: Record<string, unknown>;  // e.g. agent on-behalf-of
  timestamp: Date;          // the business moment, not the flush time
}

The enum-like fields are backed by as const constants exported from the engine:

  • AUDIT_ACTOR_TYPESAGENT / USER / SYSTEM / ANONYMOUS
  • DATA_ACTIONSCREATE / UPDATE / DELETE / READ
  • ACTION_PREFIXESMCP_TOOL ('mcp.tool'), REST ('rest'); interface-layer actions compose as <prefix>.<detail>

Querying

engine.audit is available when the subsystem is enabled:

const engine = await createEngine({ /* ... */, subsystems: { audit: { enabled: true } } });

const { rows, total } = await engine.audit.query({
  actorId: 'u100',
  action: DATA_ACTIONS.UPDATE,
  objectName: 'lead',
  from: new Date('2026-08-01'),
  to: new Date(),
  limit: 50,
  offset: 0,
});
// rows are AuditEvent[], ordered ts DESC

Buffering semantics (fire-and-forget)

record() queues the event in memory and returns immediately, so audit never blocks the business critical path — which matters under concurrent, multi-user load. Events flush as a single multi-row INSERT when the batch fills (batchSize) or the flush window elapses (flushMs).

  • engine.close() flushes the remaining buffer before the pool shuts down.
  • A process crash loses only the most recent, not-yet-flushed events (audit is best-effort).
  • Batch-write failures call onError (default console.error) and never throw into the caller.
  • The stored timestamp is the business moment, so delayed flushing does not distort the trail.

Table

weavekit_audit (
  id          bigserial PRIMARY KEY,
  ts          timestamptz NOT NULL,       -- event timestamp (business moment)
  actor_type  text NOT NULL,
  actor_id    text NOT NULL,
  action      text NOT NULL,
  object      text,
  object_id   text,
  changes     jsonb,
  is_error    boolean NOT NULL DEFAULT false,
  error_code  text,
  meta        jsonb
);
-- indexes: (ts DESC), (actor_id), (object, object_id)

The table is append-only by contract: the engine exposes no update or delete path for audit rows.

Programmatic use

createEngine wires the subsystem in automatically when enabled. For direct control:

import { createAudit, createBufferedAuditSink } from '@weave-kit/engine';

const store = await createAudit(pool);                                   // storage
const sink = createBufferedAuditSink(store, { batchSize: 50, flushMs: 100 });  // L1 buffer
await sink.record({ actorType: AUDIT_ACTOR_TYPES.SYSTEM, actorId: 'system', action: DATA_ACTIONS.CREATE, objectName: 'lead', objectId: 'L1', timestamp: new Date() });
await sink.flush();

Next

  • Custom tools & guardrails — audit diff replay (before/after snapshots, subsystems.audit.replay)
  • MCPmcp.tool.<name> tool-call events (agent on-behalf-of in meta.onBehalfOf)
  • RBAC — what the denied events record

On this page