Audit Logs
Turning on audit logging, what gets recorded, and the two admin endpoints for querying it.
Audit Logs
Audit logging is off by default. When it's on, every significant thing that happens — logins, password changes, admin actions, org membership changes — gets published as a structured audit.Event to an internal queue, processed by background workers, and (if you configured a store) queryable through the two endpoints below.
Configuration
goauth.WithAudit(goauth.AuditConfig{
Enabled: true, // optional, default false
FailureMode: audit.AuditFailureOpen, // optional, default fail-open — or audit.AuditFailureClosed
RetentionDays: 90, // optional, default 90 (0 = forever)
QueueSize: 1000, // optional, default 1000
Workers: 3, // optional, default 3
BatchSize: 50, // optional, default 50
FlushInterval: 100 * time.Millisecond, // optional, default 100ms
Sinks: []audit.EventSink{myKafkaSink}, // optional, default none
})
goauth.WithAuditSink(myOtherSink) // optional — adds one more sink at a time| Field | Type | Default | Notes |
|---|---|---|---|
Enabled | bool | false | The two endpoints below return 404 audit_not_configured while this is off. |
FailureMode | audit.AuditFailureMode | fail-open | Fail-open: if publishing an event errors, the request that triggered it still succeeds. Fail-closed: that request fails instead — use this only if you have a compliance requirement that an action must not happen without a corresponding log entry. |
RetentionDays | int | 90 | 0 means keep forever. |
QueueSize | int | 1000 | Events are published async onto this queue; if it's full, behavior follows FailureMode. |
Workers | int | 3 | Background goroutines draining the queue. |
BatchSize | int | 50 | Events per batch write to the store. |
FlushInterval | time.Duration | 100ms | Max time a partial batch waits before flushing anyway. |
Sinks | []audit.EventSink | none | Extra destinations — Kafka, NATS, a webhook — fired in addition to the queryable store. |
WithAuditSink appends one sink at a time without needing to repeat the whole config; calling WithAudit more than once doesn't duplicate sinks already added.
This is separate from application logging
config.Logger (the *slog.Logger you pass to WithLogger) is for operational logs — errors, warnings, debug output. Audit events are a distinct, structured stream meant to answer "who did what, when" for security review or compliance, and only exist at all if Enabled: true here.
Frontend client setup
The Client examples below call the same apiRequest(baseUrl, method, path, body) helper used throughout these guides — see Client → Setup. The named-method wrapper for both endpoints on this page lives on Client → Admin.
What gets recorded
Every event carries a type, a severity (info / warning / error / critical), whether it successed, and — depending on the event — an actor, a target user, a session, an org, an IP, and a user agent. The full type list, grouped by area:
| Area | Event types |
|---|---|
| Login | login.success, login.failed, login.locked, logout, admin.login.success, admin.login.failed |
| Registration | user.registered |
email.verification.sent, email.verified | |
| Password | password.changed, password.reset.requested, password.reset.completed |
| Sessions | session.created, session.refreshed, session.revoked, session.revoked_all |
| OAuth | oauth.login, oauth.linked, oauth.unlinked |
| Admin | admin.user.created, admin.user.updated, admin.user.deleted, admin.user.banned, admin.user.unbanned |
| Roles | role.changed |
| Organizations | organization.created, organization.deleted, organization.member.invited, organization.member.removed |
This list can grow across versions — treat event_type as an opaque string in your filters rather than an exhaustive enum you validate against.
Querying audit logs
List all audit logs — GET /admin/audit-logs
Query params (all optional)
| Param | Type | Notes |
|---|---|---|
offset | int | Default 0 |
limit | int | Default 50, max 200 |
event_type | string | Exact match against type |
actor_id | string | Who performed the action |
target_user_id | string | Who the action was performed on |
session_id | string | |
org_id | string | |
search | string | Matches metadata and user agent |
from, to | RFC3339 datetime | Silently ignored if the value doesn't parse — no error, just no filter applied |
success | true | false | Anything else is ignored |
Response (200 OK)
{
"events": [
{
"id": "...",
"type": "login.failed",
"severity": "warning",
"success": false,
"actorId": null,
"targetUserId": null,
"sessionId": null,
"orgId": null,
"ip": "192.168.1.1",
"userAgent": "Mozilla/5.0 ...",
"requestId": "...",
"metadata": {},
"createdAt": "2026-08-09T12:00:00Z"
}
],
"total": 1000,
"limit": 50,
"offset": 0
}Errors
| Code | Status | Cause |
|---|---|---|
audit_not_configured | 404 | WithAudit(AuditConfig{Enabled: true}) was never set |
internal_error | 500 | Query failure against the store |
curl
curl "https://api.myapp.com/admin/audit-logs?event_type=login.failed&limit=50" \
-H "Origin: https://myapp.com" \
-b admin-cookies.txtProgrammatic (Go)
events, total, err := auth.Services.AuditLog.List(ctx, port.AuditLogFilter{
Type: goauth.String("login.failed"),
Limit: 50,
})auth.Services.AuditLog is nil when audit logging isn't enabled — check that before calling into it directly the way the handler does, rather than relying on a panic.
Client
const { events, total } = await apiRequest(
API_BASE, "GET", "/admin/audit-logs?event_type=login.failed&limit=50"
);List one user's audit logs — GET /admin/users/{id}/audit-logs
Identical query params and response shape to the endpoint above — this one just forces target_user_id to the {id} in the path, overriding anything passed in the query string for that field.
curl
curl "https://api.myapp.com/admin/users/b3f1.../audit-logs?limit=50" \
-H "Origin: https://myapp.com" \
-b admin-cookies.txtProgrammatic (Go)
events, total, err := auth.Services.AuditLog.List(ctx, port.AuditLogFilter{
TargetUserID: &userID,
Limit: 50,
})Client
const { events, total } = await apiRequest(API_BASE, "GET", `/admin/users/${userId}/audit-logs`);