Skip to content

Integrity

Optional per-tenant SHA-256 hash chain for tamper evidence.

Updated View as Markdown

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’s contentHash. Genesis is INTEGRITY_GENESIS (64 zero hex).
  • contentHash — SHA-256 hex of prevHash + '\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:

  1. Unacked WAL / journal entries (including recovery waves after the bootstrap cap).
  2. Then the newest destination row (query with limit: 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).

Navigation

Type to search…

↑↓ navigate↵ selectEsc close