Nuda Kit includes a complete multi-tenant team system that allows users to create teams, invite members, and collaborate with different permission levels.
The team system provides:
Multi-Team Support
Role-Based Access
Default Team
Team Avatars
| Column | Type | Description |
|---|---|---|
id | Integer | Primary key |
name | String | Team name |
avatar | String | Avatar file path (nullable) |
created_at | Timestamp | Creation date |
updated_at | Timestamp | Last update |
| Column | Type | Description |
|---|---|---|
id | Integer | Primary key |
user_id | Integer | Foreign key to users |
team_id | Integer | Foreign key to teams |
role | Enum | Member's role in the team |
default | Boolean | Is this the user's default team? |
created_at | Timestamp | When user joined |
updated_at | Timestamp | Last update |
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
│ User │────────▶│ TeamMember │◀────────│ Team │
├─────────────┤ 1:N ├──────────────────┤ N:1 ├─────────────┤
│ id │ │ id │ │ id │
│ email │ │ user_id (FK) │ │ name │
│ firstName │ │ team_id (FK) │ │ avatar │
│ ... │ │ role │ │ ... │
└─────────────┘ │ default │ └─────────────┘
└──────────────────┘
Teams use a hierarchical role system:
| Role | Level | Description |
|---|---|---|
| Owner | 1 | Full control, can delete team |
| Super Admin | 2 | Can manage all members except owner |
| Admin | 3 | Can manage lower-level members |
| Editor | 4 | Can edit team content |
| Viewer | 5 | Read-only access |
Higher roles can manage lower roles:
| Method | Endpoint | Description |
|---|---|---|
GET | /teams | List user's teams |
POST | /teams | Create a new team |
GET | /teams/:id | Get team details |
PUT | /teams/:id | Update team |
DELETE | /teams/:id | Delete team (owner only) |
GET | /teams/:id/members | List team members |
DELETE | /teams/:id/members/:userId | Remove member |
PUT | /teams/:id/members/:userId/roles | Update member role |
PUT | /teams/:id/avatar | Upload team avatar |
DELETE | /teams/:id/avatar | Remove team avatar |
When a user creates a team:
Team record is createdTeamMember with owner role// 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
})
Each user has one default team:
team_members.default columnThe 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
}
// ...
})
Users can switch between teams they belong to:
currentTeamId cookieWhen a team is deleted:
TeamMember records are cascade deletedInvitation records are cascade deletedTeam record is deletedTeam actions are protected by policies:
| Action | Required Role |
|---|---|
| View team | Any member |
| Update team | Owner, Super Admin, Admin |
| Delete team | Owner only |
| Manage members | Owner, Super Admin, Admin |
| Update member role | Owner, Super Admin, Admin* |
| Remove member | Owner, Super Admin, Admin* |
*Cannot manage members with equal or higher role
// composables/queries/useTeamQuery.ts
const { data: teams } = useTeamQuery().getAllTeams
const { mutate: createTeam } = useTeamMutation().createTeam
createTeam({ name: 'My New Team' })
const teamStore = useTeamStore()
teamStore.setCurrentTeamId(teamId)
const teamStore = useTeamStore()
const currentTeam = teamStore.getCurrentTeam()
teams tableTeam model with new columnsUpdate 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
}