The boilerplate includes two admin dashboards: a platform-wide super admin and an organization-level admin for B2B workspaces.
Platform Admin Dashboard
Located at /admin-dashboard, accessible only to users with is_admin=true in their profile.
Platform admin dashboard with KPIs and overview
Organizations management
Organization detail view
Subscriptions overview
Platform-wide AI analytics
Platform settings configuration
Auto-Admin Assignment
The admin check uses the shared maybeAutoPromoteAdmin() helper so layout and API admin verification follow the same auto-fix path:
- Compares user email with
admin_emailinapp_settingstable - If email matches but
is_admin=false, automatically updates tois_admin=true - Works in both layout (server-side) and API routes without duplicating the update/audit logic
- Uses
supabaseAdmin(service role) to bypass RLS
Admin dashboard pages are thin Server Components. Platform-wide reads for the overview, analytics, and settings pages live in core/admin/overview.ts, core/admin/analytics.ts, and core/admin/settings.ts. Destructive admin routes delegate to focused core action modules such as core/affiliates/admin-actions.ts, which keeps audit logging and cache revalidation reusable outside the HTTP route.
Dashboard Pages
| Page | Path | Features |
|---|---|---|
| Overview | /admin-dashboard |
Total users, orgs, active subscriptions, MRR/ARR, AI stats |
| Analytics | /admin-dashboard/analytics |
AI requests (total/30d/7d/today), tokens, costs, charts, top users |
| Users | /admin-dashboard/users |
User management, disable/enable, view details |
| Organizations | /admin-dashboard/organizations |
Workspace management, member limits, subscription status |
| Subscriptions | /admin-dashboard/subscriptions |
All subscriptions, plan breakdown, status filters |
| Referrals | /admin-dashboard/referrals |
Referral monitoring, stats, manual reverse/reject (gated by REFERRAL_ENABLED) |
| Affiliates | /admin-dashboard/affiliates |
Affiliate applications queue, conversions, manual reversal (gated by AFFILIATES_ENABLED) |
| Logs | /admin-dashboard/logs |
Server-side error logs, cursor-paginated, category/level filters (gated by LOGS_ENABLED) |
| Licenses | /admin-dashboard/licenses |
License management, revenue, expiration tracking, revoke/extend actions |
| Changelog | /admin-dashboard/changelog |
Create, edit, publish/unpublish changelog entries. Multi-locale support. Toggle via appConfig.features.changelog |
| Roles | /admin-dashboard/roles |
Dynamic role management, permissions, colors, icons |
| CMS | /admin-dashboard/cms |
Pages, blocks, media library management |
| Jobs | /admin-dashboard/jobs |
Background jobs, handlers, run history |
| Settings | /admin-dashboard/settings |
Platform settings, cache management, environment status, database stats, log purging |
KPI Performance
Dashboard queries are optimized for performance:
- Server Component pages render pre-aggregated data from
core/admin/*modules instead of inlining service-role query orchestration - Parallel database queries with
Promise.all(9 queries in parallel) - MRR/ARR calculation from
pricingConfig(code-based, not DB) - Efficient counting with
count: 'exact', head: true - Explicit projections such as
select('id')orselect('amount')instead ofselect('*')for production queries supabaseAdminbypasses RLS for platform-wide stats
Admin Dashboard Design System
The admin dashboard follows a Data-Dense Dashboard design pattern optimized for information density and at-a-glance insights. The design uses shadcn/ui components with Tailwind CSS v4 theme tokens.
The first two rows below describe shared components, not classes to copy. StatCard and PageHeader (components/patterns/) exist precisely because these blocks were hand-rolled dozens of times and drifted. Copying the class strings recreates the drift — import the component instead.
| Element | Pattern | Example Classes |
|---|---|---|
| Stat/KPI Cards | Always <StatCard>. Its surface prop says what the card sits ON: canvas (default — admin and org page shells) lifts; panel recesses, required inside the private/org floating panel where a bg-card card on a bg-card panel is invisible. |
canvas: border bg-card hover:shadow-cardpanel: border-0 bg-muted/60 hover:bg-muted |
| Card Icons | Tinted chip, accent restricted to the chart ramp (chart-1…chart-5). Not primary (a KPI row would read as one undifferentiated block) and not success/warning (that implies a status the metric does not have). Never a raw palette colour — blue-500 does not follow the theme and survives no rebrand. |
h-8 w-8 rounded-lg bg-chart-1/10 text-chart-1 |
| KPI Values | Rendered by StatCard. Every numeral in a dashboard uses tabular-nums — proportional digits make a column jitter as values update, which is one of the clearest tells of unconsidered dashboard work. |
text-2xl font-semibold tabular-nums |
| Supporting stats | Small muted line under the value (hint prop) |
mt-1 text-xs text-muted-foreground |
| Table Containers | Clean card without colored accent | bg-card border border-border |
| Table Headers | Subtle muted background for scanning | TableHeader className="bg-muted/50" |
| Table Rows | Hover highlight for interactivity | hover:bg-muted/50 transition-colors |
| Page Headers | Always <PageHeader> (title + optional description + action row). It owns the page’s only <h1>; no gradients. |
text-3xl font-bold tracking-tight text-foreground |
| Sidebar Active Item | One shared <SidebarNavLink> with a variant per surface — the three active states differ on purpose (identity, not drift), and sharing the component is what keeps the collapsed-rail aria-label from going missing on one of them. |
admin: bg-primary/10 text-primary border-l-2 border-l-primaryorg: bg-primary text-primary-foregroundprivate: bg-card text-foreground shadow-raise |
| Dashboard shell | Rail + app bar + scrolling content panel. The bar (h-16) owns the mobile drawer trigger, the surface identity, and then notifications / theme toggle / account dropdown. The panel is full-bleed on mobile and insets into a rounded, raised sheet once the rail appears. |
<main>: min-h-0 flex-1 overflow-y-auto bg-card lg:my-3 lg:mr-3 lg:rounded-xl lg:shadow-card |
| Separation | By elevation and surface, not by borders. The page canvas is deliberately recessed below --card, so a panel is told apart from the page by depth. Rails draw no internal rules. The one deliberate exception is a data-table row rule, which carries scanability. |
--background vs --card = 1.195 (light) / 1.136 (dark) |
When designing pages, Claude Code follows a 4-tier skill pipeline: (1) Foundation skills (/tailwind-v4-shadcn + /ui-ux-pro-max, plus /shadcn for component work) are always invoked, (2) ONE aesthetic skill sets the visual direction (e.g., /impeccable), (3) Refinement commands (/impeccable typeset, /impeccable animate, etc.) polish specific aspects, (4) Workflow commands (/impeccable critique, /impeccable polish) handle review and shipping. Since Impeccable v4 the refinement and workflow steps are subcommands of /impeccable, not standalone skills. All colors must use oklch() theme tokens — never raw hex. See .claude/rules/design-skills.md for the full orchestration guide.
Organization Admin Dashboard
Located at /org-dashboard, accessible to workspace owners and admins.
Organization dashboard overview
Team members management
Member detail with usage stats
AI usage analytics
| Page | Path | Features |
|---|---|---|
| Admin | /org-dashboard/admin |
Workspace admin overview / management |
| Members | /org-dashboard/members |
Team list with pagination, invite form, pending invitations |
| Member Detail | /org-dashboard/members/[id] |
Usage stats, permissions, AI logs, access logs with pagination |
| Roles | /org-dashboard/members/roles |
Role overview, permission matrix |
| Billing | /org-dashboard/billing |
Credits balance, credit history, Stripe portal |
| Settings | /org-dashboard/settings |
Org name/slug, danger zone (deletion) |
| API Keys | /org-dashboard/api-keys |
B2B API key management (create/revoke, scoped) |
| Analytics | /org-dashboard/analytics |
Workspace AI usage analytics |
Pagination
Tables include reusable pagination controls:
- Rows per page selector (5, 10, 20, 50)
- Page navigation (first, previous, next, last)
- "Showing X to Y of Z entries" display
- Page indicator (Page X of Y)
- Independent pagination state per table
Shared Dashboard Context
The private and organization dashboards share request-level context helpers in core/accounts/dashboard-context.ts. They use React.cache() to deduplicate repeated auth/account reads between a layout and its child page during one request.
| Helper | Route family | Context |
|---|---|---|
getPrivateDashboardContext() |
/private-dashboard/* |
Current user, profile, memberships, pending invitation token, active account, billing access, credits, trial state, admin/org-management flags. |
getOrgDashboardContext() |
/org-dashboard/* |
Current user, profile, manageable workspaces, current workspace, member count, active subscription, billing access, trial/Stripe state. |
These helpers are not static caches. They read the current request session with getUser(), so they remain appropriate only for protected dynamic routes. Layouts and pages still own redirects: unauthenticated users go to login, users without org-manager membership go back to /private-dashboard, and B2B workspaces without access go to pricing.
Organization Dashboard Performance
Organization dashboard analytics and member usage stats are pre-aggregated in SQL instead of loading raw usage rows into the page component. Keep this boundary when adding new dashboard cards:
core/accounts/dashboard-usage.tswraps the aggregate readers used by the private dashboard overview, org overview, org analytics, member list, and member detail pages.get_org_dashboard_analyticsreturns 30-day KPIs, previous-period comparisons, daily usage, top members, model/agent breakdowns, active users, latency, and success rate for/org-dashboard/analytics.get_org_member_usage_summaryreturns per-member request count, token total, cost, last AI activity, and chat-session count for member list/detail screens.get_account_dashboard_usage_summarypowers lightweight overview counters without downloadingai_requestsorchat_sessions.- Supporting indexes cover
ai_requests(account_id, user_id, created_at desc),chat_sessions(account_id, user_id, created_at desc), anduser_access_logs(user_id, created_at desc).
Do not add dashboard code that fetches all ai_requests or chat_sessions for an account and then groups them in JavaScript. Use an aggregate RPC, a bounded latest-activity query, or a head: true count query depending on the UI.