This file provides essential information for AI coding agents working on this project. It contains project-specific details, conventions, and guidelines that complement the README.
Next.js Admin Dashboard Starter is a production-ready admin dashboard template built with:
- Framework: Next.js 16 (App Router)
- Language: TypeScript 5.7
- Styling: Tailwind CSS v4
- UI Components: shadcn/ui (New York style)
- Authentication: Clerk (with Organizations/Billing support)
- Error Tracking: Sentry
- Charts: Recharts
- Containerization: Docker (Node.js & Bun Dockerfiles)
- Package Manager: Bun (preferred) or npm
The project follows a feature-based folder structure designed for scalability in SaaS applications, internal tools, and admin panels.
- Next.js 16.0.10 with App Router
- React 19.2.0
- TypeScript 5.7.2 with strict mode enabled
- Tailwind CSS v4 (using
@import 'tailwindcss'syntax) - PostCSS with
@tailwindcss/postcssplugin - shadcn/ui component library (Radix UI primitives)
- CSS custom properties for theming (OKLCH color format)
- Zustand 5.x for local UI state (chat, kanban, notifications)
- Nuqs for URL search params state management
- TanStack Form + Zod for form handling (via
useAppFormhook)
- TanStack React Query for data fetching, caching, and mutations
- Server-side prefetching with
HydrationBoundary+dehydrate - Client-side
useQuery+ nuqsshallow: truefor tables (no RSC round-trips on pagination/filter) useMutation+invalidateQueriesfor form submissions- Query client singleton in
src/lib/query-client.ts
- Clerk for authentication and user management
- Clerk Organizations for multi-tenant workspaces
- Clerk Billing for subscription management (B2B)
- Client-side RBAC for navigation visibility
- TanStack Table for data tables
- TanStack React Query for data fetching and mutations
- Recharts for analytics/charts
- Service layer per feature (
api/types.ts→api/service.ts→api/queries.ts) - Route handlers at
src/app/api/(for Route Handler or BFF patterns) - Mock data in
src/constants/mock-api*.ts(default, swap via service layer) - API client utility in
src/lib/api-client.ts(for fetch-based patterns)
- ESLint 8.x with Next.js core-web-vitals config
- Prettier 3.x with prettier-plugin-tailwindcss
- Husky for git hooks
- lint-staged for pre-commit formatting
/src
├── app/ # Next.js App Router
│ ├── auth/ # Authentication routes (sign-in, sign-up)
│ ├── dashboard/ # Dashboard routes
│ │ ├── overview/ # Parallel routes (@area_stats, @bar_stats, etc.)
│ │ ├── product/ # Product management pages
│ │ ├── kanban/ # Kanban board page
│ │ ├── chat/ # Messaging page
│ │ ├── notifications/ # Notifications page
│ │ ├── workspaces/ # Organization management
│ │ ├── billing/ # Subscription billing
│ │ ├── exclusive/ # Pro plan feature example
│ │ └── profile/ # User profile
│ ├── api/ # API routes (if any)
│ ├── layout.tsx # Root layout with providers
│ ├── page.tsx # Landing page
│ ├── global-error.tsx # Sentry-integrated error boundary
│ └── not-found.tsx # 404 page
│
├── components/
│ ├── ui/ # shadcn/ui components (50+ components)
│ ├── layout/ # Layout components (sidebar, header, etc.)
│ ├── forms/ # Form field wrappers
│ ├── themes/ # Theme system components
│ ├── kbar/ # Command+K search bar
│ ├── icons.tsx # Icon registry
│ └── ...
│
├── features/ # Feature-based modules
│ ├── auth/ # Authentication components
│ ├── overview/ # Dashboard analytics
│ ├── products/ # Product management (React Query + nuqs)
│ │ ├── api/
│ │ │ ├── types.ts # Type contract (response shapes, filters, payloads)
│ │ │ ├── service.ts # Data access layer (swap for your backend)
│ │ │ └── queries.ts # React Query options + key factories
│ │ ├── components/ # Listing, form, table components
│ │ ├── schemas/ # Zod schemas
│ │ └── constants/ # Filter options
│ ├── users/ # User management (React Query + nuqs)
│ │ ├── api/ # Same pattern: types.ts → service.ts → queries.ts
│ │ └── components/ # Listing, table components
│ ├── react-query-demo/ # React Query showcase (Pokemon API)
│ ├── kanban/ # Kanban board with dnd-kit
│ ├── chat/ # Messaging UI (conversations, bubbles, composer)
│ ├── notifications/ # Notification center & store
│ └── profile/ # Profile management
│
├── config/ # Configuration files
│ ├── nav-config.ts # Navigation with RBAC
│ └── ...
│
├── hooks/ # Custom React hooks
│ ├── use-nav.ts # RBAC navigation filtering
│ ├── use-data-table.ts # Data table state
│ └── ...
│
├── lib/ # Utility functions
│ ├── utils.ts # cn() and formatters
│ ├── searchparams.ts # Search param utilities
│ └── ...
│
├── types/ # TypeScript type definitions
│ └── index.ts # Core types (NavItem, etc.)
│
└── styles/ # Global styles
├── globals.css # Tailwind imports + view transitions
├── theme.css # Theme imports
└── themes/ # Individual theme files
/docs # Documentation
│ ├── clerk_setup.md # Clerk configuration guide
│ ├── nav-rbac.md # Navigation RBAC documentation
│ └── themes.md # Theme customization guide
/scripts # Dev tooling
├── cleanup.js # Feature removal (self-contained, delete when done)
└── postinstall.js # Dev server cleanup message (auto-cleans)
Dockerfile # Node.js production Dockerfile
Dockerfile.bun # Bun production Dockerfile
.dockerignore # Docker build exclusions
# Install dependencies
bun install
# Development server
bun run dev # Starts at http://localhost:3000
# Build for production
bun run build
# Start production server
bun run start
# Linting
bun run lint # Run ESLint
bun run lint:fix # Fix ESLint issues and format
bun run lint:strict # Zero warnings tolerance
# Formatting
bun run format # Format with Prettier
bun run format:check # Check formatting
# Git hooks
bun run prepare # Install Husky hooksCopy env.example.txt to .env.local and configure:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
# Redirect URLs
NEXT_PUBLIC_CLERK_SIGN_IN_URL="/auth/sign-in"
NEXT_PUBLIC_CLERK_SIGN_UP_URL="/auth/sign-up"
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL="/dashboard/overview"
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL="/dashboard/overview"NEXT_PUBLIC_SENTRY_DSN=https://...@....ingest.sentry.io/...
NEXT_PUBLIC_SENTRY_ORG=your-org
NEXT_PUBLIC_SENTRY_PROJECT=your-project
SENTRY_AUTH_TOKEN=sntrys_...
NEXT_PUBLIC_SENTRY_DISABLED="false" # Set to "true" to disable in devNote: Clerk supports "keyless mode" - the app works without API keys for initial development.
- Strict mode enabled
- Use explicit return types for public functions
- Prefer interface over type for object definitions
- Use
@/*alias for imports from src
{
"singleQuote": true,
"jsxSingleQuote": true,
"semi": true,
"trailingComma": "none",
"tabWidth": 2,
"arrowParens": "always"
}@typescript-eslint/no-unused-vars: warnno-console: warnreact-hooks/exhaustive-deps: warnimport/no-unresolved: off (handled by TypeScript)
- Use function declarations for components:
function ComponentName() {} - Props interface named
{ComponentName}Props - shadcn/ui components use
cn()utility for class merging - Server components by default,
'use client'only when needed
The project uses a sophisticated multi-theme system with 10 built-in themes:
vercel(default)claudeneobrutualismsupabasemononotebooklight-greenzenastro-vistawhatsapp
- CSS files:
src/styles/themes/{theme-name}.css - Theme registry:
src/components/themes/theme.config.ts - Font config:
src/components/themes/font.config.ts - Active theme provider:
src/components/themes/active-theme.tsx
- Create
src/styles/themes/your-theme.csswith[data-theme='your-theme']selector - Import in
src/styles/theme.css - Add to
THEMESarray insrc/components/themes/theme.config.ts - (Optional) Add fonts in
font.config.ts - (Optional) Set as default in
theme.config.ts
See docs/themes.md for detailed theming guide.
Navigation is organized into groups in src/config/nav-config.ts:
import { NavGroup } from '@/types';
export const navGroups: NavGroup[] = [
{
label: 'Overview',
items: [
{
title: 'Dashboard',
url: '/dashboard/overview',
icon: 'dashboard',
shortcut: ['d', 'd'],
items: [],
access: { requireOrg: true } // RBAC check
}
]
}
];requireOrg: boolean- Requires active organizationpermission: string- Requires specific permissionrole: string- Requires specific roleplan: string- Requires specific subscription planfeature: string- Requires specific feature
The useFilteredNavItems() hook in src/hooks/use-nav.ts filters navigation client-side using Clerk's useOrganization() and useUser() hooks. This is for UX only - actual security checks must happen server-side.
Dashboard routes use Clerk's middleware pattern. Pages that require organization:
import { auth } from '@clerk/nextjs';
import { redirect } from 'next/navigation';
export default async function Page() {
const { orgId } = await auth();
if (!orgId) redirect('/dashboard/workspaces');
// ...
}Use Clerk's <Protect> component for client-side:
import { Protect } from '@clerk/nextjs';
<Protect plan='pro' fallback={<UpgradePrompt />}>
<PremiumContent />
</Protect>;Use has() function for server-side checks:
import { auth } from '@clerk/nextjs';
const { has } = await auth();
const hasFeature = has({ feature: 'premium_access' });Each feature has a three-file API layer:
src/features/<name>/api/
types.ts ← Type contract (response shapes, filters, payloads)
service.ts ← Data access functions (the ONE file to swap for your backend)
queries.ts ← React Query options + query key factories (stable, never changes)
service.ts is the only file you modify when connecting to a real backend. Queries and components import from it — they never change.
| Pattern | How to implement |
|---|---|
| Server Actions + ORM (Prisma/Drizzle/Supabase) | Add 'use server' at top of service.ts, call ORM directly |
| Route Handlers + ORM | service.ts calls /api/ routes via apiClient, route handlers call ORM |
| BFF (Next.js proxies to Laravel/Go/etc.) | service.ts calls /api/ routes via apiClient, route handlers proxy to external backend |
| Direct external API (frontend-only) | service.ts calls external URL via fetch() |
| Mock (default) | service.ts calls in-memory fake data stores |
Route handlers at src/app/api/ are ready for patterns 2 and 3. src/lib/api-client.ts provides a typed fetch wrapper.
Each feature defines a key factory in queries.ts for type-safe, hierarchical cache invalidation:
export const entityKeys = {
all: ['entities'] as const,
list: (filters: EntityFilters) => [...entityKeys.all, 'list', filters] as const,
detail: (id: number) => [...entityKeys.all, 'detail', id] as const
};
// Usage in queryOptions
queryKey: entityKeys.list(filters);
// Usage in mutations — invalidate all entity queries
queryClient.invalidateQueries({ queryKey: entityKeys.all });The project uses TanStack React Query with server-side prefetching and client-side cache management:
- Query options defined in
queries.ts— shared between server prefetch and client hooks - Server prefetch using
void queryClient.prefetchQuery()+HydrationBoundary+dehydrate—void(fire-and-forget) is the standard TanStack pattern for Next.js App Router - Client fetch using
useSuspenseQuery()— integrates with React Suspense so prefetched data streams in without showing a loading skeleton on first load - Suspense boundary wraps the client component — shows a fallback skeleton only on subsequent client-side navigations when cache is empty
// Server component: prefetch + dehydrate
const queryClient = getQueryClient();
void queryClient.prefetchQuery(entitiesQueryOptions(filters)); // void, not await
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Suspense fallback={<Skeleton />}>
<EntityTable />
</Suspense>
</HydrationBoundary>
);
// Client component: useSuspenseQuery (not useQuery)
const { data } = useSuspenseQuery(entitiesQueryOptions(filters));Why void + useSuspenseQuery:
voidfires the prefetch without blocking the server componentuseSuspenseQueryintegrates with React Suspense — the pending query streams in via Next.js streaming SSR- With
<Suspense fallback={<Skeleton />}>: skeleton shows immediately while data streams in — this is expected behavior, the skeleton IS the Suspense fallback during streaming - Without
<Suspense>wrapper: no skeleton, but the previous page stays visible until data fully resolves (feels like a slow navigation) - Once data is cached (within
staleTime), subsequent visits are instant — no skeleton
Why NOT useQuery:
useQuerydoesn't integrate with Suspense — returnsisLoading: trueand you must handle loading state manually- Hydrated pending queries from
voidprefetch won't prevent the loading state - Results in skeleton flash even when data is prefetched
Components import service functions for mutations. Use query key factories for invalidation:
import { createEntity } from '../api/service';
import { entityKeys } from '../api/queries';
const mutation = useMutation({
mutationFn: (data) => createEntity(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: entityKeys.all });
toast.success('Created');
}
});Use nuqs for search params state:
searchParamsCache(server) — reads params in server componentsuseQueryState(client) — reads/writes params in client components withshallow: true
Tables use TanStack Table with React Query:
- Query options in
features/*/api/queries.ts - Column definitions in
features/*/components/*-tables/columns.tsx - Table component in
src/components/ui/table/data-table.tsx - Column pinning via
initialState.columnPinninginuseDataTable
Sentry is configured for both client and server:
- Client config:
src/instrumentation-client.ts - Server config:
src/instrumentation.ts - Global error:
src/app/global-error.tsx
To disable Sentry in development:
NEXT_PUBLIC_SENTRY_DISABLED="true"global-error.tsx- Catches all errors, reports to Sentry- Parallel route
error.tsxfiles for specific sections
Note: This project does not include a test suite by default. Consider adding:
- Unit tests: Vitest or Jest for utilities and hooks
- Component tests: React Testing Library for UI components
- E2E tests: Playwright for critical user flows
Recommended test locations:
/src
/__tests__ # Unit tests
/features/*/tests # Feature tests
/e2e # Playwright tests
- Connect repository to Vercel
- Add environment variables in dashboard
- Deploy
Ensure these are set in your deployment platform:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYCLERK_SECRET_KEY- All
NEXT_PUBLIC_*variables for client-side access SENTRY_*variables if using error tracking
Production-ready Dockerfiles are included:
Dockerfile— Node.js-basedDockerfile.bun— Bun-based
Both use output: 'standalone' in next.config.ts. Pass NEXT_PUBLIC_* vars as --build-arg at build time, and runtime secrets via -e at run time.
- Output:
standalone(optimized for Docker/self-hosting) - Images: Configured for
api.slingacademy.com,img.clerk.com,clerk.com - Sentry source maps uploaded automatically in CI
A single scripts/cleanup.js file handles removal of optional features:
# Interactive mode — prompts for each feature
node scripts/cleanup.js --interactive
# Remove specific features
node scripts/cleanup.js clerk # Remove auth/org/billing
node scripts/cleanup.js kanban # Remove kanban board
node scripts/cleanup.js chat # Remove messaging UI
node scripts/cleanup.js notifications # Remove notification center
node scripts/cleanup.js themes # Keep one theme, remove rest
node scripts/cleanup.js sentry # Remove error tracking
# Remove multiple at once
node scripts/cleanup.js kanban chat notifications
# Preview without changing files
node scripts/cleanup.js --dry-run kanban
# List all features
node scripts/cleanup.js --listSafety: Script requires git repository with at least one commit. Use --force to skip.
After cleanup, delete scripts/cleanup.js — the dev server message auto-cleans on next start.
All icons come from a single source: src/components/icons.tsx.
The project uses @tabler/icons-react as the sole icon package. Every icon is re-exported through a centralized Icons object — never import directly from @tabler/icons-react or any other icon package.
import { Icons } from '@/components/icons';
// In JSX
<Icons.search className='h-4 w-4' />
<Icons.chevronRight className='h-4 w-4' />
// Passing as a prop
icon={Icons.check}- Import the tabler icon in
src/components/icons.tsx - Add a semantic key to the
Iconsobject - Use
Icons.yourKeyeverywhere — never the raw import
// In src/components/icons.tsx
import { IconNewIcon } from '@tabler/icons-react';
export const Icons = {
// ...existing icons
newIcon: IconNewIcon
};| Category | Example Keys |
|---|---|
| General | check, close, search, settings, trash, spinner, info, warning |
| Navigation | chevronDown, chevronLeft, chevronRight, chevronUp, chevronsUpDown |
| Layout | dashboard, kanban, panelLeft |
| User | user, account, profile, teams |
| Communication | chat, notification, phone, video, send |
| Files | page, post, media, fileTypePdf, fileTypeDoc |
| Actions | add, edit, upload, share, login, logout |
| Theme | sun, moon, brightness, laptop, palette |
| Text formatting | bold, italic, underline, text |
| Data / Charts | trendingUp, trendingDown, eyeOff, adjustments |
Browse all available icons at /dashboard/elements/icons — a searchable grid of every icon in the registry.
- Single source of truth — swap icon packages by editing one file
- Semantic naming —
Icons.trashis clearer thanIconTrashscattered across files - Discoverability — autocomplete on
Icons.shows every available icon - No direct dependencies — components never couple to a specific icon package
- Create
src/features/<name>/api/types.ts— response types, filter types, mutation payloads - Create
src/features/<name>/api/service.ts— data access functions (mock by default) - Create
src/features/<name>/api/queries.ts— query key factory +queryOptions - Create page route:
src/app/dashboard/<name>/page.tsx - Create feature components in
src/features/<name>/components/ - Add navigation item in
src/config/nav-config.ts - (Optional) Add route handlers in
src/app/api/<name>/for REST API patterns - (Optional) Register new icon in
src/components/icons.tsx
- Create:
src/app/api/my-route/route.ts - Export HTTP method handlers:
GET,POST, etc. - For BFF pattern: proxy requests to your external backend
npx shadcn add component-nameSee "Theming System" section above or docs/themes.md.
Build fails with Tailwind errors
- Ensure using Tailwind CSS v4 syntax (
@import 'tailwindcss') - Check
postcss.config.jsuses@tailwindcss/postcss
Clerk keyless mode popup
- Normal in development without API keys
- Click popup to claim application or set env variables
Theme not applying
- Check theme name matches in CSS
[data-theme]andtheme.config.ts - Verify theme CSS is imported in
theme.css
Navigation items not showing
- Check
accessproperty in nav config - Verify user has required org/permission/role
- Always use
cn()for className merging - never concatenate strings manually - Respect the feature-based structure - put new feature code in
src/features/ - Server components by default - only add
'use client'when using browser APIs or React hooks - Type safety first - avoid
any, prefer explicit types - Follow existing patterns - look at similar components before creating new ones
- Environment variables - prefix with
NEXT_PUBLIC_for client-side access - shadcn components - don't modify files in
src/components/ui/directly; extend them instead - Icons - NEVER import icons directly from
@tabler/icons-reactor any other icon package. All icons must be registered insrc/components/icons.tsxand imported asimport { Icons } from '@/components/icons'. To add a new icon: add the tabler import toicons.tsx, add a semantic key to theIconsobject, then useIcons.keyNamein your component. - Page headers - Always use
PageContainerprops (pageTitle,pageDescription,pageHeaderAction) for page headers. Never import<Heading>manually in pages —PageContainerhandles that internally. - Forms - Use TanStack Form via
useAppFormfrom@/components/ui/tanstack-form. Never useuseStateinsideAppFieldrender props — extract stateful logic into separate components. - Button loading - Use
<Button isLoading={isPending}>for loading states. Uses CSS Grid overlap trick for zero layout shift. WhenisLoadingis not passed, button behaves as default shadcn.SubmitButtonin forms handles this automatically via formisSubmittingstate. - Data layer - Always go through the service layer:
types.ts→service.ts→queries.ts. Components import types fromtypes.ts, functions fromservice.ts, query options fromqueries.ts. Never import from@/constants/mock-api*directly in components.