# RBAC Permissions

> Configure role-based access control for admin panel features and actions.

*Canonical: https://makerkit.dev/docs/nextjs-prisma/admin/rbac-permissions*

---

MakerKit's admin RBAC system controls what each admin role can do. By default, a single `admin` role has full permissions. You can add custom roles (support, moderator) with restricted access by editing `packages/rbac/src/admin-rbac.config.ts`. Permissions are enforced server-side via `withAdminPermission()` middleware and client-side via the `useAdminPermissions` hook.

> Use singular resource names (`user`, `session`) not plural. This matches Better Auth's internal permission model.

## Default Configuration

Out of the box, MakerKit provides a single `admin` role with full permissions:

```typescript
import { defineAdminRBACConfig } from './admin/factory';

export default defineAdminRBACConfig({
  // Default: admin role with all permissions
});
```

## Resources and Actions

### Built-in Resources

These resources are controlled by Better Auth and must use exact names:

| Resource | Actions | Description |
|----------|---------|-------------|
| `user` | create, list, get, update, set-role, set-password, ban, impersonate, delete | User management |
| `session` | list, revoke, delete | Session management |

### Custom Resources

These resources are MakerKit-specific:

| Resource | Actions | Description |
|----------|---------|-------------|
| `organizations` | list, view | Organization oversight |
| `dashboard` | view | Admin dashboard access |
| `subscriptions` | list | Subscription viewing |

## Adding Custom Roles

Create restricted admin roles for support staff, moderators, or other team members:

```typescript
import { defineAdminRBACConfig } from './admin/factory';

export default defineAdminRBACConfig({
  // Role hierarchy - higher number = more authority
  roles: {
    support: 50,     // Customer support staff
    moderator: 30,   // Community moderators
  },

  // Define permissions for each custom role
  // Note: 'admin' automatically gets ALL permissions
  permissions: {
    support: {
      user: ['list', 'get', 'ban'],
      session: ['list', 'revoke'],
      organizations: ['list', 'view'],
      dashboard: ['view'],
      subscriptions: ['list'],
    },
    moderator: {
      user: ['list', 'get', 'ban'],
      dashboard: ['view'],
    },
  },
});
```

### Role Hierarchy

Roles are assigned numeric values indicating authority level:

- `admin: 100` - Full access (built-in, cannot be changed)
- `support: 50` - Mid-level access
- `moderator: 30` - Basic access

Higher values indicate more authority. This is useful for permission inheritance patterns if you extend the system.

## Adding Custom Resources

Extend the RBAC system with your own resources:

```typescript
export default defineAdminRBACConfig({
  // Add custom resources
  resources: {
    REPORTS: 'reports',
    CONTENT: 'content',
  },

  // Add custom actions
  actions: {
    EXPORT: 'export',
    PUBLISH: 'publish',
    MODERATE: 'moderate',
  },

  // Define which actions are valid for each resource
  accessController: {
    reports: ['list', 'view', 'export'],
    content: ['list', 'view', 'publish', 'moderate'],
  },

  // Assign permissions to roles
  permissions: {
    support: {
      reports: ['list', 'view'],
      content: ['list', 'view'],
    },
    moderator: {
      content: ['list', 'view', 'moderate'],
    },
  },
});
```

## Server-Side Protection

### Protecting Server Actions

Use `withAdminPermission` middleware to enforce permissions:

```typescript
'use server';

import { adminActionClient, withAdminPermission } from '@kit/action-middleware';

// Require specific permission
export const banUserAction = adminActionClient
  .use(withAdminPermission({ user: ['ban'] }))
  .inputSchema(banUserSchema)
  .action(async ({ parsedInput, ctx }) => {
    // Only admins with 'user:ban' permission reach here
    await banUser(parsedInput.userId);
  });

// Require multiple permissions
export const manageUserAction = adminActionClient
  .use(withAdminPermission({
    user: ['get', 'ban'],
    session: ['revoke'],
  }))
  .inputSchema(manageUserSchema)
  .action(async ({ parsedInput, ctx }) => {
    // Requires ALL specified permissions
  });
```

### Protecting Data Loaders

Use `requireAdmin` for server components and data loaders:

```typescript
import 'server-only';
import { cache } from 'react';
import { requireAdmin } from '@kit/auth/require-admin';

export const loadReportsData = cache(async () => {
  await requireAdmin();  // Redirects if not admin

  return await reportsService.getAll();
});
```

## Client-Side Permission Checks

### useAdminPermissions Hook

Conditionally render UI based on permissions:

```typescript
'use client';

import { useAdminPermissions } from '@kit/admin/hooks/use-admin-permissions';

function UserActions({ userId }: { userId: string }) {
  const { hasPermission, isLoading } = useAdminPermissions();

  if (isLoading) return <Spinner />;

  return (
    <div>
      {hasPermission({ user: ['get'] }) && (
        <Button>View User</Button>
      )}

      {hasPermission({ user: ['ban'] }) && (
        <Button>Ban User</Button>
      )}

      {hasPermission({ user: ['delete'] }) && (
        <Button variant="destructive">Delete User</Button>
      )}
    </div>
  );
}
```

### Hook API Reference

```typescript
const {
  role,            // Current user's admin role (or null)
  isAdmin,         // Whether user has any admin role
  isLoading,       // Whether session is loading
  hasPermission,   // Check specific permission(s)
  hasAnyPermission,// Check if user has any permission on resource
  permissions,     // All permissions for current role
  getActionsFor,   // Get all actions user can perform on resource
} = useAdminPermissions();

// Check single permission
hasPermission({ user: ['ban'] });

// Check multiple permissions (all required)
hasPermission({ user: ['ban', 'delete'] });

// Check any permission on resource
hasAnyPermission('user');  // true if user has any user permission

// Get all available actions
getActionsFor('user');  // ['list', 'get', 'ban', ...]
```

### Role Checking Utilities

For simple admin role checks without permission granularity:

```typescript
// Client-side
import { isAdminRole } from '@kit/auth/is-admin-role';

function showAdminLink(userRole: string | null) {
  return isAdminRole(userRole);
}

// Server-side (non-redirecting)
import { isUserAdmin } from '@kit/auth/require-admin';

export async function getPageData() {
  const isAdmin = await isUserAdmin();
  return { showAdminFeatures: isAdmin };
}
```

## Permission Matrix

Default permissions for the built-in `admin` role:

| Resource | Actions |
|----------|---------|
| `user` | create, list, get, update, set-role, set-password, ban, impersonate, delete |
| `session` | list, revoke, delete |
| `organizations` | list, view |
| `dashboard` | view |
| `subscriptions` | list |

## Better Auth Integration

The RBAC system integrates with Better Auth's admin plugin. Important naming conventions:

- **Resources must be singular**: `user` not `users`, `session` not `sessions`
- **Actions must match Better Auth names**: `set-role` not `change-role`, `ban` for both ban and unban

This ensures server actions work correctly with Better Auth's internal permission checks.

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "Why must I use singular resource names like 'user' instead of 'users'?", "answer": "Better Auth's admin plugin expects singular resource names internally. Using 'users' instead of 'user' will cause permission checks to fail silently."},
     {"question": "Does the admin role need explicit permissions?", "answer": "No. The built-in admin role (level 100) automatically receives ALL permissions. Only custom roles like support or moderator need explicit permission grants."},
     {"question": "How do I check permissions in a React component?", "answer": "Use the useAdminPermissions hook: const { hasPermission } = useAdminPermissions(); then hasPermission({ user: ['ban'] }) returns true/false."},
     {"question": "Can I create permissions for custom resources?", "answer": "Yes. Add resources and actions to defineAdminRBACConfig(), define valid actions in accessController, then assign permissions to roles."},
     {"question": "What's the difference between requireAdmin and withAdminPermission?", "answer": "requireAdmin() checks if the user has any admin role. withAdminPermission() checks for specific permissions like user:ban or session:revoke."}
   ]
/%}

---

**Previous:** [Organization Management](./organization-management)

**Next:** [Extending Admin](./extending-admin)
