Append-Only Compliance Log
A regulator asks: who approved this transaction at 14:23 on the day of the incident? The system that has an audit trail answers in a query. The system that does not spends two weeks reconstructing the answer from logs, database snapshots, and faded memories. @frenchexdev/ddd-audit-trail reifies the audit log as a port with the same immutability discipline as event sourcing — entries are append-only, immutable, infrastructure-free.
What Audit Trail Reifies
An audit trail is operationally distinct from the event store. The event store captures what happened in the domain (events that aggregates emitted), whereas the audit trail captures what happened in the system that mattered for compliance — who logged in, who approved a transaction, who exported customer data, who changed a permission. The two overlap (some domain events are also audit entries) but the retention, queryability, and compliance contract differ enough that the corpus treats them as separate ports.
The discipline is the persisted-truth one, again: entries cannot be edited, must use ISO-string timestamps (not mutable Date), must not import infrastructure (an audit entry that depends on pg would change shape with the substrate, defeating the immutability). The naming convention is hard — AuditTrail suffix at error severity, unlike most other suffix rules in the corpus — because the registry is keyed by class name and downstream compliance machinery dispatches by suffix.
The Runtime: ddd-audit-trail and adapter
Two packages — ddd-audit-trail, ddd-audit-trail-postgres-adapter — both M4/M5 stubs pinned to AuditTrailImmutableLogRequirement. The Postgres adapter is the obvious first substrate — append-only is straightforward in SQL with a CREATE INDEX ON ... (timestamp) for the time-range queries that compliance typically asks.
The Analyzer: ddd-audit-trail-analyzer
Spec-first (spec.ts). Priority Medium. Three rules that mirror the event-sourcing trio almost line-for-line:
rules: [
{ kind: 'require-name-suffix', code: 'DDD-AUDITTRAIL-001', severity: 'error',
suffix: 'AuditTrail',
message: 'Audit trail entry must end with "AuditTrail" suffix (entries are an append-only log keyed by type)' },
{ kind: 'forbid-framework-types', code: 'DDD-AUDITTRAIL-002', severity: 'warning',
forbiddenTypeNames: ['Date'],
message: 'Audit trail parameter "{type}" uses a mutable Date — prefer an ISO-string for an append-only log' },
{ kind: 'forbid-infrastructure-import', code: 'DDD-AUDITTRAIL-003', severity: 'error',
forbiddenModulePatterns: ['pg', 'mysql', 'mongodb', 'redis', 'kafka'],
message: 'Audit trail module imports infrastructure module "{module}" — entries must remain domain-only' },
],rules: [
{ kind: 'require-name-suffix', code: 'DDD-AUDITTRAIL-001', severity: 'error',
suffix: 'AuditTrail',
message: 'Audit trail entry must end with "AuditTrail" suffix (entries are an append-only log keyed by type)' },
{ kind: 'forbid-framework-types', code: 'DDD-AUDITTRAIL-002', severity: 'warning',
forbiddenTypeNames: ['Date'],
message: 'Audit trail parameter "{type}" uses a mutable Date — prefer an ISO-string for an append-only log' },
{ kind: 'forbid-infrastructure-import', code: 'DDD-AUDITTRAIL-003', severity: 'error',
forbiddenModulePatterns: ['pg', 'mysql', 'mongodb', 'redis', 'kafka'],
message: 'Audit trail module imports infrastructure module "{module}" — entries must remain domain-only' },
],The repetition with event sourcing is deliberate. Both patterns persist immutable truth; the analyzer's rules encode the same invariants because they protect the same architectural property. The codegen-emitted stub, like event-sourcing's, will be an immutable class with readonly fields and a kind discriminator.
Cross-Links
- Conceptually adjacent to
@EventStoreand Event Sourcing — same immutability discipline, different compliance contract. - Written by
@CommandHandlers after authorisation checks succeed, by@DomainServicecalls that span aggregates, and by application-service boundary code. - Queried by compliance and audit dashboards, never by the domain itself.
- Lives behind
@Port/@Adapter; the Postgres adapter satisfies the conformance suite that pins the append-only semantics.
Back to the series index.