For AI agents: the documentation index is at /docs/aidbox/llms.txt. A Markdown version of this page is available at /docs/aidbox/access-control/audit-and-logging.md or by requesting it with the Accept: text/markdown header.
Aidbox Docs

Audit and Logging

Audit logging is essential in healthcare systems because it:

  • Protects Patient Privacy: Tracks who accessed sensitive medical records, ensuring compliance with privacy laws like HIPAA
  • Prevents Data Breaches: Helps detect and investigate unauthorized access to patient data
  • Ensures Accountability: Records all changes to medical records, creating a clear trail of who modified what and when
  • Supports Legal Requirements: Provides evidence for compliance audits and legal investigations

Aidbox provides comprehensive audit and logging capabilities:

  • FHIR Basic Audit Logging Profile (BALP) implementation
  • FHIR Resource versioning
  • Logging configuration

FHIR Basic Audit Logging Profile (BALP) implementation

Aidbox supports the FHIR BALP Implementation Guide.

Since version 2609, Aidbox publishes audit events to a built-in subscription topic. The security.audit-log.enabled setting stores them in the AuditEvent table, and the other security.audit-log.* settings are deprecated. See Migrate from the security.audit-log settings.

Audit events topic

Aidbox publishes audit events to a built-in AidboxSubscriptionTopic:

http://health-samurai.io/fhir/core/StructureDefinition/AuditEventsR4BALP

Each event is a FHIR R4 AuditEvent resource. To receive events, create an AidboxTopicDestination with this URL in the topic element. Any destination kind works: webhook, Kafka, GCP Pub/Sub, and the others listed in supported channels. To keep events in Aidbox, see Store audit events in Aidbox.

API request Aidbox Audit events topic Your application, $aidbox-audit Webhook destination Kafka destination Other destinations Recorder, AuditEvent table

How the topic behaves:

  • Destinations switch audit logging on. With no destination on the topic, Aidbox builds no audit events and requests carry no audit overhead. Delete the last destination to stop audit logging. The security.audit-log.enabled setting adds a built-in destination.
  • The topic is part of Aidbox. You do not create an AidboxSubscriptionTopic resource for it, and Aidbox rejects a stored topic that reuses its URL with 422.
  • Every destination receives each event with the same AuditEvent id, a UUIDv7. Delivery guarantees, batching, and retries come from the destination kind.
  • A destination with filterBy receives only the events that match it. Aidbox does not build an event that no destination accepts, see Filter audit events.
  • Aidbox publishes events produced from the moment a destination exists. A new destination receives no earlier events.
  • AuditEvent resources that clients create with POST /fhir/AuditEvent do not reach the topic. To publish an event of your own, call $aidbox-audit.
  • With organization-based hierarchical access control, the AuditEvent meta carries the organization of the request.

For a step-by-step setup, see How to subscribe to audit events.

Filter audit events

Add filterBy to a destination to receive part of the audit events. The audit events topic defines three filter parameters:

Filter parameterAuditEvent elementValues
typetype.coderest: FHIR and Aidbox REST API, SDC operations, password changes. 110112: SQL queries. 110113: access granted to a client. 110114: login, logout, refresh token.
subtypesubtype.codecreate, update, patch, delete, read, vread, search, $import, $load, $purge, $purge-organization, sql ($sql, $psql, $query, $dump-sql, SQL notebooks, attribute analysis), 110122 (login, refresh token), 110123 (logout), 110139 (password changed), password-force-reset, password-self-reset, AuthZ-Consent (access granted to a client). SDC operations: assemble-form, populate, populate-link, questionnaire-package, generate-form-token, generate-form-link, generate-link, start-link, submit, update-response, amend-response, submit-response.
actionactionC: create, SDC populate-link and start-link. R: read, vread. U: update, patch, password changes, SDC submit and response changes. D: delete. E: search, queries, other operations, authentication.

A subtype filter matches when any subtype of the event matches. Password reset events carry both 110139 and their reset subtype.

This filter keeps create, update, and delete events and drops reads, searches, and the rest:

"filterBy": [
  {
    "filterParameter": "action",
    "comparator": "eq",
    "value": "C,U,D"
  }
]

Comma-separated values match when any of them matches, and several filters match when all of them match. With "comparator": "ne", the same filter keeps every event except create, update, and delete. See Filter events with filterBy for the full rules.

Aidbox checks the filters before it builds an event. With the filter above on every destination, reads and searches carry no audit overhead.

Store audit events in Aidbox

To search audit events with the FHIR API and browse them on the Audit Events page of Aidbox UI, store them in the AuditEvent table. Two options do this:

  • An AidboxTopicDestination of kind audit-events-recorder on the audit events topic. You create and delete it through the API, and it accepts filterBy.
  • The security.audit-log.enabled setting. At startup, Aidbox runs a built-in recorder that stores every event.

Both options work with FHIR R4 or R4B only, and Aidbox runs one recorder at a time. See Store audit events in the AuditEvent table for setup.

Aidbox as a source of audit events

Aidbox produces audit events for significant events:

  • FHIR CRUD & Search operations for basic FHIR resources and custom resources (with BALP profiles)
  • FHIR CRUD & Search operations for Patient compartment resources (with Patient-specific BALP profiles)
  • User login and logout events (custom Aidbox event types, not BALP-conformant)
  • Password change events (DICOM subtype 110139)
  • SQL operations via $psql/$sql (custom aidbox/sql-interaction type, not BALP-conformant)
  • Bundle transaction entries (each entry audited individually with BALP profiles)

BALP profile selection

Aidbox assigns a BALP profile to each AuditEvent based on the operation type and whether the operation involves a Patient.

OperationGeneric ProfilePatient-specific Profile
CreateIHE.BasicAudit.CreateIHE.BasicAudit.PatientCreate
Read / VReadIHE.BasicAudit.ReadIHE.BasicAudit.PatientRead
Update / PatchIHE.BasicAudit.UpdateIHE.BasicAudit.PatientUpdate
DeleteIHE.BasicAudit.DeleteIHE.BasicAudit.PatientDelete
Search / QueryIHE.BasicAudit.QueryIHE.BasicAudit.PatientQuery

When Patient-specific profiles are used:

  • The resource is a Patient — CRUD operations directly on the Patient resource (e.g. PUT /fhir/Patient/123)
  • The resource is in the Patient Compartment and references a Patient (e.g. creating an Observation with subject pointing to a Patient)

Patient search (GET /fhir/Patient?...) uses the generic IHE.BasicAudit.Query profile, not IHE.BasicAudit.PatientQuery. This is because a search does not reference a specific Patient. The PatientQuery profile is used when searching compartment resources that reference a Patient (e.g. GET /fhir/Observation?patient=123).

Example: AuditEvent for Patient update

When you update a Patient resource, the generated AuditEvent uses the IHE.BasicAudit.PatientUpdate profile:

{
  "resourceType": "AuditEvent",
  "meta": {
    "profile": [
      "https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.PatientUpdate"
    ]
  },
  "type": {
    "system": "http://terminology.hl7.org/CodeSystem/audit-event-type",
    "code": "rest",
    "display": "Restful Operation"
  },
  "subtype": [
    {
      "system": "http://hl7.org/fhir/restful-interaction",
      "code": "update",
      "display": "update"
    }
  ],
  "action": "U",
  "recorded": "2026-02-25T12:00:00Z",
  "outcome": "0",
  "agent": [
    {
      "who": {
        "reference": "Client/my-client"
      },
      "requestor": true
    }
  ],
  "source": {
    "observer": {
      "display": "Aidbox"
    }
  },
  "entity": [
    {
      "what": {
        "reference": "Patient/example"
      },
      "role": {
        "system": "http://terminology.hl7.org/CodeSystem/object-role",
        "code": "4",
        "display": "Domain Resource"
      },
      "type": {
        "system": "http://terminology.hl7.org/CodeSystem/audit-entity-type",
        "code": "2",
        "display": "System Object"
      }
    }
  ]
}

Password change AuditEvent

Aidbox generates an AuditEvent with DICOM subtype 110139 ("User password changed") when:

  • PUT /User/:id sets a password that differs from the stored one;
  • a user changes their own password with /auth/change-password;
  • an administrator resets a password (extra subtype password-force-reset);
  • a user resets their password through a reset link (extra subtype password-self-reset).
{
  "resourceType": "AuditEvent",
  "type": {
    "system": "http://terminology.hl7.org/CodeSystem/audit-event-type",
    "code": "rest",
    "display": "Restful Operation"
  },
  "subtype": [
    {
      "system": "http://dicom.nema.org/resources/ontology/DCM",
      "code": "110139",
      "display": "User password changed"
    }
  ],
  "action": "U",
  "outcome": "0",
  "entity": [
    {
      "what": { "reference": "User/example-user" },
      "type": {
        "system": "http://terminology.hl7.org/CodeSystem/audit-entity-type",
        "code": "2"
      },
      "role": {
        "system": "http://terminology.hl7.org/CodeSystem/object-role",
        "code": "4"
      }
    }
  ],
  "agent": [
    {
      "who": { "identifier": { "value": "root" } },
      "requestor": true
    },
    {
      "who": { "display": "Aidbox" },
      "requestor": false
    }
  ]
}

A PUT with the unchanged password produces a regular update event, and PATCH /User/:id produces a regular patch event, both without subtype 110139. A rejected self-service change or reset produces the event with outcome 4.

Aidbox as an Audit record repository

Aidbox is an Audit record repository (ARR) for FHIR AuditEvent resources. Aidbox supports

  • POST /fhir/AuditEvent to record events
  • GET /fhir/AuditEvent to receive them

Aidbox stores the AuditEvent resources that clients create next to the events of the recorder. It does not publish them to the audit events topic. To publish an event to the topic, use $aidbox-audit.

Publish audit events from your application

Your application can publish its own AuditEvent resources to the audit events topic with the $aidbox-audit operation, available since version 2610. Aidbox delivers them to every destination of the topic next to the events it produces, so one pipeline carries the audit trail of the whole system, for example a frontend that shows patient data or a service that bypasses the FHIR API.

The operation takes a Parameters resource with two parameters:

ParameterTypeDescription
resourceAuditEventThe event to publish.
topiccanonicalURL of the audit events topic, http://health-samurai.io/fhir/core/StructureDefinition/AuditEventsR4BALP.
POST /fhir/$aidbox-audit
content-type: application/json
accept: application/json

{
  "resourceType": "Parameters",
  "parameter": [
    {
      "name": "resource",
      "resource": {
        "resourceType": "AuditEvent",
        "type": {
          "system": "http://terminology.hl7.org/CodeSystem/audit-event-type",
          "code": "rest"
        },
        "subtype": [
          {
            "system": "http://hl7.org/fhir/restful-interaction",
            "code": "read"
          }
        ],
        "action": "R",
        "recorded": "2026-10-12T10:00:00Z",
        "outcome": "0",
        "agent": [
          {
            "who": {"reference": "Practitioner/pr-1"},
            "requestor": true
          }
        ],
        "source": {
          "observer": {"display": "Patient portal"}
        },
        "entity": [
          {
            "what": {"reference": "Patient/pt-1"}
          }
        ]
      }
    },
    {
      "name": "topic",
      "valueCanonical": "http://health-samurai.io/fhir/core/StructureDefinition/AuditEventsR4BALP"
    }
  ]
}

How the operation behaves:

  • Aidbox replaces the id of the event with a UUIDv7 and delivers the event to every destination of the topic with the same id.
  • Aidbox applies the filterBy of each destination to the type, subtype, and action of the event, see Filter audit events. A destination whose filter rejects the event does not receive it.
  • Aidbox does not validate the event against the BALP profiles and does not add the organization of the request to its meta. The event reaches the destinations as you sent it.
  • Aidbox does not store the event. To keep it in the AuditEvent table, run a recorder on the topic.
  • Aidbox responds with 200 and an OperationOutcome once every destination has accepted the event. With no destinations on the topic, it responds with 200 too. A request without a Parameters body, without the resource or topic parameter, with a resource other than an AuditEvent, or with a topic other than the audit events topic returns 422.
  • The call produces no audit event of its own.

See Publish an audit event from your application for a walkthrough with the response.

External Audit record repository support

To send audit events to an external Audit record repository, create a webhook AidboxTopicDestination on the audit events topic with the repository endpoint. Aidbox delivers events as a FHIR Bundle of type history, described in Notification shape. The first entry of the bundle is an AidboxSubscriptionStatus, so check that the repository accepts this shape.

For setup instructions and a payload example, see How to subscribe to audit events.

FHIR Resource versioning

A separate version is recorded in the history table each time a resource is created, updated, or deleted.

All versions can be accessed using the _history operation.

Logging configuration

Aidbox automatically logs all auth, API, database, and network events, so in most cases, basic audit logs may be derived from Aidbox logs.

Aidbox also provides ways to extend Aidbox logs.

Audit coverage

OperationAuditedBALP ProfileNotes
REST Create (POST)YesIHE.BasicAudit.Create / PatientCreate
REST Read (GET)YesIHE.BasicAudit.Read / PatientRead
REST Update (PUT/PATCH)YesIHE.BasicAudit.Update / PatientUpdate
REST DeleteYesIHE.BasicAudit.Delete / PatientDeleteEntity reference includes /_history/version, see Known limitations
REST SearchYesIHE.BasicAudit.Query / PatientQuery
Bundle transactionYesPer-entry BALP profilesEach entry gets its own AuditEvent
Password changeYesNo (DICOM 110139)See Password change AuditEvent
$psql / $sqlYesNo (aidbox/sql-interaction)Custom Aidbox type system
User login/logoutYesNo (custom)Not BALP-conformant
GraphQLIndirectVia underlying FHIR callsThe GraphQL query text is not captured; only the translated FHIR operations are audited
Bulk $import / $loadOperation onlyNo (subtype $import / $load)One AuditEvent per operation, imported resources get no AuditEvents
$purge / $purge-organizationYesNo (subtype $purge / $purge-organization)
Refresh tokenYesNo (DICOM 110114 / 110122)
Access granted to a clientYesNo (AuthZ-Consent)
SDC operationsYesNo (SDC subtypes)See Filter audit events for the subtypes
Bulk $exportNo—
Auth token issuanceNo—client_credentials grant, /auth/token not audited
/auth/userinfoNo—
Configuration changesNo—
AuditEvent search, read, createExcluded—Intentional, prevents infinite audit loops

Known limitations

Delete entity reference includes version: Delete AuditEvents carry the reference to the deleted resource as ResourceType/id/_history/versionId (e.g. Observation/obs-1/_history/2). A search by entity=Observation/obs-1 in the AuditEvent table returns no delete events, and a consumer that matches events by Observation/obs-1 misses them unless it strips the version suffix.

Bulk import audits the operation only: $import and $load produce one AuditEvent per operation. The imported resources bypass the CRUD pipeline and get no AuditEvents of their own. If you need an AuditEvent for every resource, use individual FHIR CRUD operations or Bundle transactions instead.

GraphQL queries are not directly audited: GraphQL requests generate AuditEvents only for the underlying FHIR search/read operations, not for the GraphQL query itself. The original query text is not captured in any AuditEvent.

See also:

Last updated: