Multitenancy
openFHIR Engine supports multitenancy, allowing multiple organizations or tenants to use the same openFHIR instance while maintaining complete data isolation and tenant-specific configurations.
Tenant Isolation
All FhirConnect state and mapping access is completely separated per tenant. This includes:
Operational Templates (OPTs): Each tenant maintains their own set of operational templates
FhirConnect Context Mappers: Context configurations are isolated per tenant
FhirConnect Model Mappers: Mapping definitions are tenant-specific
ConceptMaps: Terminology mappings are maintained separately for each tenant
Mapping Insights: All mapping execution history and insights are tenant-scoped
When a tenant accesses any openFHIR functionality, they only see and can modify their own data. There is no cross-tenant data visibility or interference.
Tenant-Specific Configuration
Each tenant can be configured with specific settings that control how openFHIR processes their data. These configurations are stored as JSON properties within each tenant entity.
Currently available tenant configuration options:
contained_to_separate_bundle_entry
Type: boolean
Default: true
Controls how referenced resources are handled in FHIR Bundle generation:
true (default): Referenced resources (like PractitionerRole referenced by Consent) are created as separate entries in the FHIR Bundle. This results in more Bundle entries but follows standard FHIR referencing patterns.
false: Referenced resources are embedded as contained resources within the parent resource. This results in fewer Bundle entries but uses FHIR’s contained resource mechanism.
Example:
When contained_to_separate_bundle_entry is false:
{
"resourceType": "Bundle",
"entry": [
{
"fullUrl": "urn:uuid:5b1f1b0e-0e3a-4a3e-9a0e-1f3c9d5a7b21",
"resource": {
"resourceType": "Consent",
"contained": [
{
"resourceType": "PractitionerRole",
"id": "contained-practitioner"
}
],
"provision": {
"actor": [
{
"reference": {
"reference": "#contained-practitioner"
}
}
]
}
}
}
]
}
Either way every entry is given a fullUrl and a resource id that is legal on a Bundle entry: a resource
created behind a resolve() is instantiated with an internal #<uuid> id, and the leading # is
stripped when it ends up as an entry of its own. Resources that stay nested keep their local # reference,
as #contained-practitioner above.
When contained_to_separate_bundle_entry is true:
{
"resourceType": "Bundle",
"entry": [
{
"fullUrl": "urn:uuid:5b1f1b0e-0e3a-4a3e-9a0e-1f3c9d5a7b21",
"resource": {
"resourceType": "Consent",
"provision": {
"actor": [
{
"reference": {
"reference": "PractitionerRole/separate-practitioner"
}
}
]
}
}
},
{
"fullUrl": "urn:uuid:9c2d7e4a-6b18-4c55-8f0d-2a7e4b6c9d10",
"resource": {
"resourceType": "PractitionerRole",
"id": "separate-practitioner"
}
}
]
}
cdr_opt_source
Type: object
Default: absent
Overrides, for this tenant only, the openEHR CDR that operational templates are fetched from. See Integrations for what the integration does and for the full list of settings.
The object takes the same settings as the global openfhir.cdr configuration block. Keys may be written in kebab-case (as in the YAML
configuration) or camelCase:
{
"properties": {
"cdr_opt_source": {
"enabled": true,
"base-url": "http://tenant-cdr:8080/ehrbase/rest/openehr/v1",
"auth": "BASIC",
"username": "tenant-user",
"password": "tenant-password"
}
}
}
Resolution order for each request is: the tenant’s cdr_opt_source property, then the global openfhir.cdr configuration, then disabled.
Note
An absent property means “inherit the global default” — it is not the same as being switched off. A tenant that should keep using the
engine’s own template store while the global default has the integration enabled must set the property explicitly with
"enabled": false. Conversely, a tenant can enable the integration with its own CDR while the global default leaves it off, which is
the usual way to roll the feature out one tenant at a time.
Tenant Management API
Tenants can be managed through RESTful API endpoints:
- POST /tenant
Create a new tenant with specific configuration.
Example request:
POST /tenant HTTP/1.1 Content-Type: application/json Authorization: Bearer <token> { "id": "organization-123", "properties": { "contained_to_separate_bundle_entry": false } }
- PUT /tenant/(id)
Update an existing tenant’s configuration.
Example request:
PUT /tenant/organization-123 HTTP/1.1 Content-Type: application/json Authorization: Bearer <token> { "properties": { "contained_to_separate_bundle_entry": true } }
- GET /tenant/(id)
Retrieve a specific tenant’s configuration.
Example request:
GET /tenant/organization-123 HTTP/1.1 Authorization: Bearer <token>
- GET /tenant
Retrieve all tenants (administrative operation).
Example request:
GET /tenant HTTP/1.1 Authorization: Bearer <token>
- DELETE /tenant/(id)
Delete a tenant and all associated data.
Example request:
DELETE /tenant/organization-123 HTTP/1.1 Authorization: Bearer <token>