On this page

This document is a follow-up to [Multi-Tenant Access Control](/content/docs/access/multi-tenant-access-policy/index.html).

In an MSO Access Control model, a single practitioner often has access to multiple tenants (for example, multiple `Organization` clinics) within the same Medplum `Project`. After login, most applications need an answer to:

- Which tenant context is the user working in right now?
- When should the UI show a consolidated cross-tenant view vs. a tenant-scoped view?

This guide describes three common approaches and how to implement a tenant selector when you need one.

## At a Glance

Use the decision tree to pick the option that fits your product, then jump to that section for implementation details.

|  | Option 1: Strict Isolation | Option 2: Single Account + Selector | Option 3: Unified View |
| --- | --- | --- | --- |
| API-level tenant isolation | Strongest (one tenant per token) | Allowed tenants enforced; UI scopes within them | None beyond the project |
| Consolidated cross-tenant views | Difficult | Supported (when UI omits the filter) | Default |
| Tenant-scoped PHI screens | Yes (always) | Yes (when UI applies the filter) | No |
| Login / membership model | One membership per tenant per user | One membership covering multiple tenants | One membership covering all tenants |
| Tenant switching | Re-auth or saved-login swap | UI control (no re-auth) | N/A |
| Implementation complexity | Highest | Medium | Lowest |

## Key Concept: Allowed Tenants vs Active Tenant

- Allowed tenants: The set of tenant references the user is permitted to access (enforced by `ProjectMembership` + `AccessPolicy`).
- Active tenant: A single tenant reference that your application uses as the default filter for PHI (a UI/application concept).

When you build a "tenant selector", you're choosing and persisting the active tenant.

## Option 1: Strict Isolation (Separate Accounts / Separate Projects)

Goal: Hard isolation of PHI at the API level. At any given time, the "active tenant" is the only "allowed tenant" for the user.

### What This Looks Like

- The user has separate logins per tenant.
- Each login/profile has a `ProjectMembership` that only includes access to one tenant.

_Each login is bound to a single tenant. Switching tenants requires switching the active login._

### Switching the Active `ProjectMembership`

In Medplum, the "active membership" is not a client-side flag you can flip on an existing token. The OAuth login flow selects a membership, and the issued access token is bound to that membership.

Practically, "switching memberships" means one of:

- Switching to another existing login (you already authenticated previously and have a saved token pair).
- Starting a new login and selecting a different membership during the login handshake.

#### How Membership Selection Works in the Login Flow

- If a user only has one membership, Medplum selects it automatically.
- If a user has multiple memberships, the client displays a "choose profile" screen and posts the selected membership ID to `POST /auth/profile`.

#### How to Configure a User with Multiple Memberships

You need to invite the user once per membership you want in the project (for example one membership per tenant in a strict-isolation MSO model). See the [Invite User endpoint](/content/docs/api/project-admin/invite/index.html).

### Creating the First `ProjectMembership`

```ts
// First membership invite for this user in this project.  
await medplum.post(`admin/projects/${projectId}/invite`, {
  resourceType: 'Practitioner',
  firstName: 'George',
  lastName: 'Washington',
  email: 'dr.gw@example.gov',
  sendEmail: false,
  membership: {
    access: [
      {
        policy: { reference: 'AccessPolicy/mso-tenant-policy' },
        parameter: [{ name: 'organization', valueReference: { reference: 'Organization/clinic-a' } }],
      },
    ],
  },
});
```

### Creating an Additional `ProjectMembership`

```ts
// Second membership invite for the same user in the same project.  
await medplum.post(`admin/projects/${projectId}/invite`, {
  resourceType: 'Practitioner',
  firstName: 'George',
  lastName: 'Washington',
  email: 'dr.gw@example.gov',
  sendEmail: false,
  forceNewMembership: true,
  membership: {
    access: [
      {
        policy: { reference: 'AccessPolicy/mso-tenant-policy' },
        parameter: [{ name: 'organization', valueReference: { reference: 'Organization/clinic-b' } }],
      },
    ],
  },
});
```

## Option 2: Single Account, Multi-Tenant Access + UI-Level Tenant Context

Goal: One login (`ProjectMembership`) can access one or multiple defined tenants, but the FE application chooses an "active tenant".

### Granting Access to Multiple Tenants

If your `AccessPolicy` uses `%organization`:

```json
{
  "resourceType": "ProjectMembership",
  "access": [
    {
      "policy": { "reference": "AccessPolicy/mso-policy" },
      "parameter": [
        { "name": "organization", "valueReference": { "reference": "Organization/clinic-a" } },
        { "name": "organization", "valueReference": { "reference": "Organization/clinic-b" } }
      ]
    }
  ]
}
```

With that membership, an access policy criteria like `Patient?_compartment=%organization` can allow access to patients across both tenants.

## Option 3: Unified View (No Tenant Context)

Goal: Treat the project as the only boundary; the user always sees everything they're allowed to see.

### When to Choose It

- Your product model is truly unified (e.g., one clinical ops team across all clinics).
- Your contracts and workflows do not require tenant-scoped PHI screens.
