For AI agents: the documentation index is at /docs/aidbox/llms.txt. A Markdown version of this page is available at /docs/aidbox/tutorials/other-tutorials/migrate-from-hapi-to-aidbox.md or by requesting it with the Accept: text/markdown header.
Aidbox Docs

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 jsonb rows 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 Subscription resource. 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, and Subscription your 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 doesWhere it goes in Aidbox
AuthorizationInterceptor: allow or deny requests by user, client, or resourceAccess policies and SMART on FHIR scopes
SearchNarrowingInterceptor: limit a user to their own patients, compartment, or organizationAccess policies with the Compartments API or organization-based access control. See the example below.
ConsentInterceptor: hide resources or elements based on patient consentConsent-based access control and label-based access control with element masking
A custom interceptor that writes AuditEvent resources, or BalpAuditCaptureInterceptorBuilt-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 RepositoryValidatingInterceptorProfile 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 webhookTopic-based subscriptions with a destination such as Kafka, webhook, or GCP Pub/Sub
LoggingInterceptorObservability: structured logs and metrics
RequestTenantPartitionInterceptor and other multitenancy hooksOrganization-based access control or Multibox
A custom operation or endpointApps or AidboxQuery for SQL-backed endpoints
Anything else: enriching resources on write, business rules, calls to external systemsApps

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.

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

Last updated: