RBAC Implementation Guide
This guide shows you how to use the built-in Role-Based Access Control (RBAC) system in your application.
🎯 Quick Start: Try the Demo
Your project includes a live demo page at /demo/permissions that shows:
- ✅ Your current roles and permissions
- ✅ Live permission checks (what you can/cannot access)
- ✅ Code examples for implementing RBAC
- ✅ Interactive feature cards
Visit http://localhost:3001/demo/permissions (after logging in) to see RBAC in action!
Prerequisites
Make sure you have the ChimerAI CLI installed globally:
# Install the CLI globally
pnpm install -g @chimerai/cli
# Now you can use 'chimerai' commands anywhere
chimerai --version
Table of Contents
- Concept Overview
- Protecting API Routes
- Protecting UI Components
- Creating Custom Permissions
- Real-World Examples
Concept Overview
The RBAC system uses permissions to control access:
- Permissions follow the format:
resource:action(e.g.,posts:read,billing:write) - Users are assigned roles (e.g., "Editor", "Viewer")
- Each role contains a list of permissions
- Special permission
admin:*grants full access to everything
Protecting API Routes
Step 1: Import the permission checker
import { requirePermission } from '@/lib/auth/require-permission';
Step 2: Add permission check to your API route
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { requirePermission } from '@/lib/auth/require-permission';
import { prisma } from '@/lib/prisma';
export async function GET(req: NextRequest) {
// Check if user has permission to read posts
const permissionError = await requirePermission('posts:read');
if (permissionError) return permissionError;
// User has permission, proceed with logic
const posts = await prisma.post.findMany();
return NextResponse.json(posts);
}
export async function POST(req: NextRequest) {
// Check if user has permission to create posts
const permissionError = await requirePermission('posts:write');
if (permissionError) return permissionError;
const body = await req.json();
const post = await prisma.post.create({ data: body });
return NextResponse.json(post);
}
export async function DELETE(req: NextRequest) {
// Check if user has permission to delete posts
const permissionError = await requirePermission('posts:delete');
if (permissionError) return permissionError;
// Delete logic here...
return NextResponse.json({ success: true });
}
What happens:
- If user is not logged in → Returns
401 Unauthorized - If user lacks permission → Returns
403 Forbidden - If user has permission → Returns
null(continues execution)
Protecting UI Components
Client-Side Permission Check
'use client';
import { useSession } from 'next-auth/react';
import { hasPermission } from '@/lib/permissions';
export default function MyComponent() {
const { data: session } = useSession();
const user = session?.user as any;
// Check if user can create posts
const canCreatePosts = user && hasPermission(user, 'posts:write');
// Check if user can delete posts
const canDeletePosts = user && hasPermission(user, 'posts:delete');
return (
<div>
<h1>Posts</h1>
{/* Only show "Create Post" button if user has permission */}
{canCreatePosts && (
<button onClick={handleCreate}>Create Post</button>
)}
{/* Show posts list (everyone can see) */}
<PostList />
{/* Only show delete button if user has permission */}
{canDeletePosts && (
<button onClick={handleDelete}>Delete Post</button>
)}
</div>
);
}
Conditional Rendering
import { useSession } from 'next-auth/react';
import { hasPermission } from '@/lib/permissions';
export function AdminPanel() {
const { data: session } = useSession();
const user = session?.user as any;
// Only render admin panel if user has admin permission
if (!user || !hasPermission(user, 'admin:*')) {
return <div>Access Denied</div>;
}
return (
<div>
<h2>Admin Panel</h2>
{/* Admin-only content */}
</div>
);
}
Creating Custom Permissions
Step 1: Define your permissions
Go to /admin/roles and create a new role with custom permissions:
Example: Blog Editor Role
posts:read- View blog postsposts:write- Create and edit postsposts:delete- Delete postscomments:moderate- Moderate comments
Example: Billing Manager Role
billing:read- View invoicesbilling:write- Create invoicespayments:process- Process payments
Step 2: Create a user with the role
Go to /admin/users and assign the role to a user.
Step 3: Use the permissions in your code
// app/api/blog/posts/route.ts
export async function GET(req: NextRequest) {
const permissionError = await requirePermission('posts:read');
if (permissionError) return permissionError;
// Your logic here
}
// app/api/billing/invoices/route.ts
export async function GET(req: NextRequest) {
const permissionError = await requirePermission('billing:read');
if (permissionError) return permissionError;
// Your logic here
}
Real-World Examples
Example 1: Multi-Tenant SaaS Application
// Permissions structure
const permissions = {
// Workspace management
'workspaces:read', // View workspaces
'workspaces:write', // Create/edit workspaces
'workspaces:delete', // Delete workspaces
// Project management
'projects:read',
'projects:write',
'projects:delete',
// Team management
'team:invite', // Invite team members
'team:remove', // Remove team members
'team:manage-roles', // Change member roles
// Billing
'billing:view',
'billing:manage',
};
// Roles
const roles = [
{
name: 'Owner',
permissions: ['admin:*'] // Full access
},
{
name: 'Admin',
permissions: [
'workspaces:read',
'workspaces:write',
'projects:read',
'projects:write',
'team:invite',
'team:remove',
]
},
{
name: 'Member',
permissions: [
'workspaces:read',
'projects:read',
'projects:write',
]
},
{
name: 'Viewer',
permissions: [
'workspaces:read',
'projects:read',
]
}
];
Example 2: E-Commerce Platform
// API Route for orders
// app/api/orders/route.ts
export async function GET(req: NextRequest) {
const permissionError = await requirePermission('orders:read');
if (permissionError) return permissionError;
const orders = await prisma.order.findMany();
return NextResponse.json(orders);
}
export async function POST(req: NextRequest) {
const permissionError = await requirePermission('orders:create');
if (permissionError) return permissionError;
const body = await req.json();
const order = await prisma.order.create({ data: body });
return NextResponse.json(order);
}
// UI Component
export function OrderManagement() {
const { data: session } = useSession();
const user = session?.user as any;
const canViewOrders = hasPermission(user, 'orders:read');
const canCreateOrders = hasPermission(user, 'orders:create');
const canRefund = hasPermission(user, 'orders:refund');
if (!canViewOrders) {
return <div>Access Denied</div>;
}
return (
<div>
<h1>Orders</h1>
{canCreateOrders && <button>Create Order</button>}
<OrdersList />
{canRefund && <button>Issue Refund</button>}
</div>
);
}
Example 3: Content Management System
// Permissions
const cmsPermissions = [
'content:read', // View content
'content:write', // Create/edit content
'content:publish', // Publish content
'content:delete', // Delete content
'media:upload', // Upload media
'media:delete', // Delete media
'analytics:view', // View analytics
];
// Protecting a page route
// app/blog/edit/[id]/page.tsx
('use client');
export default function EditPostPage({ params }: { params: { id: string } }) {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
if (status === 'authenticated') {
const user = session?.user as any;
if (!hasPermission(user, 'content:write')) {
router.push('/403'); // Redirect to forbidden page
}
}
}, [status, session, router]);
// Rest of component...
}
Best Practices
1. Always protect both API and UI
// ❌ BAD: Only protecting UI
{canDelete && <button onClick={deleteUser}>Delete</button>}
// ✅ GOOD: Protect both UI and API
// UI:
{canDelete && <button onClick={deleteUser}>Delete</button>}
// API:
export async function DELETE(req) {
const permissionError = await requirePermission('users:delete');
if (permissionError) return permissionError;
// ...
}
2. Use descriptive permission names
// ❌ BAD
'feature1:do';
'stuff:action';
// ✅ GOOD
'posts:publish';
'users:deactivate';
'invoices:approve';
3. Group related permissions
// ✅ GOOD
'blog:read';
'blog:write';
'blog:delete';
'blog:publish';
4. Use wildcard for admin access
// Instead of listing all permissions for admin
{
name: 'Administrator',
permissions: ['admin:*']
}
Testing Your Permissions
Create test roles:
-
Admin Role
- Permissions:
admin:* - Should have access to everything
- Permissions:
-
Editor Role
- Permissions:
posts:read,posts:write - Can view and edit posts, but not delete
- Permissions:
-
Viewer Role
- Permissions:
posts:read - Can only view posts
- Permissions:
Test scenarios:
- Log in as Viewer → Try to create post → Should fail
- Log in as Editor → Create post → Should succeed
- Log in as Editor → Delete post → Should fail
- Log in as Admin → Delete post → Should succeed
Troubleshooting
"403 Forbidden" error when it shouldn't happen
Check:
- Is the user logged in? (
401means not logged in) - Does the user's role have the required permission?
- Is the permission name spelled correctly? (case-sensitive)
- Did you update permissions after creating the role?
Permission check not working in UI
// Make sure you're checking the right user object
const { data: session } = useSession();
const user = session?.user as any;
// Debug: Log user roles and permissions
console.log('User:', user);
console.log('Roles:', user?.roles);
console.log(
'Permissions:',
user?.roles?.flatMap((r) => r.permissions)
);
API returns 500 instead of 403
Check server logs - there might be a database error or permission check failing.
Summary
- API Routes: Use
requirePermission('resource:action')at the start of every protected route - UI Components: Use
hasPermission(user, 'resource:action')for conditional rendering - Create Roles: Go to
/admin/rolesand define permissions - Assign Roles: Go to
/admin/usersand assign roles to users - Test: Always test with different user roles to ensure permissions work
That's it! You now have a complete RBAC system ready to use in your application.