Skip to Content
Welcome to the official DBS Documentation! 📚
AdvancedSidebar Navigation

Dynamic Sidebar Navigation

The sidebar in DBS is fully database-driven. Instead of hardcoding routes in a React component, the sidebar fetches its structure from GET /api/menus — which filters menu items based on the current user’s permissions before returning the list.

Why Database-Driven?

Without a dynamic sidebar, adding a new module requires editing the sidebar component’s JSX directly. With RBAC in the mix, that code quickly becomes a maze of conditional checks.

The database-driven approach means:

  • Adding a new page = adding one record to the Menu table (via seeding or admin UI)
  • Permission filtering happens on the server — users only receive the items they’re allowed to see
  • No sidebar frontend code changes needed for new modules

Data Flow

1. Dashboard layout mounts 2. Sidebar component calls GET /api/menus 3. Server reads current user's permissions from session 4. Server filters Menu table: only items where user has requiredPermission 5. Returns { groups: [...menus] } JSON 6. Sidebar renders the filtered list

The Menu Data Model

// Prisma schema (simplified) model Menu { id Int @id @default(autoincrement()) title String url String icon String // Lucide icon name (e.g., "Users") order Int @default(0) requiredPermission String? // null = always visible groupLabel String? // Optional section header parentId Int? // For nested menus parent Menu? @relation("MenuChildren", fields: [parentId], references: [id]) children Menu[] @relation("MenuChildren") }

API Endpoint

// src/app/api/menus/route.ts import { apiGuard } from '@/lib/api-guard'; import { prisma } from '@/lib/prisma'; export async function GET() { const guard = await apiGuard(); // session only, no specific permission if (guard.error) return guard.error; const { session } = guard; const userPermissions = session.user.permissions as string[]; const isSuperAdmin = session.user.roles?.includes('super_admin'); const allMenus = await prisma.menu.findMany({ orderBy: { order: 'asc' }, where: { parentId: null }, // top-level items only include: { children: { orderBy: { order: 'asc' } } }, }); // Filter by permission const filtered = allMenus.filter(menu => { if (!menu.requiredPermission) return true; if (isSuperAdmin) return true; return userPermissions.includes(menu.requiredPermission); }); return Response.json({ menus: filtered }); }

Adding a New Menu Item

Add the entry to prisma/seed.ts

await prisma.menu.upsert({ where: { url: '/dashboard/reports' }, update: {}, create: { title: 'Reports', url: '/dashboard/reports', icon: 'BarChart2', // lucide-react icon name order: 8, requiredPermission: 'reports.read', }, });

Re-seed or use the admin UI

npm run db:seed

Create the corresponding page

src/app/dashboard/reports/page.tsx

Icon Resolution

The icon field in the database is a string. The sidebar component dynamically resolves it to a Lucide icon:

// src/components/dashboard/Sidebar.tsx (excerpt) import * as LucideIcons from 'lucide-react'; function SidebarIcon({ name }: { name: string }) { const Icon = LucideIcons[name as keyof typeof LucideIcons] as LucideIcons.LucideIcon; if (!Icon) return <LucideIcons.Circle />; return <Icon className="h-4 w-4" />; }

Browse all available icon names at lucide.dev/icons . Use the exact PascalCase name shown (e.g., BarChart2, ShieldCheck, UsersRound).

Nested Menu Groups

For grouped sidebar sections (e.g., “Management” containing Users and Teams), use the groupLabel or parentId fields:

// A group header (no URL, just a label) { title: 'Management', url: '#', icon: 'Folder', order: 2 } // Children reference the parent { title: 'Users', url: '/dashboard/users', parentId: managementMenuId, order: 1 } { title: 'Teams', url: '/dashboard/teams', parentId: managementMenuId, order: 2 }
Last updated on