Leave Localhost logoLeave LocalhostDocs
Recipes

Recipe: Write an Audit Event

Add a new event to the audit log in three small steps. All audit writes are server-side; clients can never write audit rows.

Add a new event to the audit log in three small steps. All audit writes are server-side; clients can never write audit rows.

1. Add the action to the catalog

In convex/audit/validators.ts, add a literal in three places (they are kept in lockstep on purpose):

// auditActionValidator union
v.literal("workspace.record_exported"),

// AuditAction type union
| "workspace.record_exported"

// AUDIT_ACTION_CATEGORY map
"workspace.record_exported": "organization",

Pick a category from: auth, organization, member, billing, security, admin, system.

2. Call the helper from your mutation

Inside the mutation that performs the change — in the same transaction — call writeAuditEvent:

import { writeAuditEvent } from "./audit/events";

await writeAuditEvent(ctx, {
  action: "workspace.record_exported",
  // result defaults to "success"
  actorUserId: actor.appUserId,
  actorAuthUserId: actor.authUserId,
  actorEmail: actorUser?.email ?? undefined,
  organizationId: actor.organizationId,
  targetType: "organization",
  targetId: actor.organizationId,
  summary: `Exported ${count} records`,
  metadata: { count }, // flat scalars only
});

Resolve the actor server-side from your existing context (e.g. the requireAppPermission actor, or resolveAuditActor(ctx) in convex/audit/actor.ts). Never accept actor ids from the client.

From an action, HTTP action, or scheduled job

Those contexts have no ctx.db. Call the internal mutation instead:

await ctx.runMutation(internal.audit.events.recordAuditEvent, {
  action: "workspace.record_exported",
  summary: "Exported records",
});

Rules

  • Never put secrets, tokens, codes, hashes, passwords, or raw payloads in metadata. The helper redacts known-sensitive keys, but the call site is your first line of defense. See Audit Log.
  • Keep summary short and human-readable — it is the admin table's main column.
  • metadata is a flat record of string | number | boolean | null only.

On this page