Audit Log
Banking workloads need a forensic trail: who touched what, when, with what old and new state. CrateStack records audit rows inside the same transaction as the mutation they describe, so you can never observe a committed row whose audit entry didn’t also commit.Schema attribute
Opt in per model:@@audittakes no arguments- one model can declare it at most once
What gets captured
For everycreate, update, and delete the runtime writes a row to
cratestack_audit containing:
- a fresh
event_id(UUID v4) schema_nameandmodelstrings from the.cstackoperation—create,update, ordeleteprimary_keyas JSONactorderived from theCoolContext— id, claims, optional source IPtenantfromPrincipalContext.tenant.idwhen presentbeforesnapshot (null on create) andaftersnapshot (null on delete)request_idfor trace stitchingoccurred_attimestamp
PII redaction
Field attributes participate in the snapshot serializer:@pii— value replaced with"<redacted: pii>"inbefore/after@sensitive— value replaced with"<redacted: sensitive>"@server_only— field omitted entirely from the snapshot
@pii for emails,
phone numbers, and tokenized PANs; @sensitive covers internal risk
scores, dispute notes, and operator commentary.
Transactional guarantee
The audit insert participates in the mutation’s transaction. The flow is:- begin transaction
- apply the mutation
- capture
after(andbeforefor update/delete) - insert into
cratestack_audit - commit
Fan-out to downstream sinks
The in-database table is canonical. Downstream consumers (Kafka topics, SIEM, S3 archives, HTTP webhooks) implementAuditSink:
MulticastAuditSink:
CoolError::Internal rather than
silently swallowing — banks treat downstream errors as alertable, not
fire-and-forget. The default sink is NoopAuditSink; the table is the
source of truth even without one.
Schema
(schema_name, model, occurred_at DESC),
(tenant, occurred_at DESC), and undelivered rows.
The DDL is exposed as cratestack::AUDIT_TABLE_DDL. Banks running their
own migration tooling embed it; the SqlxRuntime calls it idempotently
during bootstrap.
Retention
The framework does not delete fromcratestack_audit. Banks running
regulatory retention (5 / 7 / 10 years depending on jurisdiction) move old
rows to cold storage and prune the live table via their own tooling. The
schema is index-friendly for time-window deletes.
What this is not
- not a tamper-evident chain — no per-row cryptographic signature
- not WORM storage — anyone with
DELETEon the table can rewrite history - not a substitute for application-level event sourcing
MulticastAuditSink is the integration seam.
Read Next
- Field attributes for
@pii,@sensitive,@server_only - Transaction isolation for the transactional model the audit insert participates in