Team Accounts Overview
Complete guide to team accounts with settings, members, and multi-tenant architecture
Team Accounts are shared workspaces where multiple users can collaborate on projects, manage resources, and work together within a multi-tenant SaaS architecture.
Overview
Team accounts provide:
- Shared workspace for multiple users
- Role-based access control
- Project management capabilities
- Team-specific settings and billing
- Hierarchical permission system
Architecture
Database Schema
accounts table
- id (uuid, primary key) - name (varchar 150) - Team display name - slug (varchar 150) - URL-friendly identifier - description (varchar 280) - Team description - picture_url (varchar 1000) - Team logo/avatar - status (enum: active, inactive) - Team status - primary_owner_user_id (uuid) - Owner reference - created_at (timestamptz) - updated_at (timestamptz)
accounts_memberships table
- id (bigserial, primary key) - user_id (uuid, foreign key to auth.users) - account_id (uuid, foreign key to accounts) - role (enum: owner, manager, member, readonly) - created_at (timestamptz)
Multi-Tenant Model
┌─────────────────────────────────────────────────────┐ │ Personal Account (User) │ │ ├── Profile Settings │ │ └── Personal Billing │ └─────────────────────────────────────────────────────┘ │ ├── Member of Team Account A │ ├── Role: Owner │ └── Projects: Project 1, Project 2 │ └── Member of Team Account B ├── Role: Manager └── Projects: Project 3
Features
1. Team Creation
Location: Dashboard home Component: CreateTeamAccountDialog Server Action: createTeamAccountAction
Workflow:
- User clicks "Create Team" button
- Dialog appears with team details form
- User enters:
- Team name (required)
- Slug (auto-generated, can be customized)
- Description (optional)
- Real-time slug validation
- Team created with user as owner
- User redirected to team dashboard
2. Team Settings
Comprehensive settings management at /home/[account]/settings
Sections:
Team Logo
- Upload/delete team avatar
- Supports image files
- Stored in Supabase Storage
- Updates header in real-time
Team Details
- Team name (updates slug if not manually customized)
- URL slug (validated for uniqueness globally)
- Description (max 280 characters)
- Status (active/inactive)
Danger Zone
- Delete team (owner only)
- Requires confirmation
- Cascades to members, projects, and related data
3. Members Management
Full member management at /home/[account]/members
Features:
- Team Members Table: Shows all account-level members
- Project-Only Members Table: Members added only to specific projects
- Pending Invitations Table: Pending email invitations
- Add Members: Invite by email with role selection
- Update Roles: Change member roles (respects hierarchy)
- Remove Members: Remove members (cannot remove owner)
Role Hierarchy:
Owner (100) └── Full control ├── Manage all members ├── Delete team ├── Manage billing └── Access all projects Manager (50) └── Team management ├── Manage members (except owners) ├── Create projects └── Team settings Member (25) └── Standard access ├── Access assigned projects └── View team info Readonly (10) └── View-only access ├── View team info └── View assigned projects (readonly)
4. Navigation & Layout
Sidebar Navigation
- Team selector dropdown
- Dashboard
- Projects list
- Members
- Settings
- Billing
Header
- Team account selector with logo
- Breadcrumbs
- User account dropdown
File Organization
packages/features/team-accounts/ ├── src/ │ ├── components/ │ │ ├── settings/ │ │ │ ├── team-account-settings-container.tsx │ │ │ ├── update-team-account-name-form.tsx │ │ │ ├── update-team-account-image-container.tsx │ │ │ └── team-account-danger-zone.tsx │ │ ├── members/ │ │ │ ├── account-members-table.tsx │ │ │ ├── invite-members-dialog-container.tsx │ │ │ ├── update-member-role-dialog.tsx │ │ │ └── remove-member-dialog.tsx │ │ └── invitations/ │ │ ├── account-invitations-table.tsx │ │ └── update-invitation-dialog.tsx │ ├── server/ │ │ ├── actions/ │ │ │ ├── team-details-server-actions.ts │ │ │ ├── team-members-server-actions.ts │ │ │ └── team-invitations-server-actions.ts │ │ └── services/ │ │ ├── create-team-account.service.ts │ │ └── account-members.service.ts │ └── schema/ │ └── update-team-name.schema.ts │ apps/web/app/home/[account]/ ├── page.tsx (Dashboard) ├── settings/ │ └── page.tsx ├── members/ │ └── page.tsx ├── projects/ │ └── ... └── _components/ ├── team-account-layout-sidebar.tsx └── team-account-layout-page-header.tsx
Key Components
TeamAccountSettingsContainer
Purpose: Main container for team settings Sections:
- Team logo uploader
- Team details form (name, slug, description, status)
- Danger zone (delete team)
UpdateTeamAccountNameForm
Purpose: Form for editing team information Features:
- Smart slug auto-generation
- Real-time validation with debouncing
- Visual feedback (icons, border colors)
- Status toggle
- Description textarea
Key Innovation - Smart Slug Handling:
// Track last auto-generated slug
const lastAutoGeneratedSlug = useRef(slugify(props.account.name));
// Only auto-update if current matches last auto-generated
useEffect(() => {
if (nameValue) {
const currentSlug = form.getValues('slug');
const generatedSlug = slugify(nameValue);
if (currentSlug === lastAutoGeneratedSlug.current) {
form.setValue('slug', generatedSlug);
lastAutoGeneratedSlug.current = generatedSlug;
}
}
}, [nameValue]);
This allows:
- ✅ Auto-generation when name changes
- ✅ Manual slug customization preserved
- ✅ Updating slug independently of name
UpdateTeamAccountImage
Purpose: Upload/delete team avatar Features:
- Image upload to Supabase Storage
- Delete existing image
- Real-time header update with
router.refresh() - Toast notifications
Cache Revalidation Strategy
When team details or logo change, the header must update immediately. This uses Next.js cache revalidation:
// In server action after update
revalidatePath(`/home/${newSlug}`, 'layout');
// In client component after image update
router.refresh();
Why This Matters:
- Next.js aggressively caches layout data
- Header shows team name and logo
- Without revalidation, updates don't appear
'layout'type revalidates parent layout
Use Cases
Use Case 1: Creating a Team Account
Actor: Authenticated user Precondition: User has personal account
Flow:
- User clicks "Create Team" from dashboard
- Dialog opens with form
- User enters:
- Name: "Acme Corporation"
- Slug: auto-generates "acme-corporation"
- Description: "Our main team workspace"
- System validates slug uniqueness
- Click "Create Team"
- Team created with user as owner
- Redirect to
/home/acme-corporation - Team appears in account selector
Use Case 2: Updating Team Slug Only
Actor: Team owner Precondition: User is team owner
Flow:
- Navigate to team settings
- Current state:
- Name: "Engineering Team"
- Slug: "engineering-team"
- Want shorter URL: "eng"
- Keep name unchanged
- Change slug to "eng"
- System validates: ✓ Unique
- Click "Save Settings"
- System updates only slug
- Revalidates cache
- Redirects to
/home/eng/settings - Header URL updated
Technical Details:
// Server action
if (newSlug && newSlug !== slug) {
// Only redirect if slug actually changed
revalidatePath(`/home/${newSlug}`, 'layout');
redirect(path.replace('[account]', newSlug));
}
Use Case 3: Team Logo Update
Actor: Team manager Precondition: User has team manage permissions
Flow:
- Navigate to team settings
- Click upload in logo section
- Select image file
- Image uploads to Supabase Storage
- Database updates with new picture_url
router.refresh()called- Header logo updates immediately
- Toast notification: "Logo updated"
Technical Flow:
// After successful upload
const pictureUrl = await uploadToStorage(file);
await client
.from('accounts')
.update({ picture_url: pictureUrl })
.eq('id', accountId);
// Trigger client-side refresh
router.refresh();
API Reference
Server Actions
createTeamAccountAction
createTeamAccountAction({
name: string,
slug: string
})
updateTeamAccountDetails
updateTeamAccountDetails({
accountId: string,
name: string,
slug: string,
description: string | null,
status: 'active' | 'inactive',
path: string
})
deleteTeamAccountAction
deleteTeamAccountAction({
accountId: string
})
RPC Functions
is_account_slug_unique
is_account_slug_unique( p_slug varchar, p_account_id uuid DEFAULT NULL )
Best Practices
1. Cache Management
Always revalidate after mutations:
// Server action
revalidatePath(`/home/${slug}`, 'layout');
// Client component
router.refresh();
2. Slug Changes
Handle redirects carefully:
if (newSlug && newSlug !== oldSlug) {
// Revalidate with new slug
revalidatePath(`/home/${newSlug}`, 'layout');
// Redirect to new URL
redirect(path.replace('[account]', newSlug));
}
3. Permissions
Always validate server-side:
// Check permission in server action
const canManage = await checkPermission(userId, accountId, 'team.manage');
if (!canManage) {
throw new Error('Unauthorized');
}
4. Full Width Layout
Remove width restrictions for modern, spacious UI:
// Before
<PageBody>
<div className="max-w-2xl">
<SettingsForm />
</div>
</PageBody>
// After
<PageBody>
<SettingsForm />
</PageBody>