Aidbox Topic-Based Subscriptions
This functionality is available in Aidbox versions 2409 and later and requires FHIR Schema validation engine to be enabled.
Overview
This feature enables dynamic subscriptions to changes in FHIR resources, allowing users/systems to receive notifications through multiple channels.
Looking for FHIR-standard Subscriptions? If you need external systems or clients to create their own subscriptions using standard FHIR Subscription resources, see FHIR Topic-Based Subscriptions. FHIR Topic-Based Subscriptions are built on top of the mechanism described on this page. Aidbox Topic Destinations are best for server-side integrations managed by administrators.

For an application example, refer to Aidbox Subscriptions & Kafka AidboxTopicDestination
Key Components
AidboxSubscriptionTopicis a custom Aidbox resource modeled after the FHIR R6 SubscriptionTopic resource. The resource allows defining a set of events that clients can subscribe to, such as changes in specific resources.AidboxTopicDestinationis a custom Aidbox resource that defines where and how the notifications triggered by anAidboxSubscriptionTopicshould be sent. This resource offers flexibility in specifying various types of destinations. And is considered as a system configuration resource.AidboxSubscriptionStatusis a custom Aidbox resource which describes the notifications: what messages stored in the bundle, source and destination.
Currently supported channels
Send FHIR resource events to HTTP webhooks with retry logic and batch support.
Stream FHIR resource events to Google Cloud Pub/Sub with guaranteed delivery.
Stream FHIR resource events to Apache Kafka with best-effort or at-least-once delivery.
Export FHIR resources to ClickHouse with SQL-on-FHIR ViewDefinitions for real-time reporting.
Export FHIR resources to Google BigQuery with SQL-on-FHIR ViewDefinitions.
Integrate Aidbox topic-based subscriptions with NATS and NATS JetStream.
Connect Aidbox subscriptions to RabbitMQ over AMQP for real-time FHIR events.
Integrate Aidbox subscriptions with ActiveMQ using AMQP 1.0 for event streaming.
Route FHIR resource events to AWS EventBridge for serverless processing.
Stream FHIR resource events to AWS SNS for pub/sub messaging and fan-out notifications.
Export FHIR resources to Databricks Unity Catalog managed Delta tables.
AidboxSubscriptionTopic
The AidboxSubscriptionTopic resource describes the data sources for subscriptions. It allows clients to subscribe to events in Aidbox and filter them using user-defined triggers, which are specified by the trigger element. Supported properties:
| Property | Type | Description |
|---|---|---|
resource * | uri | Resource (reference to definition) for this trigger definition. It is binding to All Resource Types. |
supportedInteraction | code | create | update | delete |
fhirPathCriteria | string | FHIRPath based trigger rule. FHIRPath criteria are evaluated only within the context of the current resource. Supports %current and %previous variables for comparing resource states during updates. |
description | string | Text representation of the event trigger. |
canFilterBy | BackboneElement | Filter parameters that destinations and FHIR Subscriptions can use in filterBy. Each entry has filterParameter, filterDefinitionFhirPathExpression, comparator (only eq), and description. See Filter events with filterBy. |
* required.
PATCH requests (merge-patch, JSON Patch, and FHIRPath Patch) map to the update interaction — a topic with supportedInteraction: update fires for PATCH-originated changes too. A no-op patch (one that doesn't change the resource) does not fire the event.
Create AidboxSubscriptionTopic resource
POST /fhir/AidboxSubscriptionTopic
content-type: application/json
accept: application/json
{
"resourceType": "AidboxSubscriptionTopic",
"url": "http://example.org/FHIR/R5/SubscriptionTopic/QuestionnaireResponse-topic",
"status": "active",
"trigger": [
{
"resource": "QuestionnaireResponse",
"fhirPathCriteria": "status = 'completed' or status = 'amended'"
}
]
}
Built-in topics
Aidbox ships topics that exist without a stored AidboxSubscriptionTopic resource. Reference them by URL in AidboxTopicDestination.topic. Aidbox rejects a stored topic that reuses a built-in URL.
| Topic URL | Events |
|---|---|
http://health-samurai.io/fhir/core/StructureDefinition/AuditEventsR4BALP | Audit events as FHIR R4 AuditEvent resources. Filter parameters: type, subtype, action. Available since 2609. Since 2610, your application publishes its own events to it with $aidbox-audit. See Audit and Logging. |
AidboxTopicDestination
The AidboxTopicDestination resource is used to define channel configurations for processing subscription data.
Sensitive Kafka parameters like saslJaasConfig and sslKeystoreKey support external secrets — store credentials outside the database using vault config files.
Create a TopicDestination
To start processing subscription data, create a AidboxTopicDestination resource with a reference to the AidboxSubscriptionTopic. Examples of AidboxTopicDestination resources can be found in sub-sections.
Stop subscription data processing
To stop processing subscription data, delete the AidboxTopicDestination resource.
Updating a TopicDestination
Each AidboxTopicDestination runs a live sender: a Kafka producer, a webhook delivery worker, a GCP Pub/Sub session. Aidbox builds the sender when you create the destination and cannot rebuild it from an edited resource. For this reason PUT and PATCH on a destination return 405 Method Not Allowed, in direct requests and inside transaction and batch bundles, including init bundles. (AidboxSubscriptionTopic carries no runtime state and supports PUT as usual.)
Some destination kinds exempt specific parameters from this rule and accept a PUT that changes them alone. Each kind's page documents its exceptions, for example the webhook endpoint.
To change anything else, recreate the destination: DELETE the existing resource and POST a new one with the same id. Undelivered events are dropped together with the old destination, so drain the queue first if they matter: watch messagesQueued in $status reach zero before the DELETE.
In an init bundle, use a batch bundle for the recreate pattern, so the DELETE succeeds when the resource does not yet exist (returns 204):
{
"resourceType": "Bundle",
"type": "batch",
"entry": [
{
"request": { "method": "DELETE", "url": "AidboxTopicDestination/<id>" }
},
{
"resource": { "resourceType": "AidboxTopicDestination", "id": "<id>", "...": "..." },
"request": { "method": "POST", "url": "AidboxTopicDestination" }
}
]
}
Status ($status) and HA deployments
Each kind-specific tutorial (e.g. webhook, Kafka) describes the $status operation. One detail applies to every destination kind:
messagesDelivered,messageBatchesDelivered,messagesDeliveryAttempts,messageBatchesDeliveryAttempts,messagesInProcess,lastErrorDetail, andstartTimestampare per-replica and held in memory. They reset on Aidbox restart and are not summed across replicas.messagesQueuedis computed from the database and is consistent across replicas.
In a highly available deployment, calling $status through a load balancer returns whichever replica answered. To get cluster-wide totals, query each pod directly and sum the in-memory counters — see Counter semantics for the full breakdown.
AidboxTopicDestination Profile
Ensure that the resource metadata contains the kind-specific AidboxTopicDestination profile.
Elements
| Property | Type | Description |
|---|---|---|
status |
code | active - the only possible value for now. Expected to be expanded. |
topic * |
string | Url of AidboxSubscriptionTopic resource. |
kind * |
code |
Defines the destination for sending notifications. Supported values:
kafka-at-least-once,
kafka-best-effort,
webhook-at-least-once,
gcp-pubsub-at-least-once,
nats-at-least-once,
nats-best-effort,
amqp-at-least-once,
aws-eventbridge,
aws-sns,
clickhouse,
clickhouse-at-least-once,
bigquery-at-least-once,
data-lakehouse-at-least-once,
audit-events-recorder (audit events topic only, see Store audit events in Aidbox)
|
parameter * |
FHIR parameters | Defines the destination parameters for sending notifications. Parameters are restricted by profiles for each destination. |
content |
Subscription payload content |
full-resource | id-only | emptyfull-resource is the default value.
|
filterBy |
BackboneElement | Filters that narrow the events the destination receives. Each entry has filterParameter, value, and optional comparator and resource. You cannot change filterBy after creation. See Filter events with filterBy (supported since the 2609 release). |
includeEntryAction |
boolean | When true, each Bundle.entry includes the bundle-entryActionCode extension indicating the CRUD action (create | update | delete) that triggered the notification. Default: false. |
includeVersionId |
boolean | When true, each Bundle.entry includes the bundle-entryVersionId extension containing the resource's meta.versionId at the time of the notification. Default: false. |
includeVersionIdInFocusReference |
boolean | When true, the focus.reference in each notificationEvent of the AidboxSubscriptionStatus includes the version history path (e.g. Patient/123/_history/5 instead of Patient/123). Default: false. |
enableLogging |
boolean | When true, AidboxSubscriptionStatus is logged after each delivery attempt. On failure, the logged status includes an error array. Useful for debugging and monitoring topic destination delivery (supported since the 2604 release). Default: false. |
logLevel |
code | The log level used when enableLogging is true. Supported values: error | warn | info | debug | trace. Must be equal to or higher than the system-wide log level setting, otherwise messages will be filtered out (supported since the 2604 release). Default: info. |
* required.
Filter events with filterBy
filterBy on AidboxTopicDestination is available starting from version 2609.
A destination receives every event of its topic unless you add filterBy. The topic declares the parameters you can filter by in trigger.canFilterBy. Each parameter has a FHIRPath expression that extracts values from the resource of the event.
Define a filter parameter on the topic:
POST /fhir/AidboxSubscriptionTopic
content-type: application/json
accept: application/json
{
"resourceType": "AidboxSubscriptionTopic",
"url": "http://example.org/SubscriptionTopic/patient-topic",
"status": "active",
"trigger": [
{
"resource": "Patient",
"canFilterBy": [
{
"filterParameter": "gender",
"filterDefinitionFhirPathExpression": "gender",
"comparator": ["eq"],
"description": "Patient gender"
}
]
}
]
}
Filter by it in a destination:
POST /fhir/AidboxTopicDestination
content-type: application/json
accept: application/json
{
"resourceType": "AidboxTopicDestination",
"id": "female-patients",
"meta": {
"profile": [
"http://health-samurai.io/fhir/core/StructureDefinition/aidboxtopicdestination-webhookAtLeastOnceProfile"
]
},
"kind": "webhook-at-least-once",
"topic": "http://example.org/SubscriptionTopic/patient-topic",
"filterBy": [
{
"filterParameter": "gender",
"comparator": "eq",
"value": "female"
}
],
"parameter": [
{
"name": "endpoint",
"valueUrl": "https://example.com/webhook"
}
]
}
The destination receives events of female patients only. With "comparator": "ne", it receives events of every patient except female ones, including patients without gender.
Aidbox matches filters by these rules:
- Aidbox compares
valuewith the values thatfilterDefinitionFhirPathExpressionreturns. References compare by theirreferencestring, booleans and numbers by their string form. Other complex values, such as aCoding, match nothing, so point the expression at a primitive, for examplecode.coding.code. comparatoracceptseqandne. Withoutcomparator, Aidbox useseq. Aidbox rejects other comparators with422.eqmatches when any value of the event equalsvalue.nematches when no value of the event equalsvalue.- Comma-separated values in one filter match
eqwhen any of them matches, andnewhen none of them matches:"value": "male,female". - Several filters match when all of them match.
resourcelimits a filter to the trigger of that resource, for topics with several triggers.- An event with no value for the filter parameter does not match
eqand matchesne. A filter on a parameter that the topic does not define matches no event witheqand every event withne. - You cannot change
filterByafter creation:PUTreturns405. Delete the destination and create it again.
filterBy works with every destination kind. For the fhir-native-topic-based-subscription kind, Aidbox applies the destination filters first and the filterBy of each FHIR Subscription after them. Built-in topics define their own filter parameters, see Filter audit events.
Organization-based hierarchical filtering
Organization-based hierarchical filtering is available starting from version 2509.
Both AidboxSubscriptionTopic and AidboxTopicDestination support organization-based hierarchical access control.
How it works
The filtering mechanism uses organization extensions and works as consecutive filters:
-
Use organization extension:
In organizational-based access control, both events (resources) and subscription resources (
AidboxSubscriptionTopicandAidboxTopicDestination) are automatically marked with organization extensions when created through organization-specific APIs (/Organization/<org-id>/fhir/...). -
Consecutive filtering: The filters work in sequence - first the topic filter is applied, then the destination filter.
-
Resources without organization extension: If topic/destination resources are created without organization extension, they completely ignore organization information and work as before. All events will be caught by the topic if triggered (regardless of whether the event has organization extension or not).
-
Resources with organization extension:
- Events without organization extension: Will always be skipped
- Events with organization extension:
- ✅ Caught: If the event organization is under or equal to the topic/destination organization in the hierarchy
- ❌ Skipped: If the event organization is above or unrelated to the topic/destination organization
Examples
Example 1: Organization hierarchy with event filtering
Consider an organization structure where Organization B is part of Organization A. The diagram below shows how events flow through the subscription system with hierarchical filtering:
Filtering behavior:
- ✅ Event from Org A: Processed by the topic (same organization) but filtered out at the aidbox topic destination level
- ✅ Event from Org B: Processed by the topic (child organization) and forwarded to destination at the aidbox topic destination level
- ❌ Event from unrelated organization: Would be filtered out at the subscription topic level
Example 2: Filtering at the destination level only
Consider an organization structure which two organizations Organization A and Organization B. The diagram below shows how events flow through the subscription system when filtering is applied at the destination level only:
Filtering behavior:
- ✅ Event from Org A: Processed by the topic (no filtering) and forwarded to aidbox topic destination
- ✅ Event from Org B: Processed by the topic (no filtering) but filtered out at the aidbox topic destination level
- ❌ Event from unrelated organization: Would be filtered out at the aidbox topic destination level
Notification Shape
Notification is a FHIR Bundle resource with history type, containing relevant resources in its entries. The first entry is a AidboxSubscriptionStatus resource, which describes the payload.
The other entries depend on AidboxTopicDestination.content element. This element has the binding to the FHIR subscription-payload-content value set: full-resource | id-only | empty
{
"resourceType":"Bundle",
"type":"history",
"timestamp":"2024-10-03T10:07:55Z",
"entry":[
{
"resource":{
"resourceType":"AidboxSubscriptionStatus",
"status":"active",
"type":"event-notification",
"notificationEvent":[
{
"eventNumber":1,
"focus":{
"reference":"QuestionnaireResponse/458e771c-0ff1-4858-ac07-93b7a10c8e3b"
}
}
],
"topic":"http://example.org/FHIR/R5/SubscriptionTopic/QuestionnaireResponse-topic",
"topic-destination":{
"reference":"AidboxTopicDestination/kafka-destination"
}
}
},
{
"resource":{
"id":"458e771c-0ff1-4858-ac07-93b7a10c8e3b",
"item":[
{
"text":"Leishmania sp 14kD IgG Ser Ql IB",
"answer":[
{
"valueString":"123"
}
],
"linkId":"128852"
},
{
"text":"Leishmania sp 16kD IgG Ser Ql IB",
"answer":[
{
"valueString":"432"
}
],
"linkId":"128851"
}
],
"meta":{
"lastUpdated":"2024-10-03T10:07:55.843374Z",
"versionId":"1970",
"extension":[
{
"url":"ex:createdAt",
"valueInstant":"2024-10-03T10:07:42.342731Z"
}
]
},
"author":{
"identifier":{
"type":{
"coding":[
{
"code":"UID",
"system":"urn:system:aidbox",
"display":"User ID"
}
]
},
"value":"admin",
"system":"http://localhost:8765"
}
},
"status":"completed",
"authored":"2024-10-03T10:07:55.664Z",
"resourceType":"QuestionnaireResponse",
"questionnaire":"http://loinc.org/q/100109-8"
},
"fullUrl": "http://aidbox-server/fhir/QuestionnaireResponse/458e771c-0ff1-4858-ac07-93b7a10c8e3b",
"request":{
"method":"POST",
"url":"/fhir/QuestionnaireResponse"
}
}
]
}
{
"resourceType":"Bundle",
"type":"history",
"timestamp":"2024-10-03T10:07:55Z",
"entry":[
{
"resource":{
"resourceType":"AidboxSubscriptionStatus",
"status":"active",
"type":"event-notification",
"notificationEvent":[
{
"eventNumber":1,
"focus":{
"reference":"QuestionnaireResponse/458e771c-0ff1-4858-ac07-93b7a10c8e3b"
}
}
],
"topic":"http://example.org/FHIR/R5/SubscriptionTopic/QuestionnaireResponse-topic",
"topic-destination":{
"reference":"AidboxTopicDestination/kafka-destination"
}
}
},
{
"fullUrl": "http://aidbox-server/fhir/QuestionnaireResponse/458e771c-0ff1-4858-ac07-93b7a10c8e3b",
"request":{
"method":"POST",
"url":"/fhir/QuestionnaireResponse"
}
}
]
}
{
"resourceType":"Bundle",
"type":"history",
"timestamp":"2024-10-03T10:07:55Z",
"entry":[
{
"resource":{
"resourceType":"AidboxSubscriptionStatus",
"status":"active",
"type":"event-notification",
"notificationEvent":[
{
"eventNumber":1,
"focus":{
"reference":"QuestionnaireResponse/458e771c-0ff1-4858-ac07-93b7a10c8e3b"
}
}
],
"topic":"http://example.org/FHIR/R5/SubscriptionTopic/QuestionnaireResponse-topic",
"topic-destination":{
"reference":"AidboxTopicDestination/kafka-destination"
}
}
}
]
}
Entry extensions
When includeEntryAction or includeVersionId is enabled on the AidboxTopicDestination, each resource Bundle.entry (except the AidboxSubscriptionStatus entry) will include an extension array with the corresponding values:
{
"resource": { ... },
"fullUrl": "...",
"request": { ... },
"extension": [
{
"url": "http://health-samurai.io/fhir/core/StructureDefinition/bundle-entryActionCode",
"valueCode": "create"
},
{
"url": "http://health-samurai.io/fhir/core/StructureDefinition/bundle-entryVersionId",
"valueId": "1970"
}
]
}
Only the extensions corresponding to the enabled flags are included. The extensions appear on entries for both full-resource and id-only content modes. In empty mode there are no resource entries, so extensions are not included.
Focus reference with version id
When includeVersionIdInFocusReference is enabled, the focus.reference in each notificationEvent includes the version history path:
{
"notificationEvent": [
{
"eventNumber": 1,
"focus": {
"reference": "Patient/123/_history/5"
}
}
]
}
Without this flag, the reference would be Patient/123.
Correlation id
Send a correlation id on the request that creates, updates, or deletes a resource, and Aidbox copies it into the resulting notification. Use the module.topics.correlation-id-header setting to name the request header Aidbox reads (default: x-topic-correlation-id). Aidbox treats the value as an opaque string: it does not parse, validate, or generate one on your behalf.
{
"notificationEvent": [
{
"eventNumber": 1,
"focus": {
"reference": "Patient/123"
},
"correlationId": "order-4711"
}
]
}
If the request carries no correlation id header, Aidbox omits correlationId from the notification.
A single header covers every event the request produces. On a transaction bundle or a conditional delete that matches several resources, every resulting notificationEvent carries the same correlation id. The value survives redelivery: Aidbox persists it with the event before the first delivery attempt, so retries and catch-up deliveries after downtime carry the same id as the original.
All AidboxTopicDestination channels support this: webhook, Kafka, GCP Pub/Sub, and both NATS destination kinds. On NATS, Aidbox also sets the correlation id as a native NATS message header, alongside the copy in notificationEvent. See Message headers for details.
There is no per-AidboxTopicDestination toggle for this feature: whether a notification carries a correlation id depends only on the triggering request.
x-topic-correlation-id is unrelated to the X-Correlation-Id header Aidbox reads for request logging (see Extending Aidbox logs). Set module.topics.correlation-id-header if you want the two to share a value.
The native FHIR Subscription sender does not emit correlationId. See FHIR Topic-Based Subscriptions.
Delivery logging
When enableLogging is set to true on an AidboxTopicDestination, Aidbox logs the AidboxSubscriptionStatus resource after each delivery attempt. On successful delivery, the status is logged as-is. On failure, the status includes an error array.
The logLevel element controls the log level used for these messages. For example, setting logLevel to "error" emits logs at the error level, while the default "info" emits at the info level. The configured level must be equal to or higher than the system-wide log level setting — otherwise the messages will be filtered out and will not appear in the logs.
{
"resourceType": "AidboxSubscriptionStatus",
"id": "auto-generated-uuid",
"status": "active",
"type": "event-notification",
"topic": "http://example.org/FHIR/R5/SubscriptionTopic/patient-topic",
"topic-destination": {
"reference": "AidboxTopicDestination/my-destination"
},
"error": ["Connection refused: localhost:1"]
}
The logs are emitted with the event key topic-destination/status.