Leave Localhost logoLeave LocalhostDocs
Security

Audit Log

A minimal, first-party audit log for application-level events. It is a starter baseline you can keep, extend, or delete — not a compliance product. A simple time-based retention cron bounds table growth, but there is no legal hold, immutable storage, or SIEM export.

A minimal, first-party audit log for application-level events. It is a starter baseline you can keep, extend, or delete — not a compliance product. A simple time-based retention cron bounds table growth, but there is no legal hold, immutable storage, or SIEM export.

What it is

Events are written inside the same transaction as the change they describe, so an audit row commits atomically with its action (no "best effort" gap for the shipped security/admin/billing/org paths).

Local code vs. the community component

This starter ships local code rather than the community convex-audit-log component. The decision: a single table plus a small helper is easier to read, trivial to grep, and easy for a buyer to delete than a third-party dependency with its own schema and upgrade surface. Transparency wins for a boilerplate. Re-evaluate if you need features it provides (e.g. retention tooling).

What is logged

See Audit Event Catalog for the full catalog. Categories: auth, organization, member, billing, security, admin, system.

Each row stores: the action, a derived category, a result (success/failure/denied), a redacted actor snapshot (app user id, Better Auth id, normalized email), an optional organization id, an optional target (type + opaque id), a short human summary, optional flat metadata, and a timestamp.

What is NOT logged

The write helper (redactAuditMetadata) drops sensitive keys and caps value length, and call sites are written to avoid sensitive data in the first place:

  • No passwords, password state, or hashes/salts.
  • No verification codes, OTPs, session tokens, or session hashes.
  • No magic-link tokens or raw Better Auth payloads.
  • No provider webhook payloads, billing addresses, or card/CVV details.
  • No raw request bodies. Metadata is a flat record of scalars only.

Metadata keys matching pass|secret|token|hash|salt|cookie|authorization|otp| code|credential|private|ssn|card|cvv are replaced with [redacted] even if a caller passes them by mistake.

Reading the log

The log is visible only to platform super admins, at /admin/audit-log (see Admin: Global Audit Log). The query requires requireSuperAdmin and is indexed + paginated, so it stays fast at thousands of rows. A Convex query cannot write, so denied reads are not themselves audited; denied admin writes are (admin.access_denied).

Hardening paths (optional, not starter defaults)

Retention

The starter bounds audit_events growth with a daily cleanup cron, audit.cleanup.deleteExpiredAuditEvents (registered in crons.ts), which deletes events older than 365 days. It runs in bounded batches and reschedules itself while a backlog remains.

This is a starter default, not a compliance policy. The window lives in convex/audit/cleanup.ts (AUDIT_RETENTION_MS). Before launch:

  • Change the window to match your needs, or remove the cron entirely to retain events forever.
  • If you have legal-hold, immutable-storage, or export requirements, replace this with a dedicated solution (see the hardening paths above) — do not rely on this cron as a record-keeping guarantee.

Adding an event

See Recipe: Write an Audit Event.

Removing the feature

The audit log underpins the admin panel. To remove both, follow Removing the Admin Panel and Audit Log.

On this page