Edit Page

Multi-tenancy

RESTHeart Cloud

A user can belong to multiple teams simultaneously, each with a different role. The active team and its corresponding role are encoded in the JWT; switching teams 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 teams array alongside the scalar team field. The DefaultMembershipProvider stores team identifiers as MongoDB ObjectId values:

{
  "_id":     "alice@example.com",
  "roles":   ["user"],                                        // system ACL role (from default-role)
  "team":  { "$oid": "64a1b2c3d4e5f6a7b8c9d0e1" },        // active team ID (JWT claim)
  "teams": [
    { "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-team and stored in the teams array.

roles is always kept in sync with the role for the active team. teams 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 teams 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 team memberships

GET /auth/teams — requires authentication.

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

GET /auth/teams
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 team identifiers (the default) it is {"$oid": "…​"}. Custom providers that use string or integer team IDs will produce a plain JSON string or number.

Switch active team

POST /auth/switch-team — requires authentication.

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

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

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

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

On success (200 OK):

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

  • The Set-Cookie header carries the new JWT.

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

Error responses

Status Reason

400

Missing or blank teamId

401

Not authenticated

403

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

404

User document not found

Typical frontend flow

// 1. Load team list on app init
authService.getTeams()  // GET /auth/teams
  .subscribe(teams => this.teams.set(teams));

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

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

ACL considerations

No manual ACL entries are needed for /auth/teams or /auth/switch-team. 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.