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>
  )
}
'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. 🎉