Audit log
Every state-changing operation in PLUR Enterprise writes a row to the append-only audit_log table.
Schema
Section titled “Schema”audit_log table (Postgres, per-org schema):
| Column | Notes |
|---|---|
id | PK |
created_at | timestamp |
user_id | actor — pseudonymized for long-term retention (see below) |
action | string, e.g. engram.create, apikey.revoke |
target_type / target_id | what was acted on (engram, user, key, …) |
details | JSONB: action-specific details |
ip | source IP |
prev_hash | SHA-256 of the previous row — the chain link |
signature | HMAC-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.
Tamper evidence: the signed chain
Section titled “Tamper evidence: the signed chain”The audit log is a signed hash chain:
- Each row’s
prev_hashcovers the previous row including its signature, so rewriting any past row breaks the linkage from that point forward. - Each row’s
signatureis HMAC-SHA256 (key:AUDIT_HMAC_KEY, withAUDIT_HMAC_KEY_PREVsupported 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.
Pseudonymization
Section titled “Pseudonymization”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.
What’s audited
Section titled “What’s audited”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.
Querying
Section titled “Querying”From the admin dashboard
Section titled “From the admin dashboard”/admin/audit — filter and export CSV (/admin/audit.csv) or JSONL (/admin/audit.jsonl) for retention archives.
From SQL
Section titled “From SQL”The table is straightforward Postgres:
SELECT created_at, user_id, action, target_type, target_idFROM audit_logWHERE created_at > NOW() - INTERVAL '7 days'ORDER BY created_at DESC;What’s NOT audited
Section titled “What’s NOT audited”- 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).