Extending Admin
Add custom functionality to the admin panel.
To extend the admin panel, you can create a new page in the apps/web/app/[locale]/(internal)/admin folder. You can then add the page to the sidebar by adding it to the admin-sidebar.tsx file.
Admin Protection
There are several layers of protection you can use to protect the admin panel. This is already enforced using the proxy/middleware, but we recommend always adding the required checks in pages, layouts, API routes, and Server Actions.
Server Components and Loaders
Use requireAdmin to protect server components and data loaders:
import 'server-only';import { cache } from 'react';import { requireAdmin } from '@kit/auth/require-admin';export const loadFeatureData = cache(async () => { await requireAdmin(); // Redirects if not admin // Load admin-only data return await fetchData();});Server Actions
Use adminActionClient to protect server actions:
'use server';import { z } from 'zod';import { revalidatePath } from 'next/cache';import { adminActionClient } from '@kit/auth/admin-action-middleware';const updateSettingSchema = z.object({ key: z.string().min(1), value: z.string(),});export const updateSettingAction = adminActionClient .inputSchema(updateSettingSchema) .action(async ({ parsedInput, ctx }) => { const { user } = ctx; // Guaranteed to be admin // Perform admin action await updateSetting(parsedInput.key, parsedInput.value); revalidatePath('/admin', 'layout'); return { success: true }; });Client-Side Role Checking
Use isAdminRole for conditional UI rendering:
import { isAdminRole } from '@kit/auth/is-admin-role';function UserActions({ user }) { const showAdminLink = isAdminRole(user.role); return ( <div> {showAdminLink && <Link href="/admin">Admin Panel</Link>} </div> );}Server-Side Admin Check (Non-Redirecting)
Use isUserAdmin when you need to check admin status without redirecting:
import { isUserAdmin } from '@kit/auth/require-admin';export async function getPageData() { const isAdmin = await isUserAdmin(); return { showAdminFeatures: isAdmin, // ... other data };}Adding Admin Pages
1. Create Page Component
Create a page in the admin package:
// packages/admin/src/reports/page.tsximport { loadReportsData } from './lib/loaders/reports.loader';import { ReportsTable } from './components/reports-table';export async function ReportsPage() { const data = await loadReportsData(); return ( <div className="space-y-6"> <h1 className="text-2xl font-bold">Reports</h1> <ReportsTable data={data} /> </div> );}2. Create Data Loader
// packages/admin/src/reports/lib/loaders/reports.loader.tsimport 'server-only';import { cache } from 'react';import { requireAdmin } from '@kit/auth/require-admin';export const loadReportsData = cache(async () => { await requireAdmin(); // Fetch reports data return await reportsService.getAll();});3. Export from Package
// packages/admin/src/index.tsexport { ReportsPage } from './reports/page';4. Create Route
// apps/web/app/[locale]/(internal)/admin/reports/page.tsximport { ReportsPage } from '@kit/admin';export default function Page() { return <ReportsPage />;}5. Add to Sidebar
Update the sidebar config in packages/admin/src/admin-sidebar.tsx:
import { FileText } from 'lucide-react';const config = { routes: [ { label: 'Overview', children: [ { label: 'Dashboard', path: '/admin', Icon: <Home className="h-4 w-4" />, end: true, }, ], }, { label: 'Management', children: [ { label: 'Users', path: '/admin/users', Icon: <Users className="h-4 w-4" />, end: true, }, { label: 'Organizations', path: '/admin/organizations', Icon: <Building2 className="h-4 w-4" />, end: true, }, // Add new item { label: 'Reports', path: '/admin/reports', Icon: <FileText className="h-4 w-4" />, end: true, }, ], }, ],};Creating Admin Actions
Follow the existing pattern for admin server actions:
packages/admin/src/reports/lib/actions/reports-server-actions.ts
'use server';import { z } from 'zod';import { revalidatePath } from 'next/cache';import { adminActionClient } from '@kit/action-middleware';import { getLogger } from '@kit/shared/logger';const deleteReportSchema = z.object({ reportId: z.string().min(1),});export const deleteReportAction = adminActionClient .inputSchema(deleteReportSchema) .action(async ({ parsedInput, ctx }) => { const logger = await getLogger(); const { reportId } = parsedInput; const { user } = ctx; try { await reportsService.delete(reportId); logger.info({ reportId, adminId: user.id }, 'Deleted report'); revalidatePath('/admin/reports'); return { success: true }; } catch (error) { logger.error({ error, reportId }, 'Failed to delete report'); throw new Error('Failed to delete report'); } });Using Actions in Components
'use client';import { useAction } from 'next-safe-action/hooks';import { toast } from 'sonner';import { deleteReportAction } from '../lib/actions/reports-server-actions';export function DeleteReportButton({ reportId }: { reportId: string }) { const { execute, status } = useAction(deleteReportAction, { onSuccess: () => { toast.success('Report deleted'); }, onError: ({ error }) => { toast.error(error.serverError ?? 'Failed to delete report'); }, }); return ( <Button variant="destructive" disabled={status === 'executing'} onClick={() => execute({ reportId })} > {status === 'executing' ? 'Deleting...' : 'Delete'} </Button> );}Admin RBAC (Role-Based Access Control)
Admin permissions are configured in packages/rbac/src/admin-rbac.config.ts. By default, there is one role (admin) with full permissions.
Default Resources & Actions
Better Auth Resources (must use exact names):
| Resource | Actions |
|---|---|
| user | create, list, get, update, set-role, set-password, ban, impersonate, delete |
| session | list, revoke, delete |
Custom Resources (not controlled by Better Auth):
| Resource | Actions |
|---|---|
| organizations | list, view |
| dashboard | view |
Note: Better Auth expects singular resource names (user, session) and specific action names (set-role not change-role).
Adding Custom Admin Roles
Edit packages/rbac/src/admin-rbac.config.ts:
import { defineAdminRBACConfig } from './admin/factory';export default defineAdminRBACConfig({ // Add custom roles (higher number = more authority) roles: { support: 50, // Can help users but not delete moderator: 30, // Can ban but limited access }, // Define permissions for each custom role // (admin role gets ALL permissions automatically) permissions: { support: { user: ['list', 'get', 'ban'], // No 'delete', 'set-role' session: ['list', 'revoke'], organizations: ['list', 'view'], dashboard: ['view'], }, moderator: { user: ['list', 'get', 'ban'], dashboard: ['view'], }, },});Adding Custom Resources
export default defineAdminRBACConfig({ // Add custom resources resources: { REPORTS: 'reports', SUBSCRIPTIONS: 'subscriptions', }, // Add custom actions actions: { EXPORT: 'export', REFUND: 'refund', }, // Define which actions are valid for each resource accessController: { reports: ['list', 'view', 'export'], subscriptions: ['list', 'view', 'cancel', 'refund'], }, // Assign permissions to roles permissions: { support: { reports: ['list', 'view'], subscriptions: ['list', 'view'], }, },});Protecting Server Actions with Permissions
Use withAdminPermission middleware:
'use server';import { adminActionClient, withAdminPermission } from '@kit/action-middleware';export const banUserAction = adminActionClient .use(withAdminPermission({ user: ['ban'] })) .inputSchema(banUserSchema) .action(async ({ parsedInput, ctx }) => { // Only executed if user has 'ban' permission on 'user' await banUser(parsedInput.userId); });Client-Side Permission Checks
Use the useAdminPermissions hook to conditionally render UI:
'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> );}Permission Hook API
const { role, // Current user's admin role (or null) isAdmin, // Whether user has 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 for a resource} = useAdminPermissions();// Check multiple permissions (all required)hasPermission({ user: ['ban', 'delete'] });// Check any permission on resourcehasAnyPermission('user'); // true if user has any user permission// Get all user actionsgetActionsFor('user'); // ['list', 'get', 'ban', ...]Legacy: Admin Roles Array
Admin roles are also exported via @kit/better-auth/admin-config:
import { ADMIN_ROLES, isAdminRole } from '@kit/better-auth/admin-config';// Check if a role is an admin roleif (isAdminRole(user.role)) { // User is an admin}Key Imports Reference
| Import | Purpose |
|---|---|
@kit/auth/require-admin | requireAdmin, isUserAdmin for server-side protection |
@kit/action-middleware | adminActionClient for protected actions |
@kit/auth/is-admin-role | isAdminRole for client-safe role checking |
Previous: Overview