Developer Docs

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"]
	}
}

slug is 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 role field is optional and defaults to MEMBER. Only Org Admins can set role: "LEAD" via the updateTeamMemberRole mutation.

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:

  1. The maletIds field becomes the enforcement bridge
  2. Guards can check: "Is this user's team assigned to this Malet?"
  3. 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_SERVICE TCP client since the Alerts service isn't running during test execution.