Multi-tenancy
RESTHeart CloudA 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 |
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-Cookieheader carries the new JWT.
{ "team": {"$oid": "64a1b2c3d4e5f6a7b8c9d0e2"}, "role": "user" }
Error responses
| Status | Reason |
|---|---|
|
Missing or blank |
|
Not authenticated |
|
User does not belong to the requested team, or account not active |
|
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.