⚡ You're viewing a live demo of ChimerAI. Data resets daily at midnight UTC.Get the CLI →

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

  1. Concept Overview
  2. Protecting API Routes
  3. Protecting UI Components
  4. Creating Custom Permissions
  5. 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 posts
  • posts:write - Create and edit posts
  • posts:delete - Delete posts
  • comments:moderate - Moderate comments

Example: Billing Manager Role

  • billing:read - View invoices
  • billing:write - Create invoices
  • payments: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';
// ✅ 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:

  1. Admin Role

    • Permissions: admin:*
    • Should have access to everything
  2. Editor Role

    • Permissions: posts:read, posts:write
    • Can view and edit posts, but not delete
  3. Viewer Role

    • Permissions: posts:read
    • Can only view posts

Test scenarios:

  1. Log in as Viewer → Try to create post → Should fail
  2. Log in as Editor → Create post → Should succeed
  3. Log in as Editor → Delete post → Should fail
  4. Log in as Admin → Delete post → Should succeed

Troubleshooting

"403 Forbidden" error when it shouldn't happen

Check:

  1. Is the user logged in? (401 means not logged in)
  2. Does the user's role have the required permission?
  3. Is the permission name spelled correctly? (case-sensitive)
  4. 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

  1. API Routes: Use requirePermission('resource:action') at the start of every protected route
  2. UI Components: Use hasPermission(user, 'resource:action') for conditional rendering
  3. Create Roles: Go to /admin/roles and define permissions
  4. Assign Roles: Go to /admin/users and assign roles to users
  5. 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.

ChimerAI Docs · Back to Demo