The mental shift
Most teams treat audit logs as a debug artefact: write what seems relevant, hope it’s enough when someone asks. This is wrong.
The audit log is the system’s record of truth. When the regulator asks “what happened on May 12 at 3:14 PM,” the audit log is what you show them. When a customer disputes a charge, the audit log is what you reconstruct from. When you need to verify your own system didn’t misbehave, the audit log is what you query.
That’s an API. Treat it like one.
What “audit log as API” means
Schema. Every audit entry has a defined shape. New fields are added, never removed. Renamed fields go through a deprecation cycle. The schema is in version control.
Versioning. Old entries don’t change format. A query that worked against year-old entries still works. The schema has a version field; the parser handles all live versions.
SLO. Every state-changing operation produces an audit entry. The SLO is “audit entry written before the operation’s response is sent.” Operations that fail to write audit fail the operation; you’d rather fail loudly than silently miss audit.
Documentation. The audit log schema is documented like any external API. Field meanings, when each action fires, what’s in details for each action type. Customers and auditors read it.
What the Genie audit log captures
{
"seq": 12345,
"occurred_at": "2026-05-12T15:14:23.456Z",
"actor": "alice@example.com",
"action": "kyc.approve",
"target": "application_id_7890",
"details": {
"decision_path": "standard",
"bureau_scores": [720, 695, 710],
"fraud_score": 0.12,
"approver_id": "system",
"audit_root": 12340
},
"prev_hash": "ab12...",
"row_hash": "cd34..."
}
Schema is stable; new action types add to it without breaking old queries. Every entry is hash-chained to its predecessor; tampering is detectable.
What the audit log enables
Forensics. “Show me what alice@example.com did between 14:00 and 16:00.” One query.
Compliance reporting. “How many EDD-triggered KYCs in Q1?” SELECT count(*) FROM audit_log WHERE action='kyc.edd_trigger' AND occurred_at BETWEEN .... One query.
Customer disputes. “Why was my loan rejected?” Look up the customer’s application ID; query the audit log; show the decision path with reasons.
System integrity verification. Run the hash-chain verifier; if it passes, the log hasn’t been tampered with since the last verified head.
Regulator audits. “Show me 50 random EDD decisions from March.” Random sample; query each; show the audit row + the supporting details.
Five use cases, all served from one well-designed log. Replace it with grepped-application-logs and each use case becomes a forensic exercise.
The discipline
Treating the audit log as an API requires:
- Schema review on every change. A new field is a contract update. Review it like any other API change.
- Tests that verify the log shape. “After action X, audit entry must contain fields Y and Z.” Failing tests block merge.
- No silent log skips. If the log write fails, the operation fails. Don’t catch and continue.
- Independent verification. A separate process walks the chain periodically and verifies. Not the same process that writes; otherwise it can’t detect tampering.
For Genie’s pkg/compliance/audit.go, the schema is stable across the project’s life. The hash chain is verified nightly. The schema is documented at the package level. The API contract is explicit.
The case for over-engineering this
It’s tempting to call this over-engineering. “We’ll log what’s useful; if we need more, we’ll add it.”
The cost of getting the log wrong appears at the wrong moment — the regulator audit, the customer dispute, the breach investigation. By the time you need the log, it’s too late to design it.
The audit log is the part of the system most likely to save the project at a critical moment. Design it like that.
For any regulated AI workload, this is the single highest-leverage architectural choice that’s also the easiest to skip. Don’t skip it.