Skip to content

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.

Terminal window
npx @backcap/cli add audit-log

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"
}
FieldTypeDescription
idstringUnique identifier (UUID)
actorstringWho performed the action
actionAuditActionValidated NOUN.VERB format
resourcestringWhat was acted upon
metadataRecord<string, unknown> | undefinedOptional contextual data
timestampDateWhen the action occurred

AuditEntry.create() returns Result<AuditEntry, InvalidAuditAction>.

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"); // Ok
const bad = AuditAction.create("user.login"); // Fail (lowercase)
const bad2 = AuditAction.create("LOGIN"); // Fail (no dot)
EventFieldsEmitted when
EntryRecordedentryId, actor, action, resource, occurredAtA new audit entry is appended
ErrorFactoryWhen
InvalidAuditActioncreate(value)Action format validation fails
AuditQueryFailedcreate(reason)Query execution fails

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>

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>
interface IAuditStore {
append(entry: AuditEntry): Promise<void>;
query(filters: AuditFilters): Promise<{ entries: AuditEntry[]; total: number }>;
}

No delete or update methods — append-only by design.

interface AuditFilters {
actor?: string;
action?: string;
resource?: string;
fromDate?: Date;
toDate?: Date;
limit?: number;
offset?: number;
}

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.