ShipVeryFastShipVeryFast
Documentation

User dashboard

ShipVeryFast ships a complete authenticated dashboard under app/dashboard/, nine routes, a sidebar/topbar shell, a command palette and a single nav config, wired to deterministic demo data you can swap for your own Supabase queries. This page maps it out and shows how to extend it.

Mental model

Every page is a 'use client' component that reads static demo data from libs/dashboard/demo-data.ts. The navigation, breadcrumbs and command palette are all driven by one file, libs/dashboard/nav.ts, so adding a route is a two-line change.

Route map

The dashboard lives entirely under app/dashboard/. A shared app/dashboard/layout.tsx wraps every page with the sidebar, topbar, mobile drawer, command palette and toaster, then renders each route inside a <main>:

RouteFileWhat it shows
/dashboardapp/dashboard/page.tsxOverview: KPI stat cards, revenue chart, activity feed, plan card
/dashboard/analyticsapp/dashboard/analytics/page.tsxTraffic, sources, cohorts, funnel, geo breakdown
/dashboard/assistantapp/dashboard/assistant/page.tsxStreaming AI chat (provider/model picker)
/dashboard/customersapp/dashboard/customers/page.tsxCustomer table with status, plan, MRR and LTV
/dashboard/billingapp/dashboard/billing/page.tsxCurrent plan, invoices, MRR movement, metered usage
/dashboard/teamapp/dashboard/team/page.tsxTeam members, roles and pending invites
/dashboard/api-keysapp/dashboard/api-keys/page.tsxAPI keys with masked secrets, scopes and usage
/dashboard/notificationsapp/dashboard/notifications/page.tsxNotification feed with read/unread state
/dashboard/settingsapp/dashboard/settings/page.tsxAccount and workspace settings

Navigation config (nav.ts)

One file, libs/dashboard/nav.ts, is the single source of truth for the sidebar, the mobile drawer, the command palette and the breadcrumbs. It exports NAV_GROUPS (the grouped sidebar items, each with a lucide-react icon), ALL_NAV (a flattened list the CommandPalette uses), ROUTE_LABELS (path-segment → human label for breadcrumbs) and the isActive helper.

import {
  LayoutDashboard, BarChart3, Users, CreditCard, UsersRound,
  KeyRound, Bell, Settings, Sparkles, type LucideIcon,
} from 'lucide-react';

export interface NavItem {
  href: string;
  label: string;
  icon: LucideIcon;
}

export const NAV_GROUPS: { title: string; items: NavItem[] }[] = [
  {
    title: 'Workspace',
    items: [
      { href: '/dashboard', label: 'Overview', icon: LayoutDashboard },
      { href: '/dashboard/analytics', label: 'Analytics', icon: BarChart3 },
      { href: '/dashboard/assistant', label: 'Assistant', icon: Sparkles },
      { href: '/dashboard/customers', label: 'Customers', icon: Users },
      { href: '/dashboard/billing', label: 'Billing', icon: CreditCard },
    ],
  },
  {
    title: 'Account',
    items: [
      { href: '/dashboard/team', label: 'Team', icon: UsersRound },
      { href: '/dashboard/api-keys', label: 'API keys', icon: KeyRound },
      { href: '/dashboard/notifications', label: 'Notifications', icon: Bell },
      { href: '/dashboard/settings', label: 'Settings', icon: Settings },
    ],
  },
];

export const ALL_NAV: NavItem[] = NAV_GROUPS.flatMap((g) => g.items);

export function isActive(pathname: string, href: string): boolean {
  return href === '/dashboard'
    ? pathname === '/dashboard'
    : pathname.startsWith(href);
}

DashboardSidebar and MobileDrawer both render NAV_GROUPS and call isActive to highlight the current page; CommandPalette maps over ALL_NAV; and Breadcrumbs looks each path segment up in ROUTE_LABELS.

State & demo data

Two libs back every page. libs/dashboard/store.ts holds the shared client state as Zustand stores, and libs/dashboard/demo-data.ts holds the static seed data the pages render. There is also libs/dashboard/chart-theme.ts, a single CHART palette and a set of shared recharts props so every chart reads as one family on both light and dark backgrounds.

Zustand stores (store.ts)

The store exposes four hooks:

  • useFilters, the analytics period (7d / 30d / 90d / 12m) and compare mode, shared so every chart re-renders together.
  • useShell, shell UI state: sidebarCollapsed, mobileOpen and paletteOpen with their setters.
  • useToasts, a toast() dispatcher and the rendered toasts array consumed by Toaster.
  • useNotifications, notification items (seeded from the demo data) with markRead and markAllRead.

Deterministic demo data (demo-data.ts)

The demo data is intentionally deterministic: it anchors "now" to a fixed NOW constant and uses a seeded PRNG so charts never jitter between renders and relative timestamps never cause hydration mismatches. It exports ready-made datasets, KPIS, revenueSeries, trafficSeries, customers, invoices, mrrMovement, team, apiKeys, activity, notifications, plus formatters (formatCurrency, formatNumber, formatPct, relativeTime).

import { KPIS, revenueSeries, activity, currentPlan } from '@/libs/dashboard/demo-data';
import { useFilters } from '@/libs/dashboard/store';

export default function OverviewPage() {
  const { period, compare } = useFilters();
  const data = revenueSeries.slice(-PERIOD_DAYS[period]);
  // ...render StatCards, charts and the activity feed from the seed data
}

How pages are gated

Access control lives in middleware.ts, which is wrapped in NextAuth's withAuth (its authorized callback requires a JWT for any matched route). The matcher protects /admin/:path* and a few other paths.

Heads up: the dashboard guard ships disabled for local preview

In the boilerplate, the /dashboard/:path* entry in the middleware.ts matcher is commented out so you can browse the dashboard without logging in. Uncomment it before you ship so the routes require a session:

// middleware.ts
export const config = {
  matcher: [
    '/admin/:path*',          // Protect all admin routes (require authentication)
    // LOCAL-PREVIEW: /dashboard guard disabled for browsing without login.
    // Uncomment the line below before shipping:
    // '/dashboard/:path*',
    '/app/dashboard/:path*',
    '/app/profile/:path*',
  ],
};

Note that the dashboard pages themselves are all 'use client' components and do not call getServerSession, gating is handled by the middleware matcher, not per-page. See Authentication for how sessions are read on the server elsewhere.

Adding a new dashboard page

Because nav.ts drives the whole shell, adding a route is three small steps. Say you want a /dashboard/reports page:

  1. Create the route file app/dashboard/reports/page.tsx, it will inherit the sidebar/topbar shell from app/dashboard/layout.tsx automatically.
  2. Add a nav item to the right group in NAV_GROUPS (pick any lucide-react icon).
  3. Add a breadcrumb label to ROUTE_LABELS so the topbar reads nicely.
// 1. app/dashboard/reports/page.tsx
'use client';

export default function ReportsPage() {
  return (
    <div className="mx-auto max-w-4xl space-y-6">
      <h1 className="font-display text-2xl">Reports</h1>
      {/* ...your content */}
    </div>
  );
}

// 2. libs/dashboard/nav.ts, add to the Workspace group's items:
import { FileBarChart } from 'lucide-react';
// { href: '/dashboard/reports', label: 'Reports', icon: FileBarChart },

// 3. libs/dashboard/nav.ts, add a breadcrumb label:
// reports: 'Reports',

The new entry shows up in the sidebar, the mobile drawer and the command palette with no further wiring, and isActivehighlights it when you're on the page.

Swapping demo data for real queries

Every page imports its data from libs/dashboard/demo-data.ts, so that module is the one seam to replace. The file's own comment says it plainly: "Swap this module for your real data source." Replace the exported arrays/objects with live queries against your Supabase tables using the clients from libs/supabase.ts, the supabase anon client on the client side, or supabaseAdmin from a Server Component or route handler.

// Example: fetch real customers in a Server Component wrapper,
// then pass them into a client child that renders the table.
import { supabaseAdmin } from '@/libs/supabase';

export async function getCustomers() {
  const { data, error } = await supabaseAdmin
    .from('customers')
    .select('id, name, email, plan, status, mrr');
  if (error) throw error;
  return data;
}

Keep the same shapes the demo data uses (the Customer, Kpi and other types are exported from demo-data.ts) and the existing components and charts keep working unchanged. See Database & data models for the Supabase clients and Zod schemas.

Next steps