Skip to main content

Audit Trail

What is audited

Every significant action writes a ConsentAuditEvent row to PostgreSQL and a FHIR AuditEvent resource to HAPI:

ActionEvent typeFHIR action code
Consent decision permitconsent-decisionE (Execute), outcome 0
Consent decision denyconsent-decisionE (Execute), outcome 4
Consent createdconsent-createC (Create)
Consent updatedconsent-updateU (Update)
Consent revokedconsent-revocationU

All writes are @Async — the FHIR request is never blocked by an audit write.

IHE ATNA profile

FHIR AuditEvent resources conform to the IHE ATNA profile with DICOM codes:

{
"resourceType": "AuditEvent",
"type": {
"system": "http://dicom.nema.org/resources/ontology/DCM",
"code": "110110",
"display": "Patient Record"
},
"subtype": [{
"system": "http://dicom.nema.org/resources/ontology/DCM",
"code": "110153",
"display": "consent-decision"
}],
"action": "E",
"outcome": "0",
"outcomeDesc": "provision.type=permit · Observation in provision.class · operation=READ in permitted_operations=rs · status=active · period valid",
"recorded": "2025-06-01T14:32:00Z",
"agent": [
{ "who": { "reference": "Device/my-smart-app" }, "requestor": true },
{ "who": { "reference": "Patient/patient-123" }, "requestor": false }
],
"source": {
"observer": { "display": "AJ FHIR Consent Manager" },
"type": [{ "code": "4", "display": "Application Server Process" }]
},
"entity": [{
"what": { "display": "ConsentRecord/42" },
"type": { "code": "2", "display": "System Object" }
}]
}

Purpose of use

The purpose_of_use JWT claim is extracted and stored in every audit row. Missing claim defaults to TREATMENT.

CodeMeaning
TREATMENTDirect patient care (default)
PAYMENTInsurance and billing
OPERATIONSHealthcare operations
RESEARCHClinical research

Non-blocking design

1. ConsentService.evaluate() runs (synchronous — sub-millisecond)
2. FHIR request served or denied (synchronous)
3. ConsentAuditEvent row written to PostgreSQL (async)
4. FHIR AuditEvent resource written to HAPI (async, best-effort)

The local DB write always happens first. The FHIR write is best-effort — if HAPI is temporarily unavailable, the local audit row is preserved and fhir_audit_event_id remains null until the resource is written on a subsequent call.

Querying

Patient portal

Patients see their full audit trail at /consent/portal/history. The dashboard shows the last 10 events.

FHIR API

GET /fhir/AuditEvent?patient=Patient/patient-123
Authorization: Bearer <clinician-token>

Database

-- All consent decisions for a patient in the last 7 days
SELECT event_type, action, outcome, purpose_of_use, recorded_at
FROM consent_audit_event
WHERE patient_id = 'Patient/patient-123'
AND recorded_at > NOW() - INTERVAL '7 days'
ORDER BY recorded_at DESC;

-- All denied decisions across all patients
SELECT patient_id, resource_type, http_method, outcome_desc, recorded_at
FROM consent_audit_event
WHERE consent_decision = 'deny'
ORDER BY recorded_at DESC;

-- Decisions by a specific actor
SELECT patient_id, consent_decision, resource_type, recorded_at
FROM consent_audit_event
WHERE agent_id = 'Device/my-smart-app'
ORDER BY recorded_at DESC;

Retention

The audit table is append-only by design. No automated purge is implemented.

Your organisation's data retention policy and applicable regulations govern retention periods. HIPAA requires a minimum of six years for audit records. GDPR requires proportionate retention periods that can be justified.

Break-glass audit

The Community Edition does not include break-glass emergency access or the break_glass_event table. Break-glass auditing is available in the Enterprise Edition.


Next: API Reference →