Skip to main content

Multi-Tenant MSO with Medplum

· 7 min read
Finn Bergquist
Forward Deployed Engineer, Medplum

In the Medplum community of implementors, a common use case is to building an application that serves multiple clinics in the form of a Managed Service Organization (MSO). An MSO is a separate business entity that provides non-clinical services—e.g., revenue-cycle management, HR, IT, compliance, facilities, and purchasing—to physician groups or other provider organizations.

In this post, we'll focus on some key features of an MSO application using Medplum: multi-tenancy, access control, patient consent management, and project-scoped users. The application also supports FHIR, C-CDA and HL7v2 interfacing making it well suited to interface with record-keeping solutions across multiple practices.

If you haven't checked out the MSO Demo App yet, you can view the code here or watch our demo video below. The demo app is an example implementation for how to build the enrollment workflows and user management console for clinicians and patients across different clinics of an MSO provider network.

Understanding the MSO Architectural Needs

At its core, an MSO usually needs to handle the following requirements:

  • Multiple clinics (tenants) sharing the same platform
  • Enrollment workflows that allow Practitioners and Patients to be added and removed from one or more clinics
  • Customizable access control which Users have access to which resources. This can built with varying levels of sophistication depending on how restrictive the MSO wants to be.
  • Provider directory to group and display clinicians from across the MSO.

Core Implementation

Tenant Management with Organizations

To achieve multi-tenancy with Medplum while allowing for some level of coordination between tenants, the recommended pattern is to represent each healthcare clinic as an Organization resource, all within a single Medplum Project.

const clinic = await medplum.createResource<Organization>({
resourceType: 'Organization',
name: 'Kings Landing Health Center',
active: true
});

Access Control

Organizationally Contained Access Restrictions

info

To see reference implementation of each enrollment operation, see the MSO Demo App Enrollment Operations.

The most basic MSO access control model is to restrict access to resources that share a common Organization assignment.

Imposing these restrictions on Practitioner users is done via Medplum's AccessPolicies, which are applied to each Practitioner user's ProjectMembership.

Enrollment of a Practitioner into an Organization is done by adding a Organization reference to the Practitioner ProjectMembership's access field. Here is an example for a Practitioner enrolled in one Organization:

{
"resourceType": "ProjectMembership",
//...
"access": [
{
"parameter": [
{
"name": "organization",
"valueReference": {
"reference": "Organization/0195b4a4-0ed7-71ed-80cf-c6fff1e31152",
"display": "Kings Landing Health Center"
}
}
],
"policy": {
"reference": "AccessPolicy/0195b4a3-374e-75cf-a6f0-0bcee7c754c5"
}
}
]
}

Enrollment of a Patient into an Organization uses Compartments, which provide a way to tag Patient resources with the Organization references that it belongs to. So for example, a Patient enrolled in two Organizations might look like this:

{
"resourceType": "Patient",
//...
"meta": {
"project": "019571c0-035b-72fe-a5fa-75d13a09589c",
"compartment": [
{
"reference": "Organization/019571c0-035b-72fe-a5fa-75d13a09589c",
"display": "King's Landing Center for Medicine"
},
{
"reference": "Organization/0195b4a3-e637-77e2-ab0c-7ec36b68932d",
"display": "Winterfell Pediatrics Center"
},
]
}
}

Because of the Patient's compartment definition, which is essentially a way to link related resource types to the Patient resource, we can set the compartment array on the Patient resource and have that same compartment propogate to all the other resources related to the Patient. These are resources like Appointments, Observations, and DiagnosticReports that should all inherit the same access restrictions. This is done using the Patient $set-accounts FHIR operation.

For example, say you want to enroll Patient/123 into two Organizations, Organization/789 and Organization/456. You can do this by making the following request:

const response = await medplum.post('/fhir/Patient/123/$set-accounts', {
"resourceType": "Parameters",
"parameter": [
{
"name": "accounts",
"valueReference": {
"reference": "Organization/789"
}
},
{
"name": "accounts",
"valueReference": {
"reference": "Organization/456"
}
}
]
});

Then, the AccessPolicy can be configured to restrict access to all of the resources based on the Organization references in each resource's compartment. Here is what the Practitioner AccessPolicy might look like:

info

The %organization value is replaced at runtime with one or more of the Organization references from the Practitioner's ProjectMembership.access array

{
"resourceType": "AccessPolicy",
"name": "Managed Service Organization Access Policy",
"compartment": {
"reference": "%organization"
},
"resource": [
{
"resourceType": "Patient",
"criteria": "Patient?_compartment=%organization"
},
{
"resourceType": "Observation",
"criteria": "Observation?_compartment=%organization"
},
{
"resourceType": "DiagnosticReport",
"criteria": "DiagnosticReport?_compartment=%organization"
},
{
"resourceType": "Encounter",
"criteria": "Encounter?_compartment=%organization"
},
{
"resourceType": "Communication",
"criteria": "Communication?_compartment=%organization"
},
//...
]
}

What if we want to do everything discussed above but also restrict any access until the Patient has given consent?

To do this, we can create a universal Consent resource with its status set to active that can also be added to the Patient's compartment and extended to all resources related to the Patient using the $set-accounts operation. Then to revoke Patient consent, you can simply remove the Consent resource from the Patient's compartment.

Then, your access policy will look like this:

{
"resourceType": "AccessPolicy",
"name": "Managed Service Organization Access Policy with Patient Consent",
"compartment": {
"reference": "%organization"
},
"resource": [
{
"resourceType": "Patient",
"criteria": "Patient?_compartment=%organization&_compartment=Consent/<your-universal-consent-id>"
},
{
"resourceType": "Observation",
"criteria": "Observation?_compartment=%organization&_compartment=Consent/<your-universal-consent-id>"
},
{
"resourceType": "DiagnosticReport",
"criteria": "DiagnosticReport?_compartment=%organization&_compartment=Consent/<your-universal-consent-id>"
},
{
"resourceType": "Encounter",
"criteria": "Encounter?_compartment=%organization&_compartment=Consent/<your-universal-consent-id>"
},
{
"resourceType": "Communication",
"criteria": "Communication?_compartment=%organization&_compartment=Consent/<your-universal-consent-id>"
},
//...
]
}

Additional Access Control: Assigning Practitioner Access to Specific Patients

If allowing Practitioners to access all Patients enrolled in a shared Organization is not restrictive enough, you can also configure the AccessPolicy to only allow access to specifically assigned Patients. This can be done using a similar pattern to the Organizational access by not just adding Organization references to the Patient's compartment, but also adding Practitioner references to the Patient's compartment that represent the Practitioners that are allowed to access the Patient. Again, this is done using the $set-accounts FHIR operation.

For example, say you want to give Practitioner/456 in Organization/789 access to Patient/123. You can do this by making the following request:

const response = await medplum.post('/fhir/Patient/123/$set-accounts', {
"resourceType": "Parameters",
"parameter": [
{
"name": "accounts",
"valueReference": {
"reference": "Organization/456"
}
},
{
"name": "accounts",
"valueReference": {
"reference": "Practitioner/789"
}
}
]
});

along with this AccessPolicy:

note

The %profile value does not need to be explicitly added to the Practitioner's ProjectMembership like the Organization references are. %profile is a special variable that, in this case, is replaced with a reference to the Practitioner.

{
"resourceType": "AccessPolicy",
"name": "Managed Service Organization Access Policy with Patient Consent",
"compartment": {
"reference": "%organization"
},
"resource": [
{
"resourceType": "Patient",
"criteria": "Patient?_compartment=%organization&_compartment=%profile"
},
{
"resourceType": "Observation",
"criteria": "Observation?_compartment=%organization&_compartment=%profile"
},
{
"resourceType": "DiagnosticReport",
"criteria": "DiagnosticReport?_compartment=%organization&_compartment=%profile"
},
{
"resourceType": "Encounter",
"criteria": "Encounter?_compartment=%organization&_compartment=%profile"
},
{
"resourceType": "Communication",
"criteria": "Communication?_compartment=%organization&_compartment=%profile"
},
//...
]
}

User Management Strategy

Project vs Server Scoped Users

When building an MSO, it is recommended to follow the best practices for project-scoped and server-scoped users:

  • Project-scoped users: Ideal for clinicians and patients who primarily interact with a single production project
  • Server-scoped users: Best for administrators and developers who need access across multiple projects

As you can see in the MSO Demo App, we use project-scoped users for clinicians that are actually enrolled across different Organizations and server-scoped users for the administrators and developers who are provisioning clinician and patient access with the different enrollment operations.

For reference, here is the code that the MSO Demo App uses to invite clinicians as project-scoped users.