Edit Page

Multi-tenancy

RESTHeart Cloud

A user can belong to multiple tenants simultaneously, each with a different role. The active tenant and its corresponding role are encoded in the JWT; switching tenants reissues the JWT without requiring a new login.

Note

The membership model — field names, role vocabulary, storage collection — can be customised via the MembershipProvider SPI (9.4.1+). This page describes the built-in default behaviour.

Data model

Each user document stores a tenants array alongside the scalar tenant field. The DefaultMembershipProvider stores tenant identifiers as MongoDB ObjectId values:

{
  "_id":     "alice@example.com",
  "roles":   ["user"],                                        // system ACL role (from default-role)
  "tenant":  { "$oid": "64a1b2c3d4e5f6a7b8c9d0e1" },        // active tenant ID (JWT claim)
  "tenants": [
    { "id": { "$oid": "64a1b2c3d4e5f6a7b8c9d0e1" }, "role": "owner"  },  // membership role
    { "id": { "$oid": "64a1b2c3d4e5f6a7b8c9d0e2" }, "role": "member" }
  ]
}
Note
roles contains the system ACL role (e.g. user), not the membership role. The membership role (owner/member) is per-tenant and stored in the tenants array.

roles is always kept in sync with the role for the active tenant. tenants is the authoritative list of all memberships. Custom providers can use a different identifier type — see Custom Membership Providers.

Adding a user to a second team

Inviting an existing user to a different team adds a new entry to their tenants array without requiring account reactivation (the user is already active):

POST /auth/invite
Authorization: Bearer <owner-or-admin-token>
Content-Type: application/json

{ "email": "alice@example.com", "role": "member" }
  • If the user already belongs to the caller’s team → 409 Conflict.

  • If the user is new → standard invite flow (see Team Invitations).

  • If the user is active in another team → invitation sent; membership added when the user accepts; 201 Created.

List tenant memberships

GET /auth/tenants — requires authentication.

Returns all teams the current user belongs to. The entry whose active field is true matches the tenant encoded in the current JWT.

GET /auth/tenants
Authorization: Bearer <token>
[
  { "id": {"$oid": "64a1b2c3d4e5f6a7b8c9d0e1"}, "name": "Acme Corp",  "role": "owner",  "active": true  },
  { "id": {"$oid": "64a1b2c3d4e5f6a7b8c9d0e2"}, "name": "Other Corp", "role": "member", "active": false }
]

The id field is serialized as MongoDB extended JSON, preserving the native BSON type. For ObjectId tenant identifiers (the default) it is {"$oid": "…​"}. Custom providers that use string or integer tenant IDs will produce a plain JSON string or number.

Switch active tenant

POST /auth/switch-tenant — requires authentication.

Verifies that the user belongs to the requested tenant, issues a new JWT with the correct role for that tenant, and updates the auth cookie. The browser (or client) does not need to re-enter credentials.

POST /auth/switch-tenant
Authorization: Bearer <token>
Content-Type: application/json

{ "tenantId": {"$oid": "64a1b2c3d4e5f6a7b8c9d0e2"} }

The tenantId value must be in MongoDB extended JSON format, matching the id field returned by GET /auth/tenants.

On success (200 OK):

  • The response body confirms the new active tenant and role.

  • The Set-Cookie header carries the new JWT.

{ "tenant": {"$oid": "64a1b2c3d4e5f6a7b8c9d0e2"}, "role": "user" }

Error responses

Status Reason

400

Missing or blank tenantId

401

Not authenticated

403

User does not belong to the requested tenant, or account not active

404

User document not found

Typical frontend flow

// 1. Load tenant list on app init
authService.getTenants()  // GET /auth/tenants
  .subscribe(tenants => this.tenants.set(tenants));

// 2. User picks a different team
authService.switchTenant(tenantId)  // POST /auth/switch-tenant
  .pipe(switchMap(() => authService.checkSession()))
  .subscribe(() => router.navigate(['/time']));

After switchTenant, call checkSession() (GET /token) so the app state reflects the new tenant and role.

ACL considerations

No manual ACL entries are needed for /auth/tenants or /auth/switch-tenant. Both endpoints register their own allow rules via ACLRegistry at startup. They are disabled when accountsConfig.membership-endpoints-enabled: false.

See Custom Membership Providers to replace the built-in storage strategy.