Audit Trail
What is audited
Every significant action writes a ConsentAuditEvent row to PostgreSQL and a FHIR AuditEvent resource to HAPI:
| Action | Event type | FHIR action code |
|---|---|---|
| Consent decision permit | consent-decision | E (Execute), outcome 0 |
| Consent decision deny | consent-decision | E (Execute), outcome 4 |
| Consent created | consent-create | C (Create) |
| Consent updated | consent-update | U (Update) |
| Consent revoked | consent-revocation | U |
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.
| Code | Meaning |
|---|---|
TREATMENT | Direct patient care (default) |
PAYMENT | Insurance and billing |
OPERATIONS | Healthcare operations |
RESEARCH | Clinical 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.
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 →