Audit Trail¶
This document describes what is logged, where logs are stored, how long they are retained, and how to query them. All table names and endpoint paths are verified against the source code.
1. What is Logged¶
The platform maintains three distinct audit mechanisms, one per service domain:
clinical-api — AuditLog¶
Source: services/clinical-api/prisma/schema.prisma (AuditLog model)
The audit_log table records every significant CRUD action on clinical entities. Each row
contains:
| Column | Description |
|---|---|
id |
UUID primary key |
organisation_id |
Tenant identifier |
event_type |
Action descriptor (e.g. patient.created, case.updated) |
entity_type |
Model name (e.g. Patient, Case, SkinFinding) |
entity_id |
UUID of the affected record |
actor_snapshot |
JSON blob of the acting client/user identity at the time of the action |
correlation_id |
Optional — links to the originating HTTP request or event chain |
metadata |
Optional JSON — additional context (e.g. changed fields) |
timestamp |
UTC timestamp, indexed with organisation_id for efficient querying |
Write path: service-layer code calls the AuditService after mutating records. There are no
triggers; logging is at the application layer.
orchestrator — WorkflowEvent and WorkflowIntervention¶
Source: services/orchestrator/prisma/schema.prisma
workflow_event is an append-only log of every event that enters the orchestrator (Redis
Pub/Sub messages routed to workflow instances). Each row contains:
| Column | Description |
|---|---|
event_type |
Event name (e.g. ai_review.completed) |
envelope |
Full JSON event envelope |
received_at |
UTC timestamp |
processed |
Boolean — whether the event was consumed by a workflow step |
workflow_intervention logs every manual administrative action performed on a workflow
instance (retry, cancel, halt, resume, supersede). Each row contains:
| Column | Description |
|---|---|
action |
Enum: retry_step, cancel, supersede, halt, resume |
performed_by |
String identifier of the admin user |
reason |
Optional free-text justification |
before_state |
JSON snapshot of the instance state before the intervention |
after_state |
JSON snapshot of the instance state after the intervention |
human-review — ReviewAuditLog¶
Source: services/human-review/prisma/schema.prisma (ReviewAuditLog model)
The review_audit_log table records every state transition on a Review. Each row contains:
| Column | Description |
|---|---|
review_id |
FK to the Review |
reviewer_id |
Optional — the reviewer who performed the action |
action |
Enum: created, claimed, unclaimed, submitted, declined, decline_exhausted, cancelled |
metadata |
Optional JSON — additional context |
created_at |
UTC timestamp |
admin-api — AdminAuditLog¶
Source: services/admin-api/prisma/schema.prisma (AdminAuditLog model)
The admin_audit_log table records every internal-staff action against the admin
console — sign-in / sign-out and every dashboard read. This is what attributes "who
looked at this org's data, when" for compliance review:
| Column | Description |
|---|---|
actor_id |
FK to the AdminUser who performed the action |
action |
String, e.g. auth.login, auth.logout, dashboard.volume.read, dashboard.ai-review.read, dashboard.org-detail.read |
target |
Optional — usually the org id the action targeted, or ALL for platform-scope reads |
metadata |
Optional JSON — range, filters, etc. that scoped the read |
at |
UTC timestamp |
Every dashboard endpoint in admin-api records an AdminAuditLog row regardless of cache
hit or miss, so the trail captures intent (who asked) rather than just upstream traffic.
Auditable actions (non-exhaustive): auth.login, auth.logout, dashboard.<path>.read,
org.created/updated/archived/restored, admin_user.invited/updated/disabled/enabled/token_regenerated/activated,
platform_user.created/updated/deactivated/reactivated/password_reset,
platform_user.membership_added/membership_role_changed/membership_removed,
platform_user.product_granted/product_revoked,
platform_user.promoted_from_patient (patient → platform user promotion, carries
clinical_patient_id + email in metadata),
role.created/updated/deleted, product.created/updated/deleted.
2. Where it is Stored¶
All audit data is stored in each service's MySQL database (RDS in production) in the respective service schema. There is no centralised audit store in v1.
| Service | Database table | Service DB |
|---|---|---|
| clinical-api | audit_log |
clinical-api MySQL |
| orchestrator | workflow_event, workflow_intervention |
orchestrator MySQL |
| human-review | review_audit_log |
human-review MySQL |
| admin-api | admin_audit_log |
admin-api MySQL |
3. Retention¶
Currently in v1: no automated retention trim is applied to audit tables. Records accumulate
indefinitely. A RetentionPolicy model exists in clinical-api for configuring per-entity
retention days, but no nightly cron job to enforce it is shipped in v1.
Planned: a scheduled retention service that evaluates RetentionPolicy rows and removes
expired records. Audit logs will require a separate, longer retention window (typically 7
years for medical records) that should be configured explicitly before any automated trim
is enabled.
See Retention + GDPR Erasure for more detail.
4. How to Query¶
clinical-api audit log¶
GET /v1/clinical-api/audit
?entity_type=Patient
?entity_id=<uuid>
?event_type=patient.created
?limit=100
Source: services/clinical-api/src/audit/audit.controller.ts — @Get('audit') handler,
protected by @RequireScopes('events:read').
orchestrator workflow events¶
Per-instance event history:
GET /v1/orchestrator/workflow-instances/:id/events
Source: services/orchestrator/src/instances/instances.controller.ts — @Get(':id/events'),
protected by @RequireScopes('orchestrator:read-instances').
Per-instance intervention history:
GET /v1/orchestrator/workflow-instances/:id/interventions
human-review audit log¶
Per-review audit trail (admin only):
GET /v1/human-review/admin/reviews/:id/audit
Source: services/human-review/src/admin-reviews/admin-reviews.controller.ts —
@Get(':id/audit'), protected by @RequireScopes('human-review:admin').
5. Tamper-Evidence¶
Audit tables are written append-only at the application layer. There are no application-level endpoints to delete or modify audit rows in any of the three audit tables.
Limitations in v1:
- There is no cryptographic hash chain linking successive audit entries.
- Database-level access controls (preventing direct SQL deletes) are an operational concern and are not enforced at the application layer.
- There is no WORM (Write-Once Read-Many) storage or immutable log service in v1.
Planned: hash-chained audit logs or integration with an immutable log service is flagged as future work.