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.
Approach 1: Organization Roles (Recommended for Teams)
Use Clerk Organizations for team-based applications with roles like Admin, Member, and Guest.
Enable Organizations in Clerk
Navigate to your Clerk Dashboard:
- Go to Configure → Organizations
- Toggle Enable organizations to ON
- 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
- Click Save
Define Organization Roles
In Clerk Dashboard:
- Go to Configure → Roles & Permissions
- Click Create role
- 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 settingsorg:members:manage- Invite and remove membersorg:billing:manage- Manage billingorg:content:write- Create and edit contentorg: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:
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:
'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:
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:
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:
- Go to Users → Select a user
- Scroll to Metadata → Public metadata
- Add:
{ "role": "admin" } - Click Save
Via Webhook (on user creation):
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:
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:
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:
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
'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(notprivateMetadata) - Verify the user object is loaded (
isLoadedis 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
- Protect Routes - Secure routes with middleware
- Customize Sign-In - Customize authentication UI
- Add OAuth - Enable social login
Related Documentation
- Authentication Overview - Learn about Clerk integration
- Usage & Integration - Authentication patterns
- Troubleshooting - Fix common issues
How is this guide ?
Last updated on