Roles
Learn how to manage roles and permissions in your Multi Tenant Kit
Organization Roles 👥
The Multi Tenant Kit includes a powerful role-based permission system to control what members can do in your organization! ✨
Understanding Roles 🎯
Roles determine what actions members can perform within an organization. Each member has exactly one role that defines their level of access.
Three Role Types
Multi Tenant Kit includes three pre-configured roles: Owner, Admin, and User. These cover most B2B SaaS use cases!
Customize Roles
Need more roles? You can customize the role system by updating the roleEnum
in src/db/schema/organization.ts
. Add roles like Manager, Viewer, or Contributor to match your use case!
The Three Roles 🏆
Owner (Highest Permission)
The organization creator and ultimate authority:
- ✅ Full Access: Can do everything in the organization
- ✅ Billing Management: Control subscriptions and payments
- ✅ Delete Organization: Can permanently delete the organization
- ✅ Transfer Ownership: Can assign ownership to another member
- ✅ All Admin Permissions: Everything an admin can do, plus more
Use Case: Typically the person who created the organization or pays for the subscription.
Admin (Management Permission)
Trusted team members who help manage the organization:
- ✅ Member Management: Invite, remove, and change member roles
- ✅ Settings Management: Update organization settings and preferences
- ✅ Resource Management: Create, update, and delete organization resources
- ✅ View Billing: Can see billing information (but not modify)
- ❌ Cannot Delete Organization: Only owners can delete
- ❌ Cannot Transfer Ownership: Only owners can change ownership
Use Case: Team leads, managers, or trusted employees who help run the organization.
User (Basic Access)
Standard members with basic permissions:
- ✅ Use Features: Access and use the application's features
- ✅ View Organization Info: See organization details
- ✅ Manage Own Profile: Update their own profile
- ❌ Cannot Invite Members: Cannot invite new people
- ❌ Cannot Change Settings: Cannot modify organization settings
- ❌ Cannot Access Billing: Cannot see billing information
Use Case: Regular team members, employees, or collaborators.
Role Hierarchy 📊
Roles have a hierarchical structure where higher roles have all permissions of lower roles:
const roleHierarchy = {
user: 0, // Lowest
admin: 1, // Middle
owner: 2, // Highest
}
This means:
- Owners can do everything Admins and Users can do
- Admins can do everything Users can do
- Users have only their own permissions
Protecting API Routes with Roles 🔒
Use withOrganizationAuthRequired
to protect routes and enforce role requirements:
Require Minimum Role
// src/app/api/app/org/[slug]/settings/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
import { NextResponse } from 'next/server'
// Only admins and owners can access this route
export const PATCH = withOrganizationAuthRequired(
async (req, context) => {
const { organization } = context.session
const body = await req.json()
// Update organization settings
await updateOrganizationSettings(organization.id, body)
return NextResponse.json({ success: true })
},
OrganizationRole.enum.admin // Requires admin role or higher
)
Owner-Only Routes
// src/app/api/app/org/[slug]/delete/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
// Only owners can delete organizations
export const DELETE = withOrganizationAuthRequired(
async (req, context) => {
const { organization } = context.session
// Delete organization
await deleteOrganization(organization.id)
return NextResponse.json({ success: true })
},
OrganizationRole.enum.owner // Requires owner role
)
User-Level Routes (No Minimum Role)
// src/app/api/app/org/[slug]/projects/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
// Any organization member can access
export const GET = withOrganizationAuthRequired(
async (req, context) => {
const { organization } = context.session
// Fetch projects
const projects = await getProjects(organization.id)
return NextResponse.json(projects)
}
// No role parameter = any member can access
)
How Role Checking Works
When you specify a minimum role, the wrapper automatically allows users with that role or higher. For example, admin
allows both admins and owners!
Checking Roles in Components 🎨
Using useOrganization Hook
'use client'
import { useOrganization } from '@/lib/organizations/useOrganization'
export default function OrganizationSettings() {
const { organization } = useOrganization()
// Get current user's role
const userRole = organization.membership?.role
const isOwner = userRole === 'owner'
const isAdmin = userRole === 'admin' || userRole === 'owner'
return (
<div>
<h1>Organization Settings</h1>
{/* Show delete button only to owners */}
{isOwner && (
<button className="bg-red-500 text-white px-4 py-2 rounded">
Delete Organization
</button>
)}
{/* Show invite button to admins and owners */}
{isAdmin && (
<button className="bg-blue-500 text-white px-4 py-2 rounded">
Invite Members
</button>
)}
{/* Everyone sees this */}
<div>Organization Name: {organization.name}</div>
</div>
)
}
Checking Role Hierarchy
Use the hasHigherOrEqualRole
utility:
import { hasHigherOrEqualRole } from '@/lib/organizations/roles'
// Check if user has sufficient role
const canManageMembers = hasHigherOrEqualRole(
currentUserRole,
'admin'
)
if (canManageMembers) {
// Show member management UI
}
Assigning Roles to Members 👤
When Inviting New Members
Specify the role when creating an invitation:
// src/app/api/app/org/[slug]/invites/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
export const POST = withOrganizationAuthRequired(
async (req, context) => {
const { organization } = context.session
const { email, role } = await req.json()
// Create invitation with specified role
await createInvitation({
organizationId: organization.id,
email,
role: role || 'user', // Default to 'user' if not specified
})
return NextResponse.json({ success: true })
},
OrganizationRole.enum.admin // Only admins can invite
)
Changing Member Roles
Update an existing member's role:
// src/app/api/app/org/[slug]/members/[userId]/route.ts
import { withOrganizationAuthRequired } from '@/lib/auth/withOrganizationAuthRequired'
import { OrganizationRole } from '@/db/schema/organization'
export const PATCH = withOrganizationAuthRequired(
async (req, context) => {
const { organization } = context.session
const { userId } = context.params
const { role } = await req.json()
// Update member role
await updateMemberRole(organization.id, userId, role)
return NextResponse.json({ success: true })
},
OrganizationRole.enum.admin // Only admins can change roles
)
Role-Based UI Examples 🎯
Conditional Rendering
'use client'
import { useOrganization } from '@/lib/organizations/useOrganization'
export function MembersList() {
const { organization } = useOrganization()
const isAdmin = ['admin', 'owner'].includes(organization.membership?.role)
return (
<div>
<h2>Team Members</h2>
{/* List all members */}
{organization.members.map(member => (
<div key={member.id} className="flex items-center justify-between">
<div>
<p>{member.name}</p>
<p className="text-sm text-gray-500">{member.role}</p>
</div>
{/* Only admins see action buttons */}
{isAdmin && (
<div className="space-x-2">
<button>Change Role</button>
<button>Remove</button>
</div>
)}
</div>
))}
{/* Only admins see invite button */}
{isAdmin && (
<button className="mt-4 bg-blue-500 text-white px-4 py-2 rounded">
Invite New Member
</button>
)}
</div>
)
}
Navigation Based on Roles
'use client'
import Link from 'next/link'
import { useOrganization } from '@/lib/organizations/useOrganization'
export function OrgNavigation() {
const { organization } = useOrganization()
const role = organization.membership?.role
const isOwner = role === 'owner'
const isAdmin = ['admin', 'owner'].includes(role)
return (
<nav className="space-y-2">
{/* Everyone sees these */}
<Link href="/app/org/dashboard">Dashboard</Link>
<Link href="/app/org/projects">Projects</Link>
{/* Only admins and owners */}
{isAdmin && (
<>
<Link href="/app/org/members">Team Members</Link>
<Link href="/app/org/settings">Settings</Link>
</>
)}
{/* Only owners */}
{isOwner && (
<Link href="/app/org/billing">Billing</Link>
)}
</nav>
)
}
Best Practices 💡
1. Always Protect API Routes
// ❌ Bad - No role check
export async function DELETE(req: Request) {
await deleteOrganization()
}
// ✅ Good - Protected with role requirement
export const DELETE = withOrganizationAuthRequired(
async (req, context) => {
await deleteOrganization()
},
OrganizationRole.enum.owner
)
2. Use Role Hierarchy
// ❌ Bad - Checking each role individually
if (role === 'admin' || role === 'owner') {
// Allow action
}
// ✅ Good - Using hasHigherOrEqualRole
if (hasHigherOrEqualRole(role, 'admin')) {
// Allow action
}
3. Default to Least Privilege
// ✅ Default to 'user' role when inviting
const role = invitationData.role || 'user'
4. Hide UI for Unauthorized Actions
// ✅ Don't show buttons users can't use
{isAdmin && (
<button onClick={handleDelete}>Delete</button>
)}
5. Validate on Backend
// ✅ Always validate permissions on the server
export const DELETE = withOrganizationAuthRequired(
async (req, context) => {
// Server-side validation
if (context.session.membership.role !== 'owner') {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 403 }
)
}
// Proceed with deletion
}
)
Common Use Cases 📚
Creating a Members Page
'use client'
import { useOrganization } from '@/lib/organizations/useOrganization'
import { useState } from 'react'
export default function MembersPage() {
const { organization } = useOrganization()
const [inviteEmail, setInviteEmail] = useState('')
const [inviteRole, setInviteRole] = useState('user')
const isAdmin = ['admin', 'owner'].includes(
organization.membership?.role
)
const handleInvite = async () => {
await fetch(`/api/app/org/${organization.slug}/invites`, {
method: 'POST',
body: JSON.stringify({ email: inviteEmail, role: inviteRole }),
})
}
return (
<div>
<h1>Team Members</h1>
{/* Invite form - only for admins */}
{isAdmin && (
<div className="mb-8 p-4 border rounded">
<h2>Invite New Member</h2>
<input
type="email"
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
placeholder="email@example.com"
className="border px-3 py-2 rounded"
/>
<select
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value)}
className="border px-3 py-2 rounded ml-2"
>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<button
onClick={handleInvite}
className="ml-2 bg-blue-500 text-white px-4 py-2 rounded"
>
Send Invite
</button>
</div>
)}
{/* Members list */}
<div className="space-y-2">
{organization.members?.map(member => (
<div key={member.id} className="p-4 border rounded">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">{member.name}</p>
<p className="text-sm text-gray-500">{member.email}</p>
</div>
<div className="flex items-center gap-2">
<span className="px-2 py-1 bg-gray-100 rounded text-sm">
{member.role}
</span>
{isAdmin && member.role !== 'owner' && (
<button className="text-red-500 text-sm">
Remove
</button>
)}
</div>
</div>
</div>
))}
</div>
</div>
)
}
Troubleshooting 🔧
"Unauthorized" Error
Make sure you're using the correct role wrapper:
// Check that you have the required role
export const PATCH = withOrganizationAuthRequired(
async (req, context) => {
// Your code
},
OrganizationRole.enum.admin // Make sure you have admin role
)
Role Not Updating
After changing roles, refresh the session:
import { mutate } from 'swr'
// After updating role
await updateMemberRole(userId, newRole)
mutate('/api/app/org/current') // Refresh organization data
Can't Access Organization Data
Make sure you're inside an organization context:
'use client'
import { useOrganization } from '@/lib/organizations/useOrganization'
export default function MyComponent() {
const { organization, isLoading } = useOrganization()
if (isLoading) return <div>Loading...</div>
if (!organization) return <div>No organization selected</div>
// Now you can safely access organization data
}
Summary 📋
You now know how to:
- ✅ Understand the three role types (Owner, Admin, User)
- ✅ Protect API routes with role requirements
- ✅ Check roles in components
- ✅ Assign and change member roles
- ✅ Build role-based UIs
- ✅ Follow security best practices
Start Building
With roles properly configured, you can build secure multi-user organizations! Remember to always validate permissions on the backend. 🎉