Migrate from HAPI to Aidbox
HAPI FHIR and Aidbox both implement the FHIR spec, but they differ in how they store data, validate resources, and handle search, subscriptions, and access control. This guide walks through the pieces of a HAPI deployment you need to account for, and the steps to move data and configuration into Aidbox.
There is no one-click migration tool. Plan the migration in stages: map your configuration first, run a trial migration against a copy of your data, verify the result, then cut over.
Key differences
- Storage: Aidbox stores each resource type as
jsonbrows in PostgreSQL rather than a normalized relational schema, and does not index every search parameter by default. See Create indexes. - Validation: Aidbox validates against loaded StructureDefinitions and is stricter than HAPI's default validator, so expect data that was valid in HAPI to need fixes. See Validate the migrated data.
- Subscriptions: Aidbox's native topic-based subscriptions replace polling and webhook subscriptions built around the FHIR
Subscriptionresource. See Migrate subscriptions. - Access control: Authorization is declarative access policies rather than Java interceptor code. See Migrate access control rules.
- Terminology: Aidbox's terminology module can serve codes from an external server, from resources loaded locally, or both at once. See Migrate terminology.
Before you start
- Match your FHIR version. Note which FHIR version your HAPI server runs (R4, R4B, or R5) and provision your Aidbox instance with the same version.
- Inventory custom content. List every custom
StructureDefinition,SearchParameter, andSubscriptionyour HAPI deployment defines. Each has a direct but not identical equivalent in Aidbox, covered below. - Check your interceptors. HAPI deployments often carry custom Java interceptors for validation, authorization, auditing, or request rewriting. List what each one does: most map to Aidbox configuration, and the rest move to an App. See Migrate interceptors.
- Have a rollback plan. Keep your HAPI instance and its database running read-only until you've verified the Aidbox instance in production.
Run the full migration against a copy of your data first. Verify resource counts, spot-check search results, and confirm your integrations work against the Aidbox instance before touching production data.
Set up your target Aidbox instance
Deploy Aidbox before moving any data, so you can load into a running, licensed instance.
Migrate custom resources
HAPI defines custom resources through StructureDefinition. Aidbox uses the same resource for this.
Migrate custom search parameters
Recreate any custom SearchParameter resources from HAPI in Aidbox before loading data, since Aidbox indexes on load and a search parameter added after the fact does not retroactively index existing resources without a reindex.
Migrate subscriptions
Rebuild your HAPI subscriptions as Aidbox topic-based subscriptions rather than porting them as standard FHIR Subscription resources. They scale better under high write volume and support destinations like Kafka, SNS, and webhooks directly.
Migrate access control rules
HAPI authorization interceptors don't carry over as code. Reimplement your authorization rules as Aidbox access policies, and reuse your existing SMART on FHIR client configuration if you have one.
Migrate interceptors
A HAPI interceptor is a hook, so there is no one place in Aidbox to move it to. Look at the job each interceptor does and move that job to the Aidbox feature built for it. Most interceptors map to configuration. The rest become an App.
| What the HAPI interceptor does | Where it goes in Aidbox |
|---|---|
AuthorizationInterceptor: allow or deny requests by user, client, or resource | Access policies and SMART on FHIR scopes |
SearchNarrowingInterceptor: limit a user to their own patients, compartment, or organization | Access policies with the Compartments API or organization-based access control. See the example below. |
ConsentInterceptor: hide resources or elements based on patient consent | Consent-based access control and label-based access control with element masking |
A custom interceptor that writes AuditEvent resources, or BalpAuditCaptureInterceptor | Built-in BALP audit logging: Aidbox publishes AuditEvent resources to the audit events topic, and any AidboxTopicDestination (webhook, Kafka, GCP Pub/Sub, and others) delivers them to your consumer. filterBy narrows the events by type, subtype, and action, and the audit-events-recorder destination stores them in the AuditEvent table. Available since 2609. |
RequestValidatingInterceptor or RepositoryValidatingInterceptor | Profile validation against the StructureDefinitions you load. Aidbox validates every write by default. |
A STORAGE_PRECOMMIT_RESOURCE_* hook that pushes changes to Kafka, a queue, or a webhook | Topic-based subscriptions with a destination such as Kafka, webhook, or GCP Pub/Sub |
LoggingInterceptor | Observability: structured logs and metrics |
RequestTenantPartitionInterceptor and other multitenancy hooks | Organization-based access control or Multibox |
| A custom operation or endpoint | Apps or AidboxQuery for SQL-backed endpoints |
| Anything else: enriching resources on write, business rules, calls to external systems | Apps |
Example: limit a patient to their own data
In HAPI, a patient-facing app often runs a SearchNarrowingInterceptor (paired with an AuthorizationInterceptor) that finds the caller's patient and restricts every request to that patient's compartment:
public class PatientNarrowingInterceptor extends SearchNarrowingInterceptor {
@Override
protected AuthorizedList buildAuthorizedList(RequestDetails theRequestDetails) {
String patientId = findCallerPatientId(theRequestDetails);
return new AuthorizedList().addCompartments("Patient/" + patientId);
}
}
In Aidbox, link the User to the patient through User.fhirUser and create one access policy:
resourceType: AccessPolicy
id: patient-own-data
engine: matcho
link:
- reference: Operation/FhirRead
- reference: Operation/FhirCompartmentSearch
matcho:
params:
resource/type: Patient
resource/id: .user.fhirUser.id
The policy lets the user read their own Patient resource and search their own compartment. For a user whose fhirUser is Patient/p1:
GET /fhir/Patient/p1 # 200
GET /fhir/Patient/p1/Observation # 200, only Observations of p1
GET /fhir/Patient/p2/Observation # 403
GET /fhir/Observation # 403
HAPI rewrites an unscoped search such as GET /Observation and adds the patient filter for you. Aidbox rejects it, so point your client at the compartment URL /fhir/Patient/<id>/<type>. If your users belong to organizations rather than patients, use organization-based access control, which scopes the whole FHIR API to the user's organization. For more patterns, see Access policies examples.
Custom logic: Aidbox Apps
Interceptors that change data on the way in, apply business rules, or call other systems move to an App. An App is your own HTTP service, written in any language. You register it in Aidbox with the endpoints it handles, and Aidbox proxies matching requests to it. The service gets the request with the parsed resource, the user, and the client, runs your code, and writes to Aidbox through the REST API.
For example, a HAPI interceptor that tags new patients and assigns an MRN on create becomes a $register operation:
PUT /fhir/App/myorg.registration
content-type: text/yaml
resourceType: App
apiVersion: 1
type: app
endpoint:
type: http-rpc
url: https://registration.myorg.internal
secret: <app-secret>
operations:
register-patient:
method: POST
path: ['fhir', 'Patient', '$register']
Clients send POST /fhir/Patient/$register with the Patient in the body. Your service adds the tag and the identifier, creates the resource with POST /fhir/Patient, and returns the result. Access policies apply to App operations like to any other endpoint, so you control who can call them.
The same pattern covers what teams build with interceptors in HAPI: an MPI lookup before create, a custom $everything, derived resources written next to the original, or a call to a billing or scheduling system. You keep the logic in your own codebase and deploy it on its own schedule, separate from Aidbox upgrades.
Migrate terminology
If your HAPI deployment points to an external terminology server, such as Health Samurai's terminology SaaS, Aidbox's terminology module can keep using it. You can also load CodeSystems, ValueSets, and ConceptMaps into Aidbox's FHIR Artifact Registry and serve them locally, or combine both: run in hybrid mode, where Aidbox serves what it has loaded locally and falls back to the external server for everything else.
Migrate the data
Export your resources from HAPI using the standard FHIR Bulk Data Export operation, $export, which HAPI implements the same way Aidbox does. Aidbox exposes the matching endpoint for reference:
Load the resulting NDJSON into Aidbox with /fhir/$import. It skips validation and runs fastest, so use it once you've already validated your data against your target profiles in a trial run.
For a smaller dataset, a transactional bundle can be the better option: it validates every resource as part of the transaction and rolls back the whole bundle if one entry fails, so you don't have to separately validate the load afterward.
Validate the migrated data
Aidbox validates resources against loaded StructureDefinitions, which can be stricter about some constraints than HAPI's default validator. Run a validation pass over your imported data and fix any resources that fail before cutting over.
HAPI's default validator is more lenient than Aidbox's. Resources that loaded and passed validation in HAPI can fail against the same profile in Aidbox. Expect to find and fix a number of these during your trial migration rather than treating any failure as a bug.
If you loaded data with /fhir/$import, which skips validation, run $batch-validate afterward to check the resources already in the database asynchronously and get an offender-indexed report of what fails and why.
Create indexes
Aidbox stores resources as jsonb and does not create a database index for every search parameter by default. A search parameter works without one, but Postgres falls back to a sequential scan, which gets slow as a table grows. Plan to create indexes for the search parameters your clients actually query before you cut over.
Aidbox can suggest an index for a given resource type and search parameter, and, once traffic hits the new instance, surface which search parameter shapes are actually hot and lack a backing index. Run your usual client queries against the migrated instance during your trial run, then check the suggestions before cutover.
Set up backups before cutover
Before you point production traffic at the new instance, put a backup schedule in place.
Other Aidbox features
Once your migration is running, these are worth a look. None of them are required for cutover.
Load Implementation Guides, manage custom FHIR packages, and version canonical resources.
Query FHIR data directly with SQL, including ViewDefinitions for BI and reporting.
Turn a SQL query into its own REST endpoint for searches beyond what SearchParameters can express.
Build and render Questionnaires with Structured Data Capture (SDC).
Monitor logs, metrics, and audit trails across your Aidbox instance.
Scope access to resources by organization hierarchy, a lighter-weight alternative to running separate tenants.
Generic FHIR client libraries and Aidbox-specific, type-safe SDKs for your language.
Browse the complete list of Aidbox capabilities.
Checklist
- Aidbox instance deployed, licensed, and set to the source FHIR version
- Custom resources recreated using StructureDefinition
- Custom search parameters recreated
- Subscriptions rebuilt as Aidbox topic-based subscriptions
- Each HAPI interceptor mapped to access policies, audit, validation, subscriptions, or an App
- Terminology resources migrated or external terminology server repointed
- Data exported from HAPI and imported into Aidbox in a trial run
- Imported data validated against your target StructureDefinitions
- Indexes created for the search parameters your clients query
- Client applications repointed to Aidbox
- Integrations retested against Aidbox
- Backup schedule configured on the new instance
- Production data migrated to Aidbox
- HAPI instance kept read-only as a fallback