Skip to Content

Below is a production-grade audit logging design tailored to your Control Plane with direct DB authority.

This is not optional plumbing in your architecture — it is the mechanism that makes bypassing APIs survivable.

I’ll give you:

  1. Conceptual model (what an audit log is)
  2. Concrete schema (Prisma-ready)
  3. Mutation patterns (how to write logs safely)
  4. Query patterns (how you’ll actually use it)
  5. Rules that must never be violated

1️⃣ What an Audit Log is (lock this definition)

An audit log is an append-only, immutable record of intent and effect for privileged actions.

It answers who, what, when, why, and what changed.

It is not:

  • application logs
  • error logs
  • metrics
  • event streams

2️⃣ Core design principles

These must be written into docs and code comments.

Invariants

  • Append-only
  • Never updated
  • Never deleted
  • Written in the same transaction as the mutation
  • Human-attributable

If any of these are false, it is not an audit log.


3️⃣ Canonical schema (Prisma)

3.1 Audit actor

You must distinguish who caused the change.

model AuditActor { id String @id @default(cuid()) type AuditActorType userId String? // control-plane user serviceName String? // automation / job ipAddress String? userAgent String? createdAt DateTime @default(now()) logs AuditLog[] } enum AuditActorType { CONTROL_PLANE_USER SYSTEM AUTOMATION }

3.2 Audit log (root record)

model AuditLog { id String @id @default(cuid()) actorId String actor AuditActor @relation(fields: [actorId], references: [id]) action String // e.g. "ASSET_APPROVE", "SUBSCRIPTION_OVERRIDE" domain String // assets, billing, users, chat entityType String // Emote, User, Subscription entityId String reason String? // REQUIRED for destructive actions severity AuditSeverity metadata Json? // contextual info (non-sensitive) beforeState Json? afterState Json? createdAt DateTime @default(now()) @@index([domain, entityType, entityId]) @@index([actorId]) @@index([createdAt]) } enum AuditSeverity { INFO WARNING CRITICAL }

4️⃣ What goes in each field (strict rules)

action

Machine-readable, stable, uppercase

Examples:

  • EMOTE_UPLOAD
  • EMOTE_APPROVE
  • USER_ROLE_CHANGE
  • SUBSCRIPTION_MANUAL_ADJUST
  • FEATURED_CONTENT_SET

Never put prose here.


domain

One of your documented domains:

  • assets
  • billing
  • identity
  • chat
  • content

This lets you answer: “What area was touched?”


entityType / entityId

The primary object affected.

If a workflow touches multiple entities:

  • one audit log per entity or
  • one root log + child logs (advanced)

Do not overload a single record.


beforeState / afterState

Rules:

  • Must be JSON serializable
  • Must exclude secrets
  • Must be partial, not full row dumps

Example (good):

{ "status": "PENDING" }

Example (bad):

{ "entireUserRow": { ... } }

reason

Required when:

  • deleting
  • overriding billing state
  • bypassing automation
  • modifying permissions

Make it painful to skip.


5️⃣ Writing audit logs correctly (transaction pattern)

Golden rule

No mutation without an audit log in the same transaction

Example (Prisma)

await prisma.$transaction(async (tx) => { const before = await tx.emote.findUnique({ where: { id } }) await tx.emote.update({ where: { id }, data: { status: "APPROVED" }, }) await tx.auditLog.create({ data: { actorId, action: "EMOTE_APPROVE", domain: "assets", entityType: "Emote", entityId: id, severity: "INFO", beforeState: { status: before?.status }, afterState: { status: "APPROVED" }, reason: "Reviewed and approved", }, }) })

If this pattern isn’t followed everywhere, your audit system is fiction.


6️⃣ Read patterns (how you’ll actually use this)

6.1 Entity history

“What happened to this emote?”

prisma.auditLog.findMany({ where: { domain: "assets", entityType: "Emote", entityId: emoteId, }, orderBy: { createdAt: "desc" }, })

6.2 Operator activity

“What did this admin do today?”

prisma.auditLog.findMany({ where: { actorId }, orderBy: { createdAt: "desc" }, })

6.3 Dangerous actions

“Show me critical mutations”

prisma.auditLog.findMany({ where: { severity: "CRITICAL" }, })

7️⃣ Severity classification (do not hand-wave)

SeverityWhen to use
INFONormal admin workflows
WARNINGManual overrides, retries
CRITICALBilling overrides, deletions, permission changes

CRITICAL logs should:

  • require a reason
  • optionally require confirmation UI
  • be reviewed periodically

8️⃣ Explicit non-goals (document this)

Audit logs are not:

  • rollback systems
  • undo history
  • realtime event streams

They are for accountability and forensics.


9️⃣ Documentation page structure

Where this lives:

docs/ ├─ control-plane/ │ ├─ audit.mdx ← concepts + rules │ ├─ invariants.mdx ├─ domains/ │ ├─ audit/ │ │ ├─ index.mdx │ │ └─ schema.mdx ← canonical schema

Split concepts from schema so you can evolve.


10️⃣ Hard rule you should adopt

If you ever feel tempted to skip an audit log, you should not be doing that operation.

This system gives you:

  • confidence to move fast
  • safety to bypass APIs
  • historical truth when things break

If you want next

I can:

  • add DB-level safeguards (triggers / RLS-style checks)
  • design audit diff compression
  • add immutable storage / WORM strategies
  • generate admin UI patterns for audit review
  • define retention & redaction policies

Say which (or “all”).

Last updated on