Organizations
Multi-tenancy and team collaboration
📋 Overview
Organizations enable teams to collaborate on projects with role-based access control. Multiple users can belong to an organization with different permission levels (OWNER, ADMIN, MEMBER).
🎯 User Stories
- As a team lead, I want to create an organization for my team
- As an organization owner, I want to invite team members with appropriate roles
- As a member, I want to access all organization projects
- As an admin, I want to manage organization settings and members
🏗️ Database Schema
organizations table:
{
id: string
name: string
slug: string // Unique URL-friendly identifier
description: string?
createdAt: Date
updatedAt: Date
}
organizationMembers table:
{
id: string
organizationId: string
userId: string
role: 'OWNER' | 'ADMIN' | 'MEMBER'
createdAt: Date
}
Indexes:
CREATE UNIQUE INDEX organizations_slug_key ON organizations(slug);
CREATE INDEX organization_members_org_user_idx ON organization_members(organization_id, user_id);
CREATE UNIQUE INDEX organization_members_organization_id_user_id_key
ON organization_members(organization_id, user_id);
🔌 API Reference
Create Organization
const org = await trpc.organizations.create.mutate({
name: 'Acme Corp',
slug: 'acme-corp',
description: 'Our company organization'
})
// Response
{
organization: {
id: 'org_123',
name: 'Acme Corp',
slug: 'acme-corp',
description: 'Our company organization',
createdAt: '2024-01-01T00:00:00Z'
}
}
Validation:
- Name required (min 1 character)
- Slug must be unique and URL-safe
- Automatically adds creator as OWNER
List Organizations
// Get user's organizations
const orgs = await trpc.organizations.list.query({
userId: 'user_123'
})
// Response
{
organizations: [
{
id: 'org_123',
name: 'Acme Corp',
slug: 'acme-corp',
role: 'OWNER',
memberCount: 5,
projectCount: 12
}
]
}
Add Member
await trpc.organizations.addMember.mutate({
organizationId: 'org_123',
userId: 'user_456',
role: 'MEMBER'
})
Permissions:
- Only OWNER and ADMIN can add members
- Cannot add duplicate members
- Role options: OWNER, ADMIN, MEMBER
Update Member Role
await trpc.organizations.updateMemberRole.mutate({
organizationId: 'org_123',
userId: 'user_456',
role: 'ADMIN'
})
Rules:
- Only OWNER can change roles
- Cannot remove last OWNER
- Cannot change own role (prevents lockout)
Remove Member
await trpc.organizations.removeMember.mutate({
organizationId: 'org_123',
userId: 'user_456'
})
Rules:
- OWNER and ADMIN can remove members
- Cannot remove last OWNER
- Removes access to all organization projects
🎨 Frontend Implementation
Organization Selector
export function OrganizationSelector() {
const { data } = trpc.organizations.list.useQuery()
return (
<Select>
<SelectTrigger>
<SelectValue placeholder="Select organization" />
</SelectTrigger>
<SelectContent>
<SelectItem value="personal">Personal</SelectItem>
{data?.organizations.map((org) => (
<SelectItem key={org.id} value={org.id}>
{org.name}
</SelectItem>
))}
</SelectContent>
</Select>
)
}
Organization Settings Page
export function OrganizationSettings({ orgId }: Props) {
return (
<Tabs defaultValue="general">
<TabsList>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="members">Members</TabsTrigger>
<TabsTrigger value="projects">Projects</TabsTrigger>
</TabsList>
<TabsContent value="general">
<OrganizationGeneralSettings orgId={orgId} />
</TabsContent>
<TabsContent value="members">
<MemberManagement orgId={orgId} />
</TabsContent>
<TabsContent value="projects">
<OrganizationProjects orgId={orgId} />
</TabsContent>
</Tabs>
)
}
Member Management
export function MemberManagement({ orgId }: Props) {
const { data } = trpc.organizations.listMembers.useQuery({ orgId })
const removeMember = trpc.organizations.removeMember.useMutation()
return (
<Card>
<CardHeader>
<CardTitle>Members</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.members.map((member) => (
<TableRow key={member.id}>
<TableCell>{member.name}</TableCell>
<TableCell>{member.email}</TableCell>
<TableCell>
<Badge>{member.role}</Badge>
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreVerticalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Change Role</DropdownMenuItem>
<DropdownMenuItem
onClick={() => removeMember.mutate({
orgId,
userId: member.id
})}
className="text-destructive"
>
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)
}
🔐 Permission System
Role Capabilities
| Action | OWNER | ADMIN | MEMBER |
|---|---|---|---|
| View projects | ✅ | ✅ | ✅ |
| Create projects | ✅ | ✅ | ✅ |
| Delete projects | ✅ | ✅ | ❌ |
| Add members | ✅ | ✅ | ❌ |
| Remove members | ✅ | ✅ | ❌ |
| Change roles | ✅ | ❌ | ❌ |
| Delete organization | ✅ | ❌ | ❌ |
Permission Checks
function canManageMembers(userRole: string): boolean {
return ['OWNER', 'ADMIN'].includes(userRole)
}
function canDeleteProject(userRole: string): boolean {
return ['OWNER', 'ADMIN'].includes(userRole)
}
✅ Testing
describe('Organizations', () => {
it('creates organization with creator as owner', async () => {
const org = await trpc.organizations.create.mutate({
name: 'Test Org',
slug: 'test-org'
})
const members = await trpc.organizations.listMembers.query({
orgId: org.id
})
expect(members[0].role).toBe('OWNER')
})
})
🔗 Related Documentation
- Projects: Assign projects to organizations
- GitHub Integration: Organization-level OAuth apps
- API Reference: Organizations API
Next: GitHub Integration →