Dashboard
Built-in dashboard pages, navigation structure, and how to add new pages.
Built-in Pages
The dashboard is located at app/(dashboard)/dashboard/ and includes these pages out of the box:
| Route | Page | Access |
|---|---|---|
/dashboard | Overview with analytics and charts | All users |
/dashboard/organization | Org management, members, invitations | All users |
/dashboard/settings | Profile settings | All users |
/dashboard/settings/notifications | Notification preferences | All users |
/dashboard/billing | Subscription, plan, payment history | All users |
/dashboard/admin | Admin panel | ADMIN role only |
/dashboard/superadmin | Platform overview | SUPERADMIN role only |
Navigation
Navigation is config-driven in lib/config/navigation.ts. It defines groups with role-based visibility:
export const navigationConfig: NavGroup[] = [
{
heading: "Overview",
items: [
{ label: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
],
},
{
heading: "Settings",
items: [
{ label: "Profile", href: "/dashboard/settings", icon: User },
{ label: "Billing", href: "/dashboard/billing", icon: CreditCard },
{ label: "Notifications", href: "/dashboard/settings/notifications", icon: Bell },
],
},
{
heading: "Admin",
items: [
{ label: "Admin Panel", href: "/dashboard/admin", icon: Shield },
],
roles: ["ADMIN"], // only visible to ADMIN role
},
]The sidebar calls getFilteredNav(user.role) to show only groups the user's role is allowed to see.
Adding a New Dashboard Page
Follow this pattern to add a new page:
Create the page (server component)
// app/(dashboard)/dashboard/my-page/page.tsx
import type { Metadata } from "next"
import { getRequiredSession } from "@/lib/auth-utils"
import { MyContent } from "@/components/core/dashboard/my-page/my-content"
export const metadata: Metadata = { title: "My Page — YourSaaS" }
export default async function MyPage() {
const session = await getRequiredSession()
return <MyContent user={session.user} />
}Create the client component
// components/core/dashboard/my-page/my-content.tsx
"use client"
import { useMyData } from "@/hooks/core/use-my-data"
export function MyContent({ user }: { user: SessionUser }) {
const { data, isLoading } = useMyData()
// Build your UI here
}Add TanStack Query hook
// hooks/core/use-my-data.ts
import { useQuery } from "@tanstack/react-query"
import { api } from "@/lib/api"
import { queryKeys } from "@/lib/query-keys"
export function useMyData() {
return useQuery({
queryKey: queryKeys.myData.list(),
queryFn: () => api.get("/api/my-data"),
})
}Add to navigation
// lib/config/navigation.ts
{ label: "My Page", href: "/dashboard/my-page", icon: SomeIcon }Role-Gated Pages
To restrict a page to admins only:
// page.tsx
import { getAdminSession } from "@/lib/auth-utils"
export default async function AdminPage() {
await getAdminSession() // throws 403 if not ADMIN or SUPERADMIN
return <AdminContent />
}For SUPERADMIN only:
import { getSuperAdminSession } from "@/lib/auth-utils"
export default async function SuperAdminPage() {
await getSuperAdminSession() // throws 403 if not SUPERADMIN
return <SuperAdminContent />
}Dashboard Header
The header (components/core/dashboard/dashboard-header.tsx) includes:
- Breadcrumb — shows current page path
- Search button — opens the
CommandPalette(Cmd+K); hidden whenNEXT_PUBLIC_ENABLE_SEARCH=false - Notification bell — shows unread count badge, opens
NotificationCenter - User dropdown — profile link, settings link, sign out
Notification System
Notifications use the Notification Prisma model:
model Notification {
id String @id @default(cuid())
userId String
type String
title String
message String
read Boolean @default(false)
link String?
createdAt DateTime @default(now())
}Notifications are created server-side via lib/notifications.ts helpers (notify.success, notify.error, notify.warning). The bell icon shows unread count. Users mark notifications as read via PATCH /api/notifications/read.
Dashboard Layout
The layout at app/(dashboard)/layout.tsx wraps all dashboard pages. It renders:
- The collapsible sidebar (desktop + mobile sheet)
- The top header with collapse toggle
- The onboarding wizard (inline, when
onboardingDone === false)
The sidebar collapse state is stored in a cookie and read server-side to avoid layout shift.
Onboarding Wizard
When session.user.onboardingDone === false, the dashboard renders an inline 3-step wizard instead of the page content:
- Profile — set your name and avatar
- Workspace — create or join an organization
- Done — marks
onboardingDone = trueviaPOST /api/user/onboarding-complete
The wizard uses AnimatePresence for step transitions.