Skip to content

Audit log

Every state-changing operation in PLUR Enterprise writes an audit row. Reads are also audited at a coarser grain (count-and-aggregate, not per-call) to keep volume bounded.

audit_log table (Postgres):

ColumnNotes
idbigserial PK
tstimestamp, indexed
orgorg identifier
actorJSON: `{type: “user"
actionstring, e.g. engram.create, user.disable, webhook.fire
targetJSON: what was acted on (engram id, user id, scope, …)
channelmcp · rest · admin · scim · webhook · system
outcomesuccess · failure · denied
payloadJSONB: action-specific details
ipsource IP (hashed if PLUR_AUDIT_HASH_IPS=true)
user_agentclient UA
request_idcorrelation ID across log lines

writeAudit() (src/audit/write.ts) is never-throws — audit failures don’t break the request, but they log to stderr.

CategoryActions
Authauth.login, auth.logout, auth.failed, auth.token.issued, auth.token.revoked
Engramsengram.create, engram.update, engram.retire, engram.pin, engram.unpin
Episodesepisode.create, episode.export
Packspack.install, pack.uninstall, pack.export
Sessionssession.start, session.end
Usersuser.create, user.update, user.disable, user.delete
Groupsgroup.create, group.update, group.member.add, group.member.remove
API keysapikey.issue, apikey.rotate, apikey.revoke
SCIMscim.user.create, scim.user.update, scim.user.disable, scim.group.*
Adminadmin.role.assign, admin.scope.create, admin.settings.update
Webhookswebhook.register, webhook.fire, webhook.disable

Engram reads are aggregated nightly into audit_aggregate rather than logged per-call, to control volume.

/admin/audit — filter by date range, actor, action, channel, outcome. Export CSV or JSON for retention archives.

The table is straightforward Postgres; use whatever client you’d use against any DB:

SELECT ts, actor->>'name' AS who, action, target, outcome
FROM audit_log
WHERE ts > NOW() - INTERVAL '7 days'
AND outcome = 'denied'
ORDER BY ts DESC;
Terminal window
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://plur.your-org.com/api/v1/admin/audit?from=2026-05-01&channel=scim"

Subscribe to the audit.entry event for streaming delivery. Be aware: high-traffic orgs can generate hundreds of audit rows per minute — make sure your endpoint can keep up.

Default: 365 days. Configure with PLUR_AUDIT_RETENTION_DAYS. A nightly job purges rows older than that.

For compliance scenarios that require longer retention (3 years, 7 years), set retention to 0 (no purge) and rely on backups for archival. Audit volume on a mid-sized org is roughly 10–100 MB/month; storing several years is cheap.

  • The contents of engrams themselves on reads (only counts).
  • Webhook delivery payloads beyond event ID and outcome.
  • LLM-side activity (PLUR doesn’t run the LLM).

If you need fuller capture (every recall query, every inject task), enable verbose audit with PLUR_AUDIT_VERBOSE=true. This dramatically increases volume — plan storage accordingly.

For GDPR-conscious deployments, set PLUR_AUDIT_HASH_IPS=true. PLUR stores HMAC-SHA256(ip, salt) instead of the raw IP, with the salt rotated quarterly. You can still detect repeat sources within a quarter; you can’t reverse the hash.

If Postgres is briefly unavailable, audit writes go to a local fallback log (/var/lib/plur/audit-fallback.jsonl) and replay when the DB is back. The request itself completes normally — auditing is best-effort by design, but the fallback ensures no rows are silently lost in transient outages.