Users can belong to multiple teams and seamlessly switch between them. The current team context is persisted across sessions using cookies.
The team switching system provides:
Instant Switching
Persistent Selection
Default Team
Sidebar UI
Team state is managed by a Pinia store in stores/team.ts:
| Property | Type | Description |
|---|---|---|
teams | Team[] | All teams the user belongs to |
currentTeamId | number | null | Currently selected team ID (cookie) |
| Method | Description |
|---|---|
setCurrentTeamId(id) | Switch to a different team |
getCurrentTeam() | Get the currently selected team |
getCurrentTeamId() | Get the current team ID |
getTeams() | Get all user's teams |
getTeamById(id) | Get a specific team by ID |
setCurrentTeamIdToDefault() | Reset to the default team |
reset() | Clear all team state |
User logs in
│
▼
┌─────────────────────┐
│ Fetch all teams │ ◀── GET /teams
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Check cookie value │ ◀── 'nuda-current-team-id'
└──────────┬──────────┘
│
┌─────┴─────┐
▼ ▼
┌─────────┐ ┌─────────────┐
│ Cookie │ │ No cookie │
│ exists │ │ or invalid │
└────┬────┘ └──────┬──────┘
│ │
▼ ▼
┌─────────┐ ┌─────────────┐
│ Validate│ │ Use default │
│ team ID │ │ team │
└────┬────┘ └──────┬──────┘
│ │
└──────┬──────┘
▼
┌─────────────────────┐
│ Set currentTeamId │
└─────────────────────┘
// When user clicks a team in the switcher
teamStore.setCurrentTeamId(team.id)
// The cookie is automatically updated
// UI reactively updates to show new team context
The current team ID is stored in a cookie:
const currentTeamId = useCookie<number | null>('nuda-current-team-id')
On each page load, the store validates the cookie value:
watch(() => getAllTeams.data.value, (newTeams) => {
if (currentTeamId.value && teams.value.length > 0) {
// Validate that the current team ID exists
const teamExists = teams.value.some(team => team.id === currentTeamId.value)
if (!teamExists) {
// Reset to default team if invalid
const defaultTeam = teams.value.find(team => team.default)
if (defaultTeam) {
currentTeamId.value = defaultTeam.id
}
}
}
})
This handles cases where:
The TeamSwitcher.vue component provides the UI for switching teams:
components/app/TeamSwitcher.vue
The component is already included in the app sidebar:
<template>
<Sidebar>
<SidebarHeader>
<TeamSwitcher />
</SidebarHeader>
<!-- ... -->
</Sidebar>
</template>
The Team type includes role and default status:
export type Team = {
id: number
name: string
default: boolean
role: TeamRole
avatarUrl: string | null
createdAt: string
updatedAt: string
}
The TeamRole class provides utilities for role comparison:
const team = teamStore.getCurrentTeam()
// Check specific roles
if (team.role.equalsRole(TeamRoleValue.OWNER, TeamRoleValue.SUPER_ADMIN)) {
// User is owner or super admin
}
// Compare role levels
if (team.role.isHigherThan(TeamRole.admin)) {
// User has higher permissions than admin
}
// Display name
team.role.toDisplayName() // "Super Admin"
const teamStore = useTeamStore()
const currentTeam = computed(() => teamStore.getCurrentTeam())
const teamStore = useTeamStore()
// Switch to a specific team
teamStore.setCurrentTeamId(teamId)
// Reset to default team
teamStore.setCurrentTeamIdToDefault()
const teamStore = useTeamStore()
const hasTeamSelected = computed(() => !!teamStore.getCurrentTeamId())
<template>
<div v-if="isAdmin">
<Button>Admin Action</Button>
</div>
</template>
<script setup lang="ts">
const teamStore = useTeamStore()
const isAdmin = computed(() => {
const team = teamStore.getCurrentTeam()
return team?.role.equalsRole(
TeamRoleValue.OWNER,
TeamRoleValue.SUPER_ADMIN,
TeamRoleValue.ADMIN
)
})
</script>
const teamStore = useTeamStore()
watch(
() => teamStore.getCurrentTeamId(),
(newTeamId) => {
// Refetch data for new team context
console.log('Switched to team:', newTeamId)
}
)
When making API calls, the current team context can be included:
// In a composable or service
const teamStore = useTeamStore()
const currentTeamId = teamStore.getCurrentTeamId()
// Include in API requests
const response = await api.get(`/teams/${currentTeamId}/resources`)
The default team is set when:
Create your own team switcher component:
<template>
<Select v-model="selectedTeamId" @update:model-value="switchTeam">
<SelectTrigger>
<SelectValue :placeholder="currentTeam?.name" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="team in teams"
:key="team.id"
:value="team.id"
>
{{ team.name }}
</SelectItem>
</SelectContent>
</Select>
</template>
<script setup lang="ts">
const teamStore = useTeamStore()
const teams = computed(() => teamStore.getTeams())
const currentTeam = computed(() => teamStore.getCurrentTeam())
const selectedTeamId = ref(teamStore.getCurrentTeamId())
const switchTeam = (teamId: number) => {
teamStore.setCurrentTeamId(teamId)
}
</script>
Modify the cookie name in stores/team.ts:
const currentTeamId = useCookie<number | null>('my-app-team-id')