AuditEvent resources.
Key properties
| Property | Behavior |
|---|---|
| Storage | Separate audit_log table (not in resources) |
| Mutability | Append-only and immutable (DB triggers prevent UPDATE/DELETE) |
| Reliability | Best-effort: audit failures must not fail the main request |
| Throughput | Async write path (queue + background insert) |
| Output format | Full serialized FHIR AuditEvent (R4/R5 depending on fhir.version) |
| Access | Only via internal admin endpoints (not queryable via normal FHIR APIs) |
What gets audited
Zunder audits FHIR REST interactions under/fhir including (when enabled):
read,vread,historysearchcreate,update,patch,deletecapabilities(GET /fhir/metadata)$operationand$exportbatch/transaction
Audit logging currently targets the FHIR API surface. Internal routes like
/admin and /health
are not audited by the same middleware.Captured security context (SMART / OAuth2)
When requests are authenticated, Zunder binds the audit record to the SMART security context:client_id(OAuth2 client)user_id(end-user subject when present)scopes(granted scopes)token_typeinferred asuser/system/anonymous/unknown
details.
Captured data access semantics
For each interaction, Zunder captures (best-effort):- Target resource:
resource_type+resource_idwhen known - Patient subject:
patient_idwhen resolvable - Request correlation:
request_id(server-generated), client IP, user agent - Outcome:
successfor HTTP< 400authz_failurefor401/403processing_failurefor other>= 400responses
Search events and entity.query
FHIR recommends that servers capture search activity as an Execute event and include the raw
HTTP request as base64 in AuditEvent.entity.query. Zunder can do this for search requests:
- Stores the full HTTP start line + headers + body as base64binary
- Also records the “harmonized” request URL string (path + query) in
details
OperationOutcome capture
On failures, Zunder can capture the returnedOperationOutcome and include it in the serialized
AuditEvent (contained + linked from entity.what).
Patient resolution
When possible, audit records include the patient subject:- From SMART context (
patientclaim on the token, if present) - From a compartment path (
/fhir/Patient/{id}/...) - From direct resource targets (
Patient/{id}) - For successful searches: by inspecting a
searchsetBundle and extracting referenced patients
For searches that touch multiple patients, Zunder can emit one audit record per patient to
improve privacy segmentation. This is configurable.
Storage model (audit_log)
Audit records are stored in PostgreSQL in the audit_log table.
- Structured columns for common filters (action, outcome, patient_id, request_id, …)
audit_eventcolumn stores the complete FHIR AuditEvent asJSONBdetailsstores additional structured metadata (SMART + HTTP context)
server/migrations/001_init.sql) and is protected
with triggers to prevent modification.
Admin API
Audit logs are intentionally not accessible via the normal FHIR resource API. Instead, Zunder exposes an internal admin interface (behind admin auth middleware).List audit events
GET /admin/audit/events
Query parameters:
action: interaction (e.g.read,search,create,transaction)outcome:success|authz_failure|processing_failureresourceType,resourceId,patientId,clientId,userId,requestIdlimit(default100, max1000),offset
Get a single audit event
GET /admin/audit/events/{id}
Returns the full stored row including the serialized audit_event JSON.
Configuration
Audit logging is configured underlogging.audit:
Operational notes
- Audit writes are queued and inserted asynchronously; on DB errors, the server logs warnings and continues serving requests.
- The audit log table can grow quickly. Use standard Postgres operational strategies (partitioning, retention policies, backups) depending on your compliance needs.