1. What This Guide Covers
EHRbase is the leading open-source openEHR Clinical Data Repository. When openEHR templates constrain coded elements to external terminologies (SNOMED CT, LOINC, ICD-10, RxNorm, and the rest of the OMOP vocabulary set), EHRbase can delegate validation to a remote FHIR terminology server. OMOPHub’s FHIR Terminology Service is one such server. This guide shows the exact configuration that makes that work and walks through one round-trip - template upload, composition commit, code validation - against a real EHRbase instance pointed at OMOPHub. It is not a general openEHR interoperability guide: the promise is narrow and concrete - template terminology validation and ValueSet expansion, nothing else.Tested, not “drop-in.” The snippets below are verified against
ehrbase/ehrbase:next and OMOPHub’s production FHIR Terminology Service. Your
EHRbase deployment may have additional security, network, or validation
configuration that changes the details - treat this as a reference pattern to
adapt, not a turnkey install script.2. Prerequisites
- An EHRbase instance -
ehrbase/ehrbase:nextor v2.x. The integration uses Spring Security OAuth2 and Spring’sRemoteTerminologyServiceValidationSupport, which have been stable since EHRbase 2.0. - An OMOPHub API key from your dashboard. Free tier works for testing.
- Network access from EHRbase to
fhir.omophub.com:443(production) or to your own local OMOPHub dev instance. - Familiarity with openEHR templates (OPT, ADL 1.4) and basic YAML configuration.
https://fhir.omophub.com/fhir/r4/. OMOPHub also serves R5 and R6, but EHRbase will not use them.
3. Configuration
EHRbase consumes two config surfaces: one Spring Security OAuth2 client registration (for outbound authentication) and one external terminology provider block (for the validator).3.1 ehrbase.yml overlay
Mount this file into your EHRbase container at /config/ehrbase.yml:
ehrbase.yml
client-idis your OMOPHub API key (starts withoh_).client-secretis required by Spring’s config shape but not validated - use any non-empty value.- OMOPHub’s token endpoint accepts both RFC 6749 client authentication methods (
client_secret_basic/client_secret_post), so you do not need to setclient-authentication-methodexplicitly - Spring’s default works. fail-on-error: truemakes EHRbase reject composition commits when terminology validation fails. Flip tofalsein development while you’re iterating on templates.- The trailing slash on
url:matters - EHRbase’s validator concatenates path segments without normalizing.
3.2 Docker Compose
A minimal stack for local testing:docker-compose.yml
3.3 Confirm the config loaded
EHRbase prints a single, unambiguous log line at startup if the provider registered correctly:oauth2-client: reference.
4. Template Terminology Binding
openEHR templates (ADL 1.4 OPT files) bind coded elements to FHIR value sets via a<referenceSetUri> element on the terminology constraint. EHRbase recognizes three authority prefixes as “route this through the configured FHIR provider”:
//fhir.hl7.org/...terminology://fhir.hl7.org/...//hl7.org/fhir/...
url= query parameter. For OMOP-supported systems the two useful forms are:
url= value is URL-encoded because <referenceSetUri> is parsed as a URI string. The decoded form of the second example is http://snomed.info/sct?fhir_vs=isa/73211009 - the implicit SNOMED ValueSet of all descendants of Diabetes mellitus (73211009).
Other FHIR system URIs OMOPHub recognizes:
| Vocabulary | FHIR system URI |
|---|---|
| SNOMED CT | http://snomed.info/sct |
| LOINC | http://loinc.org |
| RxNorm | http://www.nlm.nih.gov/research/umls/rxnorm |
| ICD-10-CM | http://hl7.org/fhir/sid/icd-10-cm |
| NDC | http://hl7.org/fhir/sid/ndc |
- ICD-10-GM (
http://hl7.org/fhir/sid/icd-10-gm) — the German Modification used for hospital billing in Germany. Typically the right choice for any EHRbase deployment billing against G-DRG. - ICD-10-CN (
http://hl7.org/fhir/sid/icd-10-cn) — the Chinese Edition maintained by CAMS, used in Chinese hospital systems.
referenceSetUri the same way, and $expand / $validate-code will dispatch to the correct OMOP vocabulary.
5. How Validation Actually Works
When EHRbase commits a composition that contains aDV_CODED_TEXT field constrained by one of these FHIR-routed referenceSetUri values, it walks a three-step flow:
OAuth2 token exchange
EHRbase’s Spring Security OAuth2 client obtains a Bearer token from
https://fhir.omophub.com/oauth2/token using client_credentials grant type. The client_id you configured (your API key) gets echoed back as the access_token.ValueSet support check
EHRbase calls
GET /fhir/r4/ValueSet?url=<implicit-vs-url> to check whether the server knows about the referenced ValueSet. OMOPHub responds with a Bundle (type searchset) containing a stub ValueSet resource if it recognizes the system URI. An empty bundle means “not supported” and EHRbase skips validation for that field.Expansion and code match
If support check passes, EHRbase calls
GET /fhir/r4/ValueSet/$expand?url=<implicit-vs-url>. OMOPHub returns a ValueSet resource with an expansion.contains[] list. EHRbase’s JsonPath validator looks for the submitted code in expansion.contains[?(@.code=='<code>')]. If it’s there, the commit proceeds; if not, the commit is rejected.Authorization header. EHRbase caches token + expansion responses in its externalFhirTerminologyCache, so repeated commits against the same value set don’t re-hit OMOPHub.
6. Example Round-Trip
6.1 Upload a template
Any ADL 1.4 OPT that includes at least one<referenceSetUri> binding will do. EHRbase’s own test fixtures provide a good starting point - see the IDCR Allergies List.v0.opt template in the EHRbase repository.
HTTP 201. The template is now registered with whatever FHIR bindings it declared.
6.2 Create an EHR
6.3 Submit a composition with an invalid code
Build a minimal composition body that includes aDV_CODED_TEXT field bound to one of the template’s FHIR-routed constraints, with a code that is definitely not in the target value set:
composition-invalid.json
HTTP 422:
$expand, scanned the returned expansion.contains[], found no code:'99999999' entry, and refused the commit.
6.4 Submit a composition with a valid code
Replacecode_string with a concept that is actually in the value set - any descendant of the constraint’s parent concept. For the SNOMED Substance (105590001) value set, that means any concept with concept_class_id = 'Substance' whose Is a ancestry includes 105590001.
defining_code.code_string to one of the codes returned, leave terminology_id.value as http://snomed.info/sct (it must match the system field in the expansion), and re-submit - expect HTTP 201.
Terminology ID must match the expansion’s
system. EHRbase compares
DV_CODED_TEXT.defining_code.terminology_id.value against the system URI
returned by $expand. If the template sets the terminology_id to the
value-set sentinel URL (as some auto-generated example compositions do),
validation will fail with "The terminology {code} must be {system}". Always
use the canonical system URI (http://snomed.info/sct, http://loinc.org,
etc.) in submitted compositions.7. Supported Operations
EHRbase uses a subset of OMOPHub’s FHIR Terminology Service during validation:| EHRbase use | OMOPHub endpoint | When it fires |
|---|---|---|
| Fetch OAuth2 token | POST /oauth2/token | First validation call, then cached |
| ValueSet support check | GET /fhir/r4/ValueSet?url=... | Before each $expand for a new VS |
| ValueSet expansion | GET /fhir/r4/ValueSet/$expand?url=... | On composition commit, per FHIR-bound field |
| CodeSystem validation (rare) | GET /fhir/r4/CodeSystem/$validate-code | Only if templates use terminology://fhir.hl7.org/CodeSystem?... bindings |
$lookup, $translate, $subsumes, $closure, $find-matches) is not reached through EHRbase’s validator - if you need those, call OMOPHub directly from your application code. See the FHIR Integration guide for the full operation surface.
8. What OMOPHub Adds vs. Generic Terminology Servers
| Capability | OMOPHub | Ontoserver | Local ATHENA + HAPI |
|---|---|---|---|
| OMOP vocabulary coverage | 130+ vocabularies | Limited | Full (manual download) |
Semantic search ($find-matches) | Yes | No | No |
| Phoebe recommendations | Yes (opt-in) | No | No |
| FHIR → OMOP concept resolver | Yes | No | No |
| R4 support | Yes | Yes | R4 only |
| Zero infrastructure | Yes (cloud API) | Self-host | PostgreSQL + HAPI server |
| SDKs (Python, R, MCP) | Yes | No | No |
9. openEHR → OMOP Pipeline
The full loop for a site that uses EHRbase as its clinical repository and OMOP for observational research:Capture
Clinician enters data in an openEHR form. Coded fields are validated against
OMOPHub’s
$expand at commit time.Resolve
Each Coding goes through OMOPHub’s FHIR
Resolver (
POST /v1/fhir/resolve) to
get the standard OMOP concept + CDM target table.10. Limitations and Caveats
Specific limitations to be aware of:- OMOPHub does not replace openEHR’s built-in terminology. It handles external clinical terminologies only - codes from SNOMED, LOINC, ICD-10, RxNorm, etc. openEHR’s own
openehr-terminology(used for things likelanguage,territory, and AOM-level constraint codes) is served by EHRbase internally and does not route through the FHIR provider. - OMOP vocabulary content is optimized for observational research. Some source codes are collapsed to a single standard concept in OMOP where a strict clinical coding system would distinguish them. If your templates require sub-concept granularity (e.g. distinguishing two variants of the same SNOMED code), OMOP’s
Maps tobehavior may treat them as equivalent. - Implicit ValueSets only. OMOPHub supports the FHIR implicit ValueSet URL patterns (
{system}?fhir_vs,{system}?fhir_vs=isa/{code}) but not user-defined named ValueSets uploaded viaPUT /ValueSet/:id. Templates that bind to custom ValueSet IDs must be rewritten to use implicit-VS bindings. - OAuth2 is a compatibility shim. OMOPHub’s
/oauth2/tokenendpoint implements just enough of RFC 6749 §2.3.1 to satisfy Spring Security:client_credentialsgrant, eitherclient_secret_basicorclient_secret_postclient authentication, no scopes enforced, no refresh tokens,expires_inis nominal. Your API key is echoed back as the access token. Don’t build your own clients against it - use the REST API directly. - Template terminology binding only. This guide covers coded-field validation at composition commit time. It does not cover AQL terminology-aware querying, archetype-level binding, or bidirectional FHIR ↔ openEHR data transformation. Those are separate problems outside the scope of a terminology server.
- EHRbase caches aggressively. The
externalFhirTerminologyCacheholds expansion results in memory - if you change OMOPHub content, restart EHRbase (or wait for the TTL) before expecting the cache to refresh.
11. Verification Checklist
Run these in order against your own EHRbase + OMOPHub setup:| # | Test | Expected result |
|---|---|---|
| 1 | Start EHRbase with OMOPHub configured | Startup log shows Initializing 'omophub' external terminology provider (type: FHIR) at https://fhir.omophub.com/fhir/r4/ |
| 2 | curl https://fhir.omophub.com/fhir/r4/metadata -H "Authorization: Bearer $KEY" | HTTP 200, FHIR CapabilityStatement |
| 3 | curl -X POST https://fhir.omophub.com/oauth2/token -d "grant_type=client_credentials&client_id=$KEY&client_secret=unused" | HTTP 200, {"access_token":"oh_...","token_type":"Bearer",...} |
| 4 | Upload an OPT template with a terminology://fhir.hl7.org/ValueSet?... binding | HTTP 201 from EHRbase’s template endpoint |
| 5 | Submit a composition with a SNOMED code in the bound ValueSet | HTTP 201 |
| 6 | Submit a composition with a SNOMED code not in the bound ValueSet | HTTP 422, error text "does not match any option from value set" |
| 7 | Check EHRbase logs for OAuth2AuthorizationException | Should not appear - if it does, verify client-id matches your OMOPHub API key |
12. Troubleshooting
OAuth2AuthorizationException: [invalid_request] client_id is required
Your OMOPHub API key is missing or the Spring placeholder didn’t resolve. Confirm OMOPHUB_CLIENT_ID is set in the container’s environment (docker exec ehrbase printenv | grep OMOPHUB) and that ehrbase.yml is mounted at the path SPRING_CONFIG_ADDITIONAL_LOCATION points to.
502 Bad Gateway: 404 Not Found from GET .../fhir/r4/ValueSet
EHRbase’s ValueSet support check is hitting the wrong base URL. Most common cause: the url: under validation.external-terminology.provider.omophub is missing the /fhir/r4/ path prefix. The correct value is https://fhir.omophub.com/fhir/r4/ (with trailing slash).
The terminology {code} must be {system}
The terminology_id.value in your submitted DV_CODED_TEXT does not match the system URI returned by $expand. Use the canonical system URI (http://snomed.info/sct, not the ValueSet sentinel URL) in composition bodies.
Failed to validate DvCodedText ... No example for terminology ... available
You’re submitting an auto-generated example composition with placeholder values. EHRbase’s template example generator produces code_string: "42" for fields bound to unknown FHIR ValueSets - always a cache-miss. Replace with real codes before testing validation.
EHRbase log shows terminology-provider init, but commits never hit OMOPHub
Your template is not actually using the FHIR binding prefixes. Check that <referenceSetUri> starts with terminology://fhir.hl7.org/ or //fhir.hl7.org/ - other URI schemes (e.g. terminology:SNOMED-CT?subset=...) are handled by EHRbase’s internal terminology and never route through the FHIR provider.
13. Next Steps
FHIR Integration
The full surface of OMOPHub’s FHIR Terminology Service -
$lookup,
$translate, $subsumes, $find-matches, batch Bundles, plus the OMOP
FHIR Resolver for ETL pipelines.HAPI FHIR
Point a HAPI FHIR server at OMOPHub as a remote terminology backend - useful
if your openEHR stack also fronts a HAPI FHIR facade for external clients.
EHR Integration
Patterns for CDS Hooks, SMART on FHIR apps, and sidecar architectures that
resolve vocabularies at the point of care.