MyApp

Getting Started

Introduction

Content & Marketing

Add Blog PostAdd Blog AuthorAdd TestimonialsCustomize HeroAdd FAQ Items

Payments

Add Stripe ProductCreate Stripe SubscriptionAdd Stripe CouponCustomize CheckoutTest Payments Locally

Authentication

Customize Sign-InAdd OAuthImplement RolesProtect Routes

Content Management

Add Doc PageCreate Doc SectionCustomize Theme

Customization

Change ColorsAdd FontCustomize EmailsUse PostHog Analytics

Deployment

Deploy VercelDatabase Migrations

Advanced

Server ActionsAdd Rate Limiting
MyApp

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:

  1. Go to Configure → Organizations
  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 Configure → Roles & 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 Metadata → Public 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

  • 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

Add OAuth

Learn how to add OAuth providers like Google, GitHub, Microsoft, and more

Protect Routes

Learn how to protect routes with middleware, server-side checks, and role-based access control

On this page

Goal
Prerequisites
Approach 1: Organization Roles (Recommended for Teams)
Enable Organizations in Clerk
Define Organization Roles
Check Roles in Server Components
Check Roles in Client Components
Protect API Routes
Protect Routes with Middleware
Approach 2: Custom Metadata (Simple Role System)
Add Role to User Metadata
Check Role in Server Components
Create Role Helper Functions
Role-Based UI Components
Conditional Rendering Component
Common Issues
Role Not Updating
Permission Denied Despite Correct Role
Metadata Not Accessible
Organization Roles Not Working
Best Practices
Next Steps
Related Documentation