Skip to content

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.