@venturekit-pro/audit
@venturekit-pro/audit is a strictly-scoped, append-only audit log. It records WHO did WHAT, TO WHAT, WHEN, and HOW IT WENT — nothing else. Domain concerns (USD cost, payment totals, token usage, LLM provider/model, latency) live in their own packages’ tables (e.g. @venturekit-pro/ai’s llm_cost_events) and reference the audit row through correlationId / target. The package never sees money, tokens, or any other domain primitive.
Event kinds ('blog.save', 'order.refunded', 'security.login', …) are free-form strings the calling app chooses and namespaces however it likes.
What it gives you
Section titled “What it gives you”- One canonical
audit_eventstable keyed by(tenant_id, kind, target_type, target_id). - Append-only by construction — the migration
REVOKEsUPDATEandDELETEonaudit_eventsfromPUBLIC, so a SQL bug cannot silently mutate compliance history. Retention pruning runs as a privileged maintenance role. - Idempotent writes via an optional
idempotencyKey— retries of the same operation never produce duplicate rows. - Pure-audit aggregation —
countEvents()/monthlyEventCounts()with an optionalbyKindPrefixsplit for per-namespace tallies.
Installation
Section titled “Installation”npm install @venturekit-pro/auditThe package ships its schema as vk_audit_0001_init.sql and declares it in its package.json ("vk": { "migrations": "src/migrations" }), so vk migrate auto-discovers and applies it for any project that depends on the package — no wiring required. getAuditMigrationsDir() returns the directory for a custom migration runner.
Every helper takes a Querier as its first argument — the same shape as @venturekit/data’s query (or a transaction’s query), so the package has no hard data-layer dependency.
Recording
Section titled “Recording”import { record } from '@venturekit-pro/audit';import { query } from '@venturekit/data';
await record(query, { tenantId, actor: { type: 'user', id: userSub }, // 'user' | 'service' | 'cron' | 'api_key' | 'system' kind: 'order.refunded', // free-form, app-namespaced target: { type: 'order', id: order.id }, // optional — 'cron.tick' has no target payload: { reason: 'duplicate' }, // free-form jsonb; keep it small and PII-free});
// Thread several events into one logical operation, and make retries safe:await record(query, { tenantId, actor: { type: 'service', id: 'workflow-runner' }, kind: 'workflow.step.completed', target: { type: 'workflow_run', id: run.id }, status: 'succeeded', // 'queued' | 'running' | 'succeeded' | 'failed' | 'skipped' (default 'succeeded') correlationId: run.id, payload: { stepName: 'critique' }, idempotencyKey: `${run.id}/step-3/attempt-1`,});Pass tenantId: null only for platform-wide events. On status: 'failed', add errorMessage. record() returns the persisted AuditEvent row.
Listing
Section titled “Listing”import { listEvents } from '@venturekit-pro/audit';
await listEvents(query, { tenantId, limit: 50 });await listEvents(query, { tenantId, kindPrefix: 'order.' });await listEvents(query, { tenantId, targetType: 'order', targetId });await listEvents(query, { tenantId, correlationId: runId });await listEvents(query, { tenantId, status: 'failed' });await listEvents(query, { tenantId, actorType: 'cron' });await listEvents(query, { tenantId, limit: 50, beforeId: lastSeenId }); // keyset paginationCounting / activity volume
Section titled “Counting / activity volume”import { countEvents, monthlyEventCounts } from '@venturekit-pro/audit';
// Total events in a window (same filters as listEvents, plus since/until):await countEvents(query, { tenantId, since: new Date('2026-05-01'), until: new Date('2026-06-01') });// → { count: 1247, byKindPrefix: {} }
// Split by whatever kind-prefixes the app cares about:await countEvents(query, { tenantId, byKindPrefix: ['order.', 'security.', 'workflow.'] });// → { count: 1247, byKindPrefix: { 'order.': 320, 'security.': 12, 'workflow.': 915 } }
// Monthly activity buckets:await monthlyEventCounts(query, { tenantId, monthCount: 6, byKindPrefix: ['order.'] });// → [{ month: '2025-12-01', count: 245, byKindPrefix: { 'order.': 80 } }, …]Retention
Section titled “Retention”pruneAuditEvents() is the only mutating helper besides record(). It must run as a privileged maintenance role (the application role has DELETE revoked by the migration), is batched to avoid long transactions (batchSize, default 1000, max 100_000), and never auto-fires — wire it into your own cron with a retention policy that fits your compliance regime.
import { pruneAuditEvents, previewPrune } from '@venturekit-pro/audit';
// What WOULD be pruned — safe from the app role:await previewPrune(query, { olderThanDays: 365, batchSize: 1000 });// → [{ id, createdAt, tenantId, kind }, …]
// Actually delete (requires DELETE privileges); returns the number of rows removed:await pruneAuditEvents(adminQuery, { olderThanDays: 365, batchSize: 1000, tenantId });Related
Section titled “Related”- API Reference — every export
@venturekit-pro/ai— records cost events that reference audit rows bycorrelationId@venturekit/data— thequeryfunction the helpers expect