Audit Log
In short: A running record of security- and administration-relevant actions (who did what, when, to whom) — serves traceability after the fact, e.g. for role assignments or other admin interventions.
In more detail: An audit log has to be written in a deliberately fault-tolerant way — a logging error must never block or crash the actual action itself. Typical structure: who acted (actor), what was done (action), on which object (target), plus optional structured details.
Our context: app/utils/auditLog.ts at Emzett, visible in the admin panel under “Audit log” (see Dynamic role system (granular RBAC) for access control to it). Writes protected by try/catch, so that a DB error while logging doesn’t prevent, say, a role assignment.
In Depth
An audit log differs from normal application logging (error/debug output that is rotated/deleted at some point) in purpose and requirements: it has to be complete, permanent and tamper-proof, because in case of doubt it serves as evidence — who changed which permission when, who accessed which data. That’s why an audit log typically ends up in its own, usually insert-only table (entries are never changed or deleted, only appended), often with stricter access rights than the rest of the database — ideally not even an administrator can delete individual entries afterwards without that being logged in turn.
Structure of an entry
A typical audit log entry follows an actor-action-target scheme:
- Actor — who acted (user ID, but system processes such as a cron job count as actors too)
- Action — what was done, usually as a descriptive string (
role.grant,user.delete,settings.update) - Target — on which object (ID of the affected record)
- Details — optional structured additional information (e.g. old and new value for a change)
- Timestamp and often also the actor’s IP address or session ID, so that incidents can later be attributed to a specific session
For security-critical changes (e.g. permission changes), it’s worth storing both the old and the new value ({ from: "user", to: "admin" }) instead of only the new one — otherwise it can no longer be reconstructed afterwards what the previous state was.
Fault tolerance as a core principle
The “write it fault-tolerantly” principle is crucial: an audit log entry is a side effect of the actual action, not the action itself. If a logging error blocked the main action, a broken logging system could accidentally paralyse the whole application — a classic example of “the control must not become more important than the thing being controlled”. At the same time, a failed audit log entry mustn’t simply vanish silently — it’s common to at least write the error to the regular error logging (e.g. Sentry), so that gaps in the audit log are noticed even if they don’t block the main action.
async function grantRole(userId: string, role: string) {
await db.update(users).set({ role }).where(eq(users.id, userId))
try {
await logAudit({ actor: currentUser.id, action: "role.grant", target: userId, details: { role } })
} catch (err) {
console.error("Audit log failed:", err) // the role assignment still went through
}
}How it differs from related concepts
An audit log is often confused with an event log or activity feed, but serves a different purpose: an activity feed (e.g. “X commented on Y”) is meant for users and may well be incomplete or shortened; an audit log is meant for traceability/compliance and has to be complete. A database changelog (e.g. via Postgres’ pg_audit extension or trigger-based history tables) automatically records EVERY change at the database level, independent of the application logic — which is more comprehensive, but also much noisier, because it provides no semantic classification such as “this was a role assignment”, only raw row changes.
Legally, an audit log is also relevant in the context of the GDPR: it documents who had access to personal data, which can be decisive in the case of a request for access or a data breach (Art. 33 GDPR, obligation to report within 72 hours) in order to be able to narrow down the scope of an incident at all.
See also: Dynamic role system (granular RBAC), Protocol, GDPR