Audit Service — Domain Model
Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · 02 DDD
1. Aggregates
1.1 AuditEntry (root aggregate — immutable)
Write-once. No methods modify state post-persist. The immutability invariant is enforced at the PostgreSQL role level.
| Field | Type | Description |
|---|---|---|
id | AuditEntryId | ULID with aud_ prefix |
tenantId | TenantId | null | null = platform-level event |
eventType | AuditEventType | Taxonomy enum (see §3) |
actorId | UserId | null | User or service account ID |
actorType | ActorType | USER | SERVICE_ACCOUNT | SYSTEM |
resourceType | ResourceType | e.g. USER, PATIENT, TENANT |
resourceId | string | ID of the affected resource |
action | AuditAction | CREATE | UPDATE | DELETE | READ | EVALUATE | EXPORT |
outcome | AuditOutcome | SUCCESS | FAILURE | PARTIAL |
sourceService | string | Originating service name |
sourceEventId | string | CloudEvents id — globally unique, enables dedup |
nodeId | NodeId | null | Hierarchy node context if applicable |
metadata | Record<string, unknown> | Additional context (changed fields, IP, purpose) |
chainHash | ChainHash | SHA-256 hex 64 chars |
occurredAt | Timestamp | When the action happened (from event envelope) |
recordedAt | Timestamp | When Audit Service ingested the entry (server time) |
Invariants:
sourceEventIdis unique; duplicate ingestion is idempotently ACK'd and silently skipped.chainHash = SHA-256(prev_id : sourceEventId : tenantId : occurredAt : resourceId).- No UPDATE or DELETE is ever issued against this aggregate.
tenantIdmay only benullforactorType = SYSTEMor platform-level sources.
State machine: None. AuditEntry is a terminal write-once fact.
1.2 AuditExport (aggregate)
Represents an asynchronous export job. Mutable until reaching a terminal state.
| Field | Type | Description |
|---|---|---|
id | AuditExportId | ULID with exp_ prefix |
tenantId | TenantId | null | Scope of the export |
requestedBy | UserId | Super Admin who requested it |
status | ExportStatus | queued | processing | completed | failed |
filters | AuditQueryFilters | Immutable filter bag |
format | ExportFormat | ndjson | csv |
fileUrl | string | null | Signed URL; set on completion |
recordCount | number | null | Set on completion |
createdAt | Timestamp | — |
completedAt | Timestamp | null | — |
State machine:
Invariants:
fileUrlmust be set before transitioning tocompleted.- On
completed→ emitaudit.export.completedevent. - The export request is itself stored as an
AuditEntrywith typeBULK_EXPORT.
2. Value Objects
| Name | Type | Description |
|---|---|---|
AuditEntryId | Branded<string, 'AuditEntryId'> | ULID with aud_ prefix |
AuditExportId | Branded<string, 'AuditExportId'> | ULID with exp_ prefix |
ChainHash | Branded<string, 'ChainHash'> | 64-char hex SHA-256 |
AuditQueryFilters | Value object | Immutable filter bag |
3. Enums
| Enum | Values |
|---|---|
ActorType | USER, SERVICE_ACCOUNT, SYSTEM |
AuditAction | CREATE, UPDATE, DELETE, READ, EVALUATE, EXPORT |
AuditOutcome | SUCCESS, FAILURE, PARTIAL |
ExportFormat | ndjson, csv |
ExportStatus | queued, processing, completed, failed |
4. Event Type Taxonomy
| Category | Types |
|---|---|
| Identity | USER_CREATED, USER_UPDATED, USER_SUSPENDED, USER_REACTIVATED, USER_DEACTIVATED, USER_LOGIN, USER_LOGIN_FAILED, USER_LOGOUT, USER_MFA_ENROLLED, USER_SESSION_EXPIRED |
| Service Accounts | SERVICE_ACCOUNT_CREATED, SERVICE_ACCOUNT_REVOKED, SERVICE_ACCOUNT_SECRET_ROTATED |
| Tenant | TENANT_CREATED, TENANT_ACTIVATED, TENANT_SUSPENDED, TENANT_REACTIVATED, TENANT_TERMINATED, TENANT_CONFIG_CHANGED, SUBSCRIPTION_UPDATED, SUBSCRIPTION_EXPIRED |
| Hierarchy | NODE_CREATED, NODE_UPDATED, NODE_DEACTIVATED, EDGE_CREATED, EDGE_REMOVED, PROFILE_CREATED, MEMBERSHIP_ASSIGNED, MEMBERSHIP_REMOVED |
| Licensing | LICENSE_ASSIGNED, LICENSE_UPDATED, LICENSE_REVOKED, LICENSE_EXPIRED |
| Access Policy | ROLE_CREATED, ROLE_UPDATED, ROLE_ARCHIVED, ASSIGNMENT_CREATED, ASSIGNMENT_REMOVED, POLICY_CREATED, POLICY_UPDATED |
| Platform Admin | PLATFORM_CONFIG_CHANGED, FEATURE_FLAG_CREATED, FEATURE_FLAG_UPDATED, FEATURE_FLAG_ARCHIVED |
| Patient Data Access | PATIENT_RECORD_READ, CLINICAL_NOTE_READ, LAB_RESULT_READ, MEDICATION_READ |
| Security | ACCESS_DENIED, ACCOUNT_LOCKED, SUSPICIOUS_ACTIVITY, BULK_EXPORT |
| Config | FEATURE_CREATED, FEATURE_UPDATED, ROLE_GRANT_CREATED, ROLE_GRANT_UPDATED, USER_OVERRIDE_CREATED, USER_OVERRIDE_DELETED, DESIGN_TOKEN_UPDATED |
5. Domain Events Emitted by This Service
| Event | NATS Subject | Trigger |
|---|---|---|
audit.export.requested.v1 | com.ghasi-ehr.audit.export.requested | Export job queued |
audit.export.completed.v1 | com.ghasi-ehr.audit.export.completed | Export file written |
audit.dlq.alert.v1 | com.ghasi-ehr.audit.dlq.alert | Message max-deliver exceeded |
6. Ubiquitous Language
| Term | Definition |
|---|---|
| AuditEntry | An immutable, write-once record of a compliance-relevant platform event |
| Chain Hash | SHA-256 digest binding each entry to the previous one; enables tamper detection |
| Accounting of Disclosures | HIPAA-analogue right: patient knows who accessed their record and when |
| Ingestion | Consuming a CloudEvent from NATS and normalising it into an AuditEntry |
| Dead Letter Queue | NATS subject audit.dlq for messages exhausting max-deliver retries |
| Export | Async job streaming AuditEntry rows matching a filter set to NDJSON or CSV |
| Tamper-evident | Property that any modification of a stored entry invalidates the chain hash |
| Meta-audit | Auditing the audit service's own operations (e.g. bulk exports) |
| Platform Level | Events where tenant_id IS NULL — actions by platform infrastructure |
| Dedup | Idempotent skip of an event whose source_event_id already exists |