We use tracking cookies to understand how you use the product and help us improve it. For more information on how we store cookies, read our  privacy policy.

Implement Roles

Learn how to add role-based access control with Clerk organizations and permissions

Learn how to implement role-based access control (RBAC) in your Plainform application using Clerk's organization features.

Goal

By the end of this recipe, you'll have implemented user roles and permissions to control access to different parts of your application.

Prerequisites

  • A working Plainform installation
  • Clerk account with application configured
  • Basic understanding of authentication concepts

Clerk provides two approaches for roles: Organization Roles (for team-based apps) and Custom Metadata (for simple role systems). This guide covers both.

Use Clerk Organizations for team-based applications with roles like Admin, Member, and Guest.

Enable Organizations in Clerk

Navigate to your Clerk Dashboard:

  1. Go to ConfigureOrganizations
  2. Toggle Enable organizations to ON
  3. Configure organization settings:
    • Max allowed organizations per user: Set limit or leave unlimited
    • Allow users to create organizations: Enable if users can create teams
    • Allow users to delete organizations: Enable with caution
  4. Click Save

Define Organization Roles

In Clerk Dashboard:

  1. Go to ConfigureRoles & Permissions
  2. Click Create role
  3. Add roles for your application:

Example roles:

  • Admin: Full access to organization settings and data
  • Member: Can view and edit content
  • Billing: Can manage billing and subscriptions
  • Guest: Read-only access

For each role, define permissions:

  • org:settings:manage - Manage organization settings
  • org:members:manage - Invite and remove members
  • org:billing:manage - Manage billing
  • org:content:write - Create and edit content
  • org:content:read - View content

Start with basic roles (Admin, Member) and add more as your app grows. You can always add roles later.

Check Roles in Server Components

Use auth() to check user roles and permissions:

app/admin/page.tsx
import { auth } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';

export default async function AdminPage() {
  const { userId, orgRole, has } = await auth();

  if (!userId) {
    redirect('/sign-in');
  }

  // Check role or permission
  if (orgRole !== 'org:admin' || !has({ permission: 'org:settings:manage' })) {
    return <div>Access denied. Admin role required.</div>;
  }

  return <div>Admin Dashboard</div>;
}

Check Roles in Client Components

Use useAuth() hook for client-side role checks:

components/AdminButton.tsx
'use client';

import { useAuth } from '@clerk/nextjs';

export function AdminButton() {
  const { orgRole, has } = useAuth();

  if (orgRole !== 'org:admin' || !has({ permission: 'org:settings:manage' })) {
    return null;
  }

  return <button>Admin Action</button>;
}

Protect API Routes

Add role checks to API routes:

app/api/admin/users/route.ts
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export async function GET() {
  const { userId, has } = await auth();

  if (!userId || !has({ role: 'org:admin' })) {
    return new NextResponse('Forbidden', { status: 403 });
  }

  const users = await getAllUsers();
  return NextResponse.json(users);
}

Protect Routes with Middleware

Add role-based protection in proxy.ts:

proxy.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isAdminRoute = createRouteMatcher(['/admin(.*)']);
const isBillingRoute = createRouteMatcher(['/billing(.*)']);

export default clerkMiddleware(async (auth, req) => {
  // Protect admin routes
  if (isAdminRoute(req)) {
    await auth.protect((has) => {
      return has({ role: 'org:admin' });
    });
  }

  // Protect billing routes
  if (isBillingRoute(req)) {
    await auth.protect((has) => {
      return has({ permission: 'org:billing:manage' });
    });
  }
});

Approach 2: Custom Metadata (Simple Role System)

Use custom metadata for simple role systems without organizations.

Add Role to User Metadata

Set user role in Clerk Dashboard or via API:

In Clerk Dashboard:

  1. Go to Users → Select a user
  2. Scroll to MetadataPublic metadata
  3. Add: { "role": "admin" }
  4. Click Save

Via Webhook (on user creation):

app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix';

export async function POST(req: Request) {
  const payload = await req.json();
  const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
  const evt = wh.verify(JSON.stringify(payload), headers);

  if (evt.type === 'user.created') {
    await clerkClient.users.updateUserMetadata(evt.data.id, {
      publicMetadata: { role: 'member' }, // Default role
    });
  }

  return new Response('', { status: 200 });
}

Check Role in Server Components

Access role from user metadata:

app/admin/page.tsx
import { currentUser } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';

export default async function AdminPage() {
  const user = await currentUser();

  if (!user) {
    redirect('/sign-in');
  }

  const userRole = user.publicMetadata.role as string;

  if (userRole !== 'admin') {
    return <div>Access denied. Admin role required.</div>;
  }

  return <div>Admin Dashboard</div>;
}

Create Role Helper Functions

Create utility functions for role checks:

lib/auth/roles.ts
import { auth, currentUser } from '@clerk/nextjs/server';

export type UserRole = 'admin' | 'member' | 'guest';

export async function getUserRole(): Promise<UserRole> {
  const user = await currentUser();
  return (user?.publicMetadata?.role as UserRole) || 'guest';
}

export async function isAdmin(): Promise<boolean> {
  const role = await getUserRole();
  return role === 'admin';
}

export async function hasRole(requiredRole: UserRole): Promise<boolean> {
  const role = await getUserRole();
  
  const roleHierarchy: Record<UserRole, number> = {
    admin: 3,
    member: 2,
    guest: 1,
  };

  return roleHierarchy[role] >= roleHierarchy[requiredRole];
}

Usage:

app/admin/page.tsx
import { isAdmin } from '@/lib/auth/roles';

export default async function AdminPage() {
  if (!(await isAdmin())) {
    return <div>Access denied</div>;
  }

  return <div>Admin Dashboard</div>;
}

Role-Based UI Components

Conditional Rendering Component

components/RoleGate.tsx
'use client';

import { useUser } from '@clerk/nextjs';

interface RoleGateProps {
  allowedRoles: string[];
  children: React.ReactNode;
  fallback?: React.ReactNode;
}

export function RoleGate({ allowedRoles, children, fallback }: RoleGateProps) {
  const { user } = useUser();
  const userRole = user?.publicMetadata?.role as string;

  if (!allowedRoles.includes(userRole)) {
    return fallback || null;
  }

  return <>{children}</>;
}

Usage:

<RoleGate allowedRoles={['admin', 'member']}>
  <AdminPanel />
</RoleGate>

Common Issues

Role Not Updating

  • Clear browser cache and cookies
  • Sign out and sign in again
  • Verify metadata is saved in Clerk Dashboard
  • Check that you're reading from the correct metadata field

Permission Denied Despite Correct Role

  • Verify role name matches exactly (case-sensitive)
  • Check middleware configuration
  • Ensure user is in the correct organization (for org roles)
  • Review Clerk Dashboard → Roles & Permissions

Metadata Not Accessible

  • Ensure you're using publicMetadata (not privateMetadata)
  • Verify the user object is loaded (isLoaded is true)
  • Check that metadata was saved correctly in Clerk Dashboard

Organization Roles Not Working

  • Verify organizations are enabled in Clerk Dashboard
  • Check that user is a member of an organization
  • Ensure roles are defined in Clerk Dashboard
  • Confirm user has been assigned a role in the organization

Best Practices

  • Use Organizations for Teams: If your app has teams/workspaces, use Clerk Organizations
  • Use Metadata for Simple Roles: For single-tenant apps, custom metadata is simpler
  • Server-Side Checks: Always verify roles server-side, never trust client-side checks alone
  • Role Hierarchy: Define clear role hierarchy (Guest < Member < Admin)
  • Audit Logging: Log role changes and permission checks for security
  • Default Role: Assign a default role to new users (usually "member" or "guest")

Next Steps

How is this guide ?

Last updated on