Teams & Sub-Groups
Group members of an Organization into named Teams โ each with their own LEAD role that acts as a scoped "mini admin". Teams enable Malet Owners to partition their staff by function (e.g. Engineering, Marketing, Kitchen) while maintaining clear access control boundaries. For fine-grained permission assignment beyond base roles, see Custom RBAC.
Architecture Overview
Teams are fully contained within the organizations subgraph. The Team entity groups members, while the TeamMembership join collection tracks which users belong to which teams and in what role.
| Component | Entity | Purpose |
|---|---|---|
| Team | Team |
Named group within an organization |
| Team Membership | TeamMembership |
Join entity linking users to teams with roles |
| Team Role | TeamRole |
LEAD (mini admin) or MEMBER |
| Malet Scoping | Team.maletIds |
Forward-thinking link to assigned Malets |
Authorization Model
Teams use a dual-check authorization pattern โ certain operations require either org-level admin permissions OR team-level LEAD status:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Org Admin (OWNER/ADMIN) โ
โ โ
โ โ
createTeam โ
deleteTeam โ
updateTeamMemberRole โ
โ โ
updateTeam โ
addTeamMember โ
removeTeamMember โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Team LEAD (scoped) โ
โ โ
โ โ createTeam โ deleteTeam โ updateTeamMemberRole โ
โ โ
updateTeam โ
addTeamMember โ
removeTeamMember โ
โ (own team only) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- Org Admin Only: Create teams, delete teams, promote members to LEAD
- Org Admin OR Team LEAD: Update team info, add/remove members within the team
Entity Schema
Team
| Field | Type | Description |
|---|---|---|
id |
ID! |
Unique identifier (nanoid) |
organizationId |
ID! |
Parent organization |
name |
String! |
Team name (2โ50 chars) |
description |
String |
Brief description |
slug |
String! |
URL-friendly slug (unique within org, auto-generated if omitted) |
memberCount |
Int! |
Number of members (auto-maintained) |
maletIds |
[String] |
Malet IDs this team is responsible for (scoping, not enforced) |
createdBy |
ID! |
User who created the team |
createdAt |
DateTime! |
Creation timestamp |
updatedAt |
DateTime! |
Last update timestamp |
Indexes:
{ organizationId: 1 }โ Fast org-level team lookups{ organizationId: 1, slug: 1 }โ Unique slug per organization
TeamMembership
| Field | Type | Description |
|---|---|---|
id |
ID! |
Unique identifier |
teamId |
ID! |
Team reference |
userId |
ID! |
User reference |
organizationId |
ID! |
Denormalized for efficient cross-team queries |
role |
TeamRole! |
LEAD or MEMBER (default: MEMBER) |
joinedAt |
DateTime! |
When the user joined the team |
Indexes:
{ teamId: 1, userId: 1 }โ Unique constraint: one membership per user per team{ organizationId: 1, userId: 1 }โ Fast "which teams is this user in?" queries
GraphQL Mutations
Create Team (Org Admin Only)
Create a new team within an Organization. Requires MANAGE_TEAMS permission.
mutation CreateTeam($input: CreateTeamInput!) {
createTeam(input: $input) {
id
name
slug
organizationId
memberCount
maletIds
}
}
Variables:
{
"input": {
"orgId": "org-abc123",
"name": "Kitchen Staff",
"description": "Handles all food preparation",
"slug": "kitchen-staff",
"maletIds": ["malet-cafe-01"]
}
}
slugis optional โ if omitted, an 8-character alphanumeric slug is auto-generated. Must be unique within the organization.
Update Team (Org Admin OR Team LEAD)
mutation UpdateTeam($input: UpdateTeamInput!) {
updateTeam(input: $input) {
id
name
description
maletIds
}
}
Variables:
{
"input": {
"teamId": "team-xyz789",
"name": "Kitchen & Bakery",
"description": "Updated scope to include bakery operations",
"maletIds": ["malet-cafe-01", "malet-bakery-02"]
}
}
Delete Team (Org Admin Only)
Deletes a team and cascades all team memberships. Requires both orgId and teamId.
mutation DeleteTeam($orgId: ID!, $teamId: ID!) {
deleteTeam(orgId: $orgId, teamId: $teamId) {
id
name
}
}
Add Team Member (Org Admin OR Team LEAD)
Add an existing Organization member to a team. The user must already be a member of the Organization โ this is validated server-side.
mutation AddTeamMember($input: AddTeamMemberInput!) {
addTeamMember(input: $input) {
id
memberCount
}
}
Variables:
{
"input": {
"teamId": "team-xyz789",
"userId": "user-staff-001",
"role": "MEMBER"
}
}
The
rolefield is optional and defaults toMEMBER. Only Org Admins can setrole: "LEAD"via theupdateTeamMemberRolemutation.
Remove Team Member (Org Admin OR Team LEAD)
mutation RemoveTeamMember($input: RemoveTeamMemberInput!) {
removeTeamMember(input: $input) {
id
memberCount
}
}
Update Team Member Role (Org Admin Only)
Promote or demote a team member between LEAD and MEMBER. Team LEADs cannot promote others โ this is an org-admin-only operation to prevent privilege escalation.
mutation UpdateTeamMemberRole($input: UpdateTeamMemberRoleInput!) {
updateTeamMemberRole(input: $input) {
id
userId
role
}
}
Variables:
{
"input": {
"orgId": "org-abc123",
"teamId": "team-xyz789",
"userId": "user-staff-001",
"role": "LEAD"
}
}
GraphQL Queries
List Organization Teams
Returns all teams within an organization, sorted alphabetically.
query OrganizationTeams($orgId: ID!) {
organizationTeams(orgId: $orgId) {
id
name
slug
description
memberCount
maletIds
createdBy
createdAt
}
}
Get Single Team
query Team($teamId: ID!) {
team(teamId: $teamId) {
id
name
description
memberCount
maletIds
}
}
List Team Members
query TeamMembers($teamId: ID!) {
teamMembers(teamId: $teamId) {
id
userId
role
joinedAt
}
}
My Teams
Get the current user's teams within an organization โ useful for rendering team badges or filtering content by team context.
query MyTeams($orgId: ID!) {
myTeams(orgId: $orgId) {
id
name
slug
memberCount
}
}
Audit Events
All team operations are logged to the Organization Audit Trail and visible via the existing organizationAuditEvents query.
| Event Type | Trigger | Metadata |
|---|---|---|
TEAM_CREATED |
Team created | teamId, name, slug |
TEAM_UPDATED |
Team name/description/maletIds changed | teamId, changes |
TEAM_DELETED |
Team deleted (cascades memberships) | teamId, name |
TEAM_MEMBER_ADDED |
Member added to team | teamId, role, targetUserId |
TEAM_MEMBER_REMOVED |
Member removed from team | teamId, targetUserId |
TEAM_MEMBER_ROLE_CHANGED |
Member role updated | teamId, oldRole, newRole, targetUserId |
Permission Matrix
The MANAGE_TEAMS permission is automatically granted to Organization OWNER and ADMIN roles via BASE_ROLE_PERMISSIONS. Members and Viewers do not have team management access.
| OrgRole | MANAGE_TEAMS |
Team-Scoped via LEAD |
|---|---|---|
OWNER |
โ | N/A (has full access) |
ADMIN |
โ | N/A (has full access) |
MEMBER |
โ | โ If promoted to LEAD on specific team |
VIEWER |
โ | โ |
Malet Scoping (Forward-Thinking)
The maletIds field on Team establishes a data-model link between teams and the Malets they manage. This is not enforced in access control today โ it's queryable metadata.
With the Custom RBAC system now live:
- The
maletIdsfield becomes the enforcement bridge - Guards can check: "Is this user's team assigned to this Malet?"
- No schema changes needed โ the field simply starts being checked
This design allows Malet Owners to assign specific teams to specific Malets now (e.g., "The Kitchen Staff team manages Cafรฉ Malet"), with cross-subgraph enforcement being the next step.
Module Structure
apps/organizations/src/team/
โโโ team.types.ts # TeamRole enum (LEAD, MEMBER)
โโโ team.entity.ts # Team entity (Typegoose + GraphQL)
โโโ team-membership.entity.ts # TeamMembership join entity
โโโ team.service.ts # CRUD + membership management
โโโ team.resolver.ts # GraphQL mutations + queries
โโโ team.loader.ts # DataLoader for batched lookups
โโโ team.service.spec.ts # 25 unit tests
โโโ team.resolver.spec.ts # 12 unit tests
โโโ dto/
โโโ create-team.input.ts # CreateTeamInput
โโโ update-team.input.ts # UpdateTeamInput
โโโ team-member.input.ts # Add/Remove/UpdateRole inputs
Error Handling
| Scenario | Error Type | Message |
|---|---|---|
| Duplicate slug within org | BadRequestException |
"A team with this slug already exists in this organization." |
| User not an org member | BadRequestException |
"User must be a member of the organization before joining a team" |
| User already on team | BadRequestException |
"User is already a member of this team" |
| Team not found | NotFoundException |
"Team not found" |
| User not on team (remove) | NotFoundException |
"User is not a member of this team" |
| Insufficient permissions | ForbiddenException |
"Permission denied: requires MANAGE_TEAMS permission or Team LEAD role" |
Frontend UI Components
The Teams and Sub-Groups functionality is implemented in the primary Management Dashboard (/orgs/[slug]/manage) within the Teams & Members tab.
TeamsPanel.svelteโ The root layout containing the list of rendered teams and the form to create new teams.TeamCard.svelteโ Contains the inner state management for expanding a single team, iterating its memberships, deleting the team, and role management.
The UI integrates fully with organizations.ts GraphQL queries.
Testing
| Suite | Tests | Coverage |
|---|---|---|
team.service.spec.ts |
25 | CRUD, membership, audit logging, error cases |
team.resolver.spec.ts |
12 | Dual-check auth: admin vs LEAD vs member |
teams.e2e-spec.ts |
13 | Full lifecycle: create โ add members โ role change โ delete cascade |
Running Tests
# Unit tests
npm run test -- apps/organizations
# E2E tests
npx jest --config apps/organizations/test/jest-e2e.json --testPathPattern=teams
Note: E2E tests use in-memory MongoDB and mock the
ALERTS_SERVICETCP client since the Alerts service isn't running during test execution.
Related
- Custom RBAC โ Admin-defined custom roles with configurable permission matrices that layer on top of team membership
- Organizations & Permissions โ Base roles, vertical roles, and the
PermissionsGuardinfrastructure - Corporate Identity (SAML & SCIM) โ Enterprise provisioning that auto-creates Teams via SCIM Group sync
- SCIM โ Provisioning Engine โ SCIM Groups map to Teams via the
externalIdfield - Organization & Malet Management โ Frontend management UI for Organizations, activity feeds, and billing
- Edit History Audit Trail โ Field-level change tracking and the cross-entity
maletActivityFeed - Entertainment & Experiences โ Entertainment Malet teams use
maletIdsscoping for venue staff organization - Professional Services & Client Portals โ Professional Malet teams manage client engagements and document vaults
- Wellness & Beauty โ Wellness Malet teams organize practitioners for service-linked booking
- Culture & Arts โ Culture Malet teams organize curators and artists for exhibitions and events
- Tech & Electronics โ Tech Malet teams manage support staff for warranty claims and repairs
- Arcade & Gaming โ Arcade Malet teams organize game studio staff for launches and key distribution
- User Identity Resolution โ
TeamCard.svelteusesprofileResolver.tsto display member handles instead of raw UUIDs - Vertical Seeding Infrastructure โ Multi-tenant test data seeding with simulated org teams and malet ownership