One log. Every action. Every actor. Every module.
Every user-performable operation in Odexy is a typed
action. Every action writes one row to audit_log
with the actor + input hash + result. UI, AI agent, MCP
server, and CLI all use the same action registry — one
write path means one log, no matter how the write got in.
No matter how the write got in, one row lands.
Every entry point — the web UI a human clicks, the AI agent via MCP, the developer's cURL against the API, the operator's CLI script, an inbound webhook, a scheduled cron — reaches the same action handler through the action registry. The handler is the only place `audit_log` gets written. Which means every write is:
- Actor-attributed. The row carries the original user + the tool that invoked (`web` / `mcp` / `api` / `cli` / `webhook` / `cron`) + the AI token id if the AI was the tool.
- Input-hashed. A SHA-256 of the JSON input + a truncated preview lets you find "the deal-update that changed the amount from $50k to $500k" without scanning every row.
- Result-tagged. Success + error code (Zod validation / policy denied / not_found / etc.) stored on the row. Failed writes still log — no silent drops.
- Event-linked. The audit row cross-references the domain event that fired (`crm.deal.won`, `sales.invoice.issued`, etc.) so you can replay the exact event chain that led to any state.
- Idempotency-key-stamped. When the caller supplied an `Idempotency-Key`, it's on the row. Duplicate keys within the 24h window return the original row + a dedup marker rather than double-writing.
Retro-editing history is detectable.
Every night we compute a SHA-256 over the day's audit rows, chained onto yesterday's head. The chain-head is exposed to admins in-product + streamed to your SIEM. If someone tries to alter a row after the fact — including someone with DB access — the recomputed hash won't match, and the mismatch surfaces in the daily audit-integrity report.
What the chain looks like
day_hash = SHA256(prev_day_hash || day_rows_sorted_by_id_serialized)
# Yesterday: sha256:83b4e...cd91
# Today: sha256:d7a2c...5f08 # includes yesterday's hash
# Tomorrow: sha256:f102b...9e77 # includes today's hash Verify externally: hash the exported rows offline; compare to the chain head published in-product. Mismatch = investigate.
Ship the log to Splunk / Datadog / Sumologic / your data lake.
- Formats: CSV, JSONL, Parquet. Every export stream carries the chain-head + a manifest so downstream verification is trivial.
- Delivery: signed URL (pull), scheduled S3 sync (push), or streaming HTTP (long-poll). Cadence per org: real-time, hourly, daily, weekly.
- Filtering: per-module / per-actor / per-event- class / date range. Sensitive fields optionally redacted at export time (PII scrubbing before it leaves).
- Replay-friendly. Every row carries the event class + payload hash, so a replay tool can reconstruct the state at any timestamp.
- Retention. 12 months in-product on Business, 7 years on Enterprise. Beyond that, exports live in your SIEM or your S3.
What frameworks the audit log actually helps you pass.
- SOC 2 CC7 (System Operations). Continuous monitoring evidence; per-user access logs; anomaly detection signals — all first-class.
- HIPAA §164.312(b) (Audit Controls). Every PHI access + every PHI change auditable with actor + input + result. BAA-friendly on Enterprise.
- GDPR Article 30 (Records of processing). The audit log IS your Article 30 record — per-operation actor + purpose + retention are queryable.
- PCI-DSS 10 (Track and monitor access). Applies where you touch cardholder data. We route cardholder data through PCI-Level-1 gateways; the audit log tracks the surrounding operations (refund, dispute, subscription change).
- ISO 27001 A.12.4 (Logging and monitoring). Meets the "protected log" control class — hash-chained + integrity-verified + retention-policy-attached.
Every AI-driven action is auditable as the delegating operator's action.
Because the AI operates through the same action registry as humans, every AI-driven write is one audit_log row. The row carries BOTH the delegating operator's user id + the AI token that invoked. Filter "everything the AI did last week" or "everything Jane did — including via AI." No parallel audit surface. No "AI wing" of your SIEM.
Sample row
{
"id": "01H...",
"occurredAt": "2026-08-29T14:32:11.123Z",
"orgId": "01H...",
"actorUserId": "01H...jane",
"actorTool": "mcp",
"aiTokenId": "ai_tok_01H...",
"action": "crm.deal.update",
"inputHash": "sha256:83b4e...",
"inputPreview": "{\"dealId\":\"01H...\",\"stage\":\"negotiation\"}",
"result": "ok",
"eventClass": "crm.deal.stage_changed",
"idempotencyKey": null
} Wire your SIEM up.
Every action; every actor; every module; one log.
