Team Management

Teams Logic

Multi-tenant team management system.

Nuda Kit includes a complete multi-tenant team system that allows users to create teams, invite members, and collaborate with different permission levels.

Overview

The team system provides:

Multi-Team Support

Users can belong to multiple teams and switch between them.

Role-Based Access

Five permission levels from Owner to Viewer.

Default Team

Each user has a default team that loads automatically.

Team Avatars

Upload and manage team profile images.

Data Model

Teams Table

ColumnTypeDescription
idIntegerPrimary key
nameStringTeam name
avatarStringAvatar file path (nullable)
created_atTimestampCreation date
updated_atTimestampLast update

Team Members Table (Pivot)

ColumnTypeDescription
idIntegerPrimary key
user_idIntegerForeign key to users
team_idIntegerForeign key to teams
roleEnumMember's role in the team
defaultBooleanIs this the user's default team?
created_atTimestampWhen user joined
updated_atTimestampLast update

Relationships

┌─────────────┐         ┌──────────────────┐         ┌─────────────┐
│    User     │────────▶│   TeamMember     │◀────────│    Team     │
├─────────────┤   1:N   ├──────────────────┤   N:1   ├─────────────┤
│ id          │         │ id               │         │ id          │
│ email       │         │ user_id (FK)     │         │ name        │
│ firstName   │         │ team_id (FK)     │         │ avatar      │
│ ...         │         │ role             │         │ ...         │
└─────────────┘         │ default          │         └─────────────┘
                        └──────────────────┘

User Roles

Teams use a hierarchical role system:

RoleLevelDescription
Owner1Full control, can delete team
Super Admin2Can manage all members except owner
Admin3Can manage lower-level members
Editor4Can edit team content
Viewer5Read-only access

Role Hierarchy

Higher roles can manage lower roles:

  • Owner → Can manage everyone
  • Super Admin → Can manage Admins, Editors, Viewers
  • Admin → Can manage Editors, Viewers
  • Editor → Cannot manage members
  • Viewer → Cannot manage members

API Endpoints

MethodEndpointDescription
GET/teamsList user's teams
POST/teamsCreate a new team
GET/teams/:idGet team details
PUT/teams/:idUpdate team
DELETE/teams/:idDelete team (owner only)
GET/teams/:id/membersList team members
DELETE/teams/:id/members/:userIdRemove member
PUT/teams/:id/members/:userId/rolesUpdate member role
PUT/teams/:id/avatarUpload team avatar
DELETE/teams/:id/avatarRemove team avatar

Team Creation

When a user creates a team:

  1. A new Team record is created
  2. The user is added as a TeamMember with owner role
  3. The team is returned with the user's membership
// Backend: CreateTeamController
const team = await db.transaction(async (trx) => {
  const newTeam = await Team.create({ name: payload.name }, { client: trx })

  await user.related('teams').attach({
    [newTeam.id]: { role: TeamMemberRole.OWNER }
  }, trx)

  return newTeam
})

Default Team

Each user has one default team:

  • Set automatically when joining first team
  • Used as the active team on login
  • Stored in team_members.default column
  • Persisted in browser cookie for quick switching

Frontend Team Store

The frontend uses Pinia to manage team state:

const useTeamStore = defineStore('team', () => {
  const teams = ref<Team[]>([])
  const currentTeamId = useCookie<number | null>('nuda-current-team-id')

  const getCurrentTeam = () => {
    return teams.value.find(team => team.id === currentTeamId.value)
  }

  const setCurrentTeamId = (newCurrentTeamId: number) => {
    currentTeamId.value = newCurrentTeamId
  }

  // ...
})

Team Switching

Users can switch between teams they belong to:

  1. Frontend updates the currentTeamId cookie
  2. All subsequent API calls use the new team context
  3. UI updates to show the selected team's data

Team Deletion

When a team is deleted:

  1. Only the owner can delete a team
  2. All TeamMember records are cascade deleted
  3. All pending Invitation records are cascade deleted
  4. Team avatar is removed from storage
  5. The Team record is deleted

Authorization

Team actions are protected by policies:

ActionRequired Role
View teamAny member
Update teamOwner, Super Admin, Admin
Delete teamOwner only
Manage membersOwner, Super Admin, Admin
Update member roleOwner, Super Admin, Admin*
Remove memberOwner, Super Admin, Admin*

*Cannot manage members with equal or higher role


Frontend Integration

Getting Teams

// composables/queries/useTeamQuery.ts
const { data: teams } = useTeamQuery().getAllTeams

Creating a Team

const { mutate: createTeam } = useTeamMutation().createTeam

createTeam({ name: 'My New Team' })

Switching Teams

const teamStore = useTeamStore()

teamStore.setCurrentTeamId(teamId)

Getting Current Team

const teamStore = useTeamStore()

const currentTeam = teamStore.getCurrentTeam()

Customization

Adding Team Fields

  1. Create a migration to add columns to teams table
  2. Update the Team model with new columns
  3. Update validators if needed
  4. Update frontend types

Changing Roles

Update the TeamMemberRole enum in app/models/team_member.ts:

export enum TeamMemberRole {
  OWNER = 'owner',
  SUPER_ADMIN = 'super-admin',
  ADMIN = 'admin',
  EDITOR = 'editor',
  VIEWER = 'viewer',
  // Add new roles here
}
Remember to update the database enum in a migration if adding new roles.

Resources