Skip to content

Audit log

Every state-changing operation in PLUR Enterprise writes a row to the append-only audit_log table.

audit_log table (Postgres, per-org schema):

ColumnNotes
idPK
created_attimestamp
user_idactor — pseudonymized for long-term retention (see below)
actionstring, e.g. engram.create, apikey.revoke
target_type / target_idwhat was acted on (engram, user, key, …)
detailsJSONB: action-specific details
ipsource IP
prev_hashSHA-256 of the previous row — the chain link
signatureHMAC-SHA256 over the row’s canonical bytes

Audit writes are fire-and-forget and never-throws (src/audit/write.ts) — an audit failure doesn’t break the request, but it logs to stderr.

The audit log is a signed hash chain:

  • Each row’s prev_hash covers the previous row including its signature, so rewriting any past row breaks the linkage from that point forward.
  • Each row’s signature is HMAC-SHA256 (key: AUDIT_HMAC_KEY, with AUDIT_HMAC_KEY_PREV supported for graceful rotation) over the row’s canonicalized bytes.
  • An offline verifier ships with the codebase and walks the whole chain, reporting the first divergence.

Append-only is enforced at three layers: table ownership is held by the admin role (not the runtime role), the runtime role is granted only SELECT, INSERT, and triggers block UPDATE/DELETE/TRUNCATE for everyone else. A compromised app process cannot rewrite its own audit trail. Off-host backups double as tamper evidence — an attacker on the app host cannot rewrite copies they can’t reach.

Limitation worth knowing: in a single-host deployment the HMAC key lives on the same host as the database credentials, so the chain protects against database-level tampering, not full host compromise.

Audit rows reference actors by user_id. For GDPR-conscious retention, user IDs in audit rows are pseudonymized after 24 months: the org prefix is preserved and the username is replaced by a truncated SHA-256 hash. You can still correlate a pseudonymized actor’s rows; you can’t reverse the hash.

State changes across the system: engram create/retire/pin/rescope, session start/end, API key issue/revoke, SCIM provisioning operations, SSO provider changes, admin actions, webhook subscription changes, and authentication events.

/admin/audit — filter and export CSV (/admin/audit.csv) or JSONL (/admin/audit.jsonl) for retention archives.

The table is straightforward Postgres:

SELECT created_at, user_id, action, target_type, target_id
FROM audit_log
WHERE created_at > NOW() - INTERVAL '7 days'
ORDER BY created_at DESC;
  • The contents of engrams on reads.
  • Webhook delivery payloads (delivery attempts and outcomes are logged separately).
  • LLM-side activity (PLUR doesn’t run the LLM).