Audit Log Domain
The audit-log domain provides a tamper-evident record of all significant actions with validated action formats, rich filtering, and pagination for TypeScript backends. It enforces append-only design at every layer — domain, application, and contracts.
Install
Section titled “Install”npx @backcap/cli add audit-logDomain Model
Section titled “Domain Model”AuditEntry Entity
Section titled “AuditEntry Entity”The AuditEntry entity is immutable by design. It has no mutation methods — audit entries are append-only.
import { AuditEntry } from "./domains/audit-log/domain/entities/audit-entry.entity";
const result = AuditEntry.create({ id: crypto.randomUUID(), actor: "user-123", action: "USER.LOGIN", resource: "auth/session", metadata: { ip: "192.168.1.1" },});
if (result.isOk()) { const entry = result.unwrap(); console.log(entry.actor); // "user-123" console.log(entry.action.value); // "USER.LOGIN" console.log(entry.resource); // "auth/session"}| Field | Type | Description |
|---|---|---|
id | string | Unique identifier (UUID) |
actor | string | Who performed the action |
action | AuditAction | Validated NOUN.VERB format |
resource | string | What was acted upon |
metadata | Record<string, unknown> | undefined | Optional contextual data |
timestamp | Date | When the action occurred |
AuditEntry.create() returns Result<AuditEntry, InvalidAuditAction>.
AuditAction Value Object
Section titled “AuditAction Value Object”Validates that actions follow the NOUN.VERB format — uppercase, dot-separated (e.g., USER.LOGIN, POST.CREATED, FLAG.TOGGLED).
import { AuditAction } from "./domains/audit-log/domain/value-objects/audit-action.vo";
const action = AuditAction.create("USER.LOGIN"); // Okconst bad = AuditAction.create("user.login"); // Fail (lowercase)const bad2 = AuditAction.create("LOGIN"); // Fail (no dot)Domain Events
Section titled “Domain Events”| Event | Fields | Emitted when |
|---|---|---|
EntryRecorded | entryId, actor, action, resource, occurredAt | A new audit entry is appended |
Domain Errors
Section titled “Domain Errors”| Error | Factory | When |
|---|---|---|
InvalidAuditAction | create(value) | Action format validation fails |
AuditQueryFailed | create(reason) | Query execution fails |
Use Cases
Section titled “Use Cases”RecordEntry
Section titled “RecordEntry”Append a new audit entry.
const recordEntry = new RecordEntry(auditStore);const result = await recordEntry.execute({ actor: "user-123", action: "USER.LOGIN", resource: "auth/session", metadata: { ip: "192.168.1.1" },});// Result<{ output: RecordEntryOutput; event: EntryRecorded }, Error>QueryAuditLog
Section titled “QueryAuditLog”Search and filter audit entries.
const queryAuditLog = new QueryAuditLog(auditStore);const result = await queryAuditLog.execute({ actor: "user-123", fromDate: new Date("2024-01-01"), limit: 50, offset: 0,});// Result<{ entries: QueryAuditLogEntry[]; total: number }, AuditQueryFailed>IAuditStore
Section titled “IAuditStore”interface IAuditStore { append(entry: AuditEntry): Promise<void>; query(filters: AuditFilters): Promise<{ entries: AuditEntry[]; total: number }>;}No delete or update methods — append-only by design.
AuditFilters
Section titled “AuditFilters”interface AuditFilters { actor?: string; action?: string; resource?: string; fromDate?: Date; toDate?: Date; limit?: number; offset?: number;}Retention Policies
Section titled “Retention Policies”The audit log stores entries indefinitely by default. To implement retention, create a scheduled job outside the domain layer using direct database access. Consider archiving entries to cold storage before purging.