Set integrityChain: true to seal each log with prevHash and contentHash. Default is false.
This is detection only — not WORM object storage, not a legal hold.
Fields
On LogbunLog when integrity is on:
prevHash— previous sealed log’scontentHash. Genesis isINTEGRITY_GENESIS(64 zero hex).contentHash— SHA-256 hex ofprevHash + '\n' + canonical payload.
The canonical payload is a stable JSON subset: id, tenantId, actorId, action, entityId, oldValues, newValues, metadata, createdAt. Request decoration (ipAddress, userAgent) is excluded.
Chains are per-tenant. Logs without a tenant use the __global__ tip.
Root identity fields (id, action, actorId, createdAt, tenantId, prevHash, contentHash) are never deleted by redactPaths.
Restore after restart
The per-tenant tip is restored from:
- Unacked WAL / journal entries (including recovery waves after the bootstrap cap).
- Then the newest destination row (
querywithlimit: 1, newest-first).
logs[0] is the newest tip. A missing contentHash on that row is an empty chain, not genesis from a wrong/older row. Restore fails closed rather than sealing from a truncated recovery prefix.
Destination adapters must persist and round-trip prevHash / contentHash. Built-in SQLite/Turso/ClickHouse adapters store prev_hash / content_hash and best-effort ALTER TABLE older schemas.
Verify
verifyIntegrity expects an oldest-first list:
import { INTEGRITY_GENESIS } from 'logbun';
const { ok, failedAt, error } = await audit.verifyIntegrity(oldestFirst, {
genesis: INTEGRITY_GENESIS, // default
});Returns { ok, failedAt, error? }. Failures include missing integrity fields, prevHash mismatch, and contentHash mismatch (payload tampered).