Dashboard Layout
How the dashboard layout is built: sidebar navigation, header, breadcrumb, mobile menu, and past-due banner, plus adding nav items and gating them to admins.
The shell
Every route under /dashboard is wrapped by app/dashboard/layout.tsx. It's a Server Component that guards access first, then renders the shell around your page.
The guard runs before anything else:
getSession()reads the current session. No session redirects to/auth/sign-in.- If
session.user.onboardingCompleteis false, it redirects to/onboarding.
The file also exports a static metadata built with generateMeta({ noIndex: true, ... }), so search engines skip dashboard pages.
Once the guard passes, the layout renders these pieces:
<SubscriptionProvider>
<SidebarProvider>
<DashboardSidebar session={session} />
<SidebarInset>
<header>
<SidebarTrigger />
<DashboardBreadcrumb /> {/* desktop only */}
<DashboardMobileHeader user={...} /> {/* mobile only */}
</header>
<PastDueBanner />
<main>{children}</main>
</SidebarInset>
</SidebarProvider>
</SubscriptionProvider>| Piece | What it does |
|---|---|
SubscriptionProvider | Loads subscription state (planName, activeSubscription) for the sidebar footer and the past-due banner |
SidebarProvider | Holds the open/closed state, saves it to a cookie, and binds the Cmd/Ctrl+B shortcut |
DashboardSidebar | The nav rail (components/dashboard/sidebar.tsx) |
SidebarInset | The main content column. It sits beside the sidebar on desktop and goes full-width on mobile |
SidebarTrigger | Toggles the sidebar (collapse on desktop, slide-in sheet on mobile) |
DashboardBreadcrumb | The trail at the top, desktop only |
DashboardMobileHeader | A centered logo plus a menu button, mobile only |
PastDueBanner | A warning bar shown only when the subscription status is past_due |
The sidebar primitives in components/ui/sidebar.tsx are stock shadcn/ui. SidebarProvider defaults to defaultOpen={true}. It saves the open state to a sidebar_state cookie, so the sidebar stays the way you left it after a reload.
Navigation
Nav items live in one typed array, navigationItems, at the top of components/dashboard/sidebar.tsx. (The public marketing nav is separate. It lives inline in components/header.tsx.)
| Item | Route | Icon | Group | Admin-only |
|---|---|---|---|---|
| Dashboard | /dashboard | Home | main | No |
| Files | /dashboard/files | Files | system | Yes |
| Users | /dashboard/users | Users | system | Yes |
| Billing | /dashboard/billing | CreditCard | secondary | No |
| Settings | /dashboard/settings | Settings | secondary | No |
Items render in three groups, based on each item's group field:
| Group | Position | Label | Who sees it |
|---|---|---|---|
main | Top | (none) | Everyone |
system | Middle | "SYSTEM" | Admins only |
secondary | Bottom (mt-auto) | (none) | Everyone |
The system group is filled in only when session.user.role === "admin". For everyone else it's an empty array, so the labeled group doesn't render at all.
Active state
The sidebar highlights one item based on the current path:
/dashboardis active only on an exact match (pathname === "/dashboard").- Any other item is active when the path equals its
href, or starts with itshrefplus a slash. So/dashboard/usersstays highlighted on a nested route like/dashboard/users/abc-123.
Footer dropdown
The footer is a DropdownMenu. Its trigger button shows the user's avatar, their display name, and {planName} Plan below the name. The plan name comes from SubscriptionProvider. The display name is user.name, or the part of the email before the @ if there is no name.
Opening the menu shows:
- The avatar, name, and email again at the top
- Profile →
/dashboard/profile - A theme toggle (the
ThemeTogglecomponent) - Sign out → calls
signOut(), then redirects to/auth/sign-in
While SubscriptionProvider is still loading, the sidebar content and footer render as skeletons (gray placeholders) instead.
The sidebar header (above the nav) holds the app logo linking to /dashboard, and an ExternalLink icon linking to / ("View homepage").
Breadcrumb
components/dashboard/breadcrumb.tsx builds the trail from usePathname(). There's no manual config to keep in sync; it reads the current URL.
/dashboard -> Dashboard (not a link)
/dashboard/settings -> Dashboard > Settings
/dashboard/users/<uuid> -> Dashboard > Users (UUID dropped)The rules:
- At
/dashboard, it shows a single non-clickableDashboardlabel. - Deeper routes always start with a
Dashboardlink. Thedashboardsegment is filtered out of the rest of the path, so it is never repeated. - UUID segments are filtered out, so resource IDs never show in the trail.
- Each remaining segment is title-cased (hyphens become spaces). The last segment is plain text; earlier ones are links.
The breadcrumb is desktop-only (hidden md:flex).
Mobile
Below the md breakpoint the shell changes:
- The sidebar collapses to a slide-in Sheet (18rem wide), opened by
SidebarTrigger. - The header hides the breadcrumb and shows
DashboardMobileHeaderinstead: a centered logo, plus aMobileNavmenu button on the right. - Tapping any sidebar link closes the sheet. The handler only fires on mobile:
const { setOpenMobile, isMobile } = useSidebar();
const handleMobileClose = () => isMobile && setOpenMobile(false);What you can change
Add a nav item
-
Add one entry to
navigationItemsincomponents/dashboard/sidebar.tsx. Give it alucide-reacticon and agroup:{ name: "Analytics", href: "/dashboard/analytics", icon: BarChart, group: "main" }, -
Create the page at
app/dashboard/analytics/page.tsx. The active highlight works on its own.
Make a link admin-only
Set its group to "system" so it hides for non-admins in the sidebar.
The sidebar is a UI convenience only. It does not protect the page. Enforce access on the server by putting the page inside the app/dashboard/(admin)/ route group. That group's layout.tsx re-checks the session and redirects anyone who isn't an admin to /dashboard. The (admin) parentheses keep that segment out of the URL, so the page still resolves to /dashboard/<name>. The built-in files and users pages already live there.
Change the header
Edit the <header> block in app/dashboard/layout.tsx to add controls next to SidebarTrigger, or to swap out DashboardBreadcrumb. To change what the breadcrumb shows (for example, custom labels), edit components/dashboard/breadcrumb.tsx.
Add a section-only layout
Drop a layout.tsx into any dashboard subfolder (for example app/dashboard/profile/layout.tsx) to add tabs or a sub-nav scoped to that section. It nests inside the outer shell, so you don't need a session check. The parent layout already guarded the route.
File Uploads
How Next Starter handles file uploads: direct browser-to-Cloudflare R2 transfers via presigned URLs, with server-side validation and an S3-compatible config.
UI Components
The 30 shadcn/ui components pre-installed in Next Starter: new-york style on Radix primitives, styled with Tailwind CSS v4 and themed with CSS variables.