Multi-tenant FHIR

Multi-Tenant FHIR for Digital Health SaaS, Clinics, Hospital Networks, and Research Sites

Healthcare multi-tenancy rarely stops at "isolate the customers." Specialists consult across organisations, patients transfer between clinics, support staff need scoped access, and roles inside each tenant differ between doctors, nurses, and IT. Fire Arrow expresses tenant boundaries in standard FHIR resources (Organization, PractitionerRole, managingOrganization) and enforces them on every request, so the same backend serves a digital health SaaS platform, a clinic network, a hospital group, or a research consortium without parallel permission systems.

What you can build

  • Independent tenants on shared infrastructure

    Clinics, sites, or programs share one Fire Arrow Server while their data stays isolated. New tenant onboarding is a new Organization resource, not a new deployment.

  • Role differentiation per tenant

    Doctors, nurses, IT staff, support, and patients all in the same backend. PractitionerRole codes select which rules apply to whom; IT staff with no clinical-resource rules simply fall through to `Forbidden`.

  • Cross-organisational specialist access scoped to one patient

    Add the specialist to the patient's CareTeam. The CareTeam validator grants access to that patient's data, nothing else. Remove the entry to revoke access.

  • Hierarchical access through Organization.partOf

    Hospital networks, regional health authorities, SaaS vendor support staff at the platform root, and research consortium coordinators all use `role-inheritance-levels` to inherit access downward through the org tree.

  • Patient transfers and role changes as data updates

    Update one field (`managingOrganization`) and access shifts immediately. Deactivate a PractitionerRole and access is revoked on the next request. No code, no infrastructure changes, no migration.

  • One declarative permission model

    Auditors and architects review rules in one place instead of tracing scattered conditional logic. The effective access for any role is inspectable, not emergent.

  • Cross-app consistency across REST and GraphQL

    REST search, GraphQL search, GraphQL read, includes, and reverse-includes all flow through the same rule set. Adding a new API surface does not mean re-implementing tenant checks.

How it works

  1. 1
    Describe the tenant structure in FHIR

    Create one Organization for each clinic, site, or program. Use `Organization.partOf` to express hierarchies: a hospital network above its clinics, a SaaS platform above its customer organisations, a study sponsor above its sites. Each clinician has a Practitioner record plus one PractitionerRole entry per organisation they work in, tagged with what they do (doctor, nurse, IT, support). Each patient is linked to the organisation that owns their record. The tenant boundary now lives in the same data that already describes your clinical and organisational world.

  2. 2
    Deny by default

    Set the server's default policy to deny. Anything that does not match an explicit rule is rejected, so adding a new resource type, a new endpoint, or a new role can never silently leak data across tenants. Policy becomes an explicit allow-list, not a series of conditional checks scattered across the codebase.

  3. 3
    Write rules per role, per resource, per operation

    For each staff role, declare which resources they can read, search, create, or update. Doctors might get read/search/create/update on clinical resources; nurses read/search; IT staff are limited to administrative resources like Practitioner, Organization, and Device. The rule names the role; the server intersects it with the practitioner's organisation, so a doctor at clinic A cannot reach clinic B's patients even if the rule itself never mentions a tenant.

  4. 4
    Allow cross-organisation access through CareTeam

    When a specialist outside a clinic needs to see one specific patient, add them to that patient's CareTeam. The server treats the membership as an additional, narrowly scoped grant: the specialist keeps their normal access at their own organisation, and on top of that they see the one patient they were added for. Removing the CareTeam entry takes the access away. No separate sharing system to maintain.

  5. 5
    Cascade access down through the hierarchy

    Pick how many levels access should cascade downward through the org tree. A support team placed at the platform root then sees everything below; a regional authority sees its hospitals; a study sponsor sees its sites. Clinic-level staff never see siblings or parents. Inheritance only flows down.

  6. 6
    Inspect why a request was allowed or denied

    During development, ask the server for a debug trace on any request. The response then includes which rule matched, why it matched, and (for denied requests) near-miss hints like "the practitioner has no role at this organisation", "the role code does not match this rule", or "this patient has no managing organisation". Use it to verify policy intent against actual behaviour. Turn it off in production because the output exposes your full rule configuration.

  7. 7
    Operate it as data, not code

    Patients move between clinics by updating one field on the patient record. A practitioner stops working somewhere by deactivating their role entry. New tenants are new Organization records. Caches that depend on these relationships invalidate themselves when the underlying data changes. No deploy, no service restart, no separate access-management system to keep in sync.

What you get out of the box

Capability With Fire Arrow Building it yourself
Tenant isolation LegitimateInterest validator + Patient.managingOrganization. Search narrowing applies organisational scope at the database layer for both REST and GraphQL. Per-table `tenant_id` column with row-level security (RLS) or per-endpoint filter logic. Mind every search parameter, every JOIN, every endpoint.
Role differentiation within a tenant PractitionerRole code referenced via `practitioner-role-code` on each rule. Different roles map to different rule sets without parallel role tables. Custom roles table, per-endpoint role checks, and a separate authorization layer that has to know about every operation.
Cross-organisational access exception CareTeam validator. Membership scoped to one patient via Practitioner, PractitionerRole, or Organization participant entry. Additive with LegitimateInterest. Tenant-wide override (which over-grants), or custom per-patient sharing logic with its own audit story.
Organisational hierarchy `Organization.partOf` + `role-inheritance-levels` configuration. One value controls how far access cascades downward. Build hierarchy resolution by hand. Decide downward vs upward inheritance per query.
Search safety on both REST and GraphQL Server narrows REST search by appending parameters. GraphQL uses alternative search parameter maps. Same rule set drives both. Per-query filter in REST and again in GraphQL resolvers. Keep the two paths in sync as new resource types are added.
Direct read protection Compartment validators enforce on `GET /fhir/Patient/{id}` too. Tag-based isolation that bypasses on direct reads is not the model. Post-fetch verification step in every endpoint. Easy to miss when adding a new resource type.
Search side-channel protection `blocked-search-params` and `blocked-includes` reject probing queries. `_filter`, `_has`, `_text`, `_content` rejected fail-closed when blocked params are configured. Manual review of every search parameter, sort key, include directive, and reverse-chain expression per role.
Patient transfer between tenants Update one field (`managingOrganization`). Access shifts on the next request. Cache invalidation is automatic. Migrate rows across partitions or rewrite references. Coordinate with every system that referenced the patient.
New tenant onboarding Create an Organization resource and PractitionerRole entries for staff. The existing rules apply automatically. Provision a database, namespace, or deployment. Run migrations. Update DNS and infrastructure.
Cache invalidation correctness Multi-layer cache automatically invalidated when underlying resources change. Deferred until after the database transaction commits. Build the cache, build invalidation, debug the stale-read race conditions.
Debugging unexpected denials `X-Fire-Arrow-Debug` header returns the rule trace plus near-miss hints (missing PractitionerRole, role-code mismatch, patient with no managingOrganization). Trace through scattered application code and access logs to figure out which check denied the request.

Tenant boundaries belong in the domain data

Healthcare multi-tenancy is rarely "one platform, multiple customers, no cross-tenant visibility". A clinic needs different visibility for doctors, nurses, and IT staff. Specialists consult across organisations. Support teams need scoped operational access. New clinics onboard. Patients transfer.

Building all of that as a parallel permission system means custom code that grows with every product change. Modelling it instead as the FHIR resources that already describe the clinical and organisational world (Organization, PractitionerRole, the patient's managing organisation, CareTeam) keeps the boundary in one place. Authorisation rules connect those relationships to the server's checks, and the server enforces them on every request, whether it arrived over REST, GraphQL, or any other supported access path.

Row-level security and SMART scopes do not cover this on their own

Row-level security at the database is close to the data and works well for simple "this row belongs to that customer" filtering. It does not express "this practitioner has a cardiology role at clinic A but not a radiology role at clinic B", or "a CareTeam membership grants this one specific patient even though the practitioner is in another organisation". Those decisions need knowledge of the FHIR data model and of clinical relationships, which the database layer does not have.

SMART on FHIR is the standard way an application gets a token from an identity provider and presents it. Its scope syntax (for example, a scope meaning "this user can read and search Observations") does not say which organisation the practitioner belongs to, where they sit in the hierarchy, or whether a CareTeam membership grants a one-patient exception. The SMART specification itself defers those decisions to a separate layer. Fire Arrow operates at that layer and accepts standard OAuth bearer tokens, so the identity provider does not have to understand FHIR scopes for any of this to work.

Searches must be narrowed inside authorisation, not after it

A missing tenant filter on a search endpoint does not throw an error. It returns results from every tenant. With a single tenant in development this often goes undetected; in production the same unfiltered search exposes cross-tenant data silently. OWASP has ranked broken access control as the top web application security risk for several editions running, and unfiltered search is one of its most common shapes.

Fire Arrow narrows searches inside the authorisation pipeline rather than after it. For REST searches, the server adds the tenant-scoping parameters before the query runs. For GraphQL, the same rules drive the query through an equivalent path, so client code does not need to differ between the two. Reads of a single resource by id (for example, fetching one patient directly) go through the same check, not through a shorter alternate code path that could be forgotten when a new resource type is added.

Multiple scopes add up rather than override

A practitioner who works at two clinics has one role record per clinic, and the server adds the two scopes together rather than asking you to pick one. A clinician with normal access at their organisation and a CareTeam membership for a patient elsewhere keeps both scopes in the same request; neither displaces the other.

This is what makes cross-organisational specialists fit cleanly. The specialist does not need to be granted broad access into another tenant; they get a single-patient exception that simply adds to whatever they already had, and removing the exception leaves their original access untouched.

Runtime changes are data updates, not deploys

A patient transferring between clinics is one field update on the patient record. The next request that hits the server evaluates against the new clinic. A practitioner who has stopped working somewhere is one flag flipped on their role entry; their access through that role is gone on the next request. Reparenting a clinic under a different network is a single change to the org-tree link, and the server's hierarchy cache picks it up automatically.

All cache invalidation is deferred until the database transaction commits, so concurrent requests cannot read a cached value that contradicts data that was just written. From an operations point of view, none of these changes need a deploy, a migration, or a service restart.

Example deployments

  • Multi-clinic SaaS platform

    Two clinics share one Fire Arrow Server. Each has its own doctors, nurses, IT staff, and patients, none of whom can see the other clinic's data. The SaaS vendor's support team sits at the platform's root organisation, with access cascading two levels down so they can investigate issues in either clinic without parallel admin accounts.

  • Hospital network with departmental hierarchy

    A regional health authority sits over its hospitals, and each hospital sits over its departments. Authority staff see across the network; hospital staff see their hospital; department staff see their department. Cross-department referrals are recorded as CareTeam memberships, so a referred specialist gets access to that one patient, not to the receiving department as a whole.

  • Research consortium with site isolation

    Each study site is its own Organization. Site investigators see only the participants enrolled at their site. A study coordinator at the sponsor organisation sees data across every site through the same downward cascade, with no separate "sponsor view" pipeline to maintain.

  • Care coordination program with cross-organisational specialists

    A multidisciplinary diabetes team (primary care, endocrinologist, dietician, nurse educator) each work at a different organisation. Adding each of them to a patient's CareTeam gives that one team access to that one patient, without granting any of them broader access to the others' clinics.

FAQ

What if a tenant requires fully separated infrastructure?

Run a separate Fire Arrow Server deployment for that tenant. The shared-infrastructure model is for tenants that accept logical separation with enforced boundaries; fully separated infrastructure stays an option when a contract or a regulator requires it.

Can a single Practitioner work at multiple clinics?

Yes. Add one role entry per clinic to the practitioner's record. The server adds the scopes together rather than asking you to pick one, so the same person sees both clinics' data through the same login. No duplicate accounts, no proxy users, no out-of-band reconciliation between systems.

How do I onboard a new tenant?

Create the Organization for the new tenant (placing it correctly in the hierarchy if applicable), add the practitioners and their role entries, and assign their patients to the new organisation. The existing rules cover the new tenant from the next request. No schema migration, no new deployment, no parallel database.

Does the model handle support staff that need cross-tenant access?

Yes. Place the support team's role record at the platform's root organisation and configure how many levels access should cascade downward. Support sees everything below them; clinic staff never see siblings or parents.

What about IoT devices and caregiver apps?

Devices have their own scope, limited to their configuration and to the observations they submit. Caregivers using a patient app have a tighter scope than the patient's own; they see what the patient has chosen to share, no more. Both fit into the same additive model: their grants stack with whatever else applies, never overriding it.

How do I prove the rules do what they claim?

During development, ask the server for a debug trace on a request. The response includes the full rule trace, which rule matched, and (for denied requests) near-miss hints that point at the most likely cause (missing role entry, wrong role code, patient with no managing organisation). Use the trace to verify policy intent against actual behaviour. Turn it off in production because the output exposes your full rule configuration.