The question a Slack search can't answer

A user's role silently changes from ADMIN to USER. Nobody remembers approving it. The obvious places to look — Slack, email, memory — all fail the same way: they record intent, not action, and only when someone thought to write it down. What you actually need is a record the system itself keeps, independent of anyone remembering to document anything.

What gets recorded, and what doesn't

WKFGo's audit log is append-only — rows are only ever inserted, never updated or deleted, so a compromised admin account can't cover its tracks by editing history. It tracks governance-relevant actions specifically, not every click in the app:

const (
    ActionLoginSuccess   = "auth.login.success"
    ActionLoginFailure   = "auth.login.failure"
    ActionLogin2FAPrompt = "auth.login.2fa_required"
    Action2FAEnabled     = "auth.2fa.enabled"
    Action2FADisabled    = "auth.2fa.disabled"
    ActionUserCreated    = "user.created"
    ActionUserDeleted    = "user.deleted"
    ActionUserRoleChange = "user.role_changed"
    ActionPermissionSet  = "permission.changed"
    ActionProjectDeleted = "project.deleted"
    ActionTaskDeleted    = "task.deleted"
    ActionPlanChanged    = "plan.changed"
)

That's a deliberate boundary. A log of every task edit and comment would be noise nobody reads — the entries that matter for governance are the ones with security or billing consequences: who logged in and from where, who turned 2FA off, who changed a role, who deleted a project. Each entry carries the actor, the action, what it targeted, a JSON metadata blob for action-specific detail, the source IP, and a timestamp — enough to reconstruct exactly what happened without guessing.

Scoped to your organization, not the whole platform

type AuditLog struct {
    OrganizationID *uint          `json:"organizationId,omitempty"`
    ActorID        *uint          `json:"actorId,omitempty"`
    Action         string         `json:"action"`
    TargetType     string         `json:"targetType,omitempty"`
    TargetID       string         `json:"targetId,omitempty"`
    Metadata       datatypes.JSON `json:"metadata,omitempty"`
    IP             string         `json:"ip,omitempty"`
    CreatedAt      time.Time      `json:"createdAt"`
}

An org admin querying the log only ever sees their own organization's rows — the same tenant boundary enforced everywhere else in WKFGo applies here too. A platform super-admin can opt into an all-organizations view for cross-tenant investigation, but that's a distinct, explicitly-elevated query path, not the default.

Reading the log: filters that matter

The query supports narrowing by actor, action, target type, and a date range — the shape you actually need when investigating something specific rather than scrolling a firehose. "Show me every permission change in the last 30 days" and "show me everything user 42 did on March 3rd" are both single filtered queries, not a manual scan through thousands of rows.

Common mistakes

Treating the audit log as a replacement for real-time alerting. It answers "what happened," reliably, after the fact. It doesn't page anyone when something happens — that's a separate concern (WKFGo's in-app notifications cover the "right now" case; the audit log covers "prove it later").

Expecting every action to be there. The log is intentionally narrow — task and comment edits aren't in it. If you need a full edit history for a specific task, that's task history, a different and much noisier record kept per-task, not per-organization.

Assuming a deleted user erases their trail. It doesn't — ActorID on past entries stays as-is even after the actor is gone, because the point of an audit log is that it survives the thing it's auditing.

FAQ

Can a regular team member see the audit log? No — it's an admin-scoped view (ADMIN/SUPER_ADMIN on the organization), same tier that can change roles and permissions in the first place.

Does failed-login tracking help against brute-force attempts? It's a record, not a blocker — rate limiting is the thing that actually slows a brute-force attempt down; the audit log is where you go afterward to see which account was targeted and when.

Is the log itself ever purged? No retention job runs against it — unlike recycle-bin cleanup or old backup files, audit rows are meant to accumulate, not expire.

Next step