next-shadcn-dashboard-starter Documentation

Free, open source, AI-friendly admin dashboard template built with Next.js 16, shadcn/ui, Tailwind CSS, and TypeScript. Production-ready tables, forms,...

RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy next-shadcn-dashboard-starter on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name next-shadcn-dashboard-starter -p 3000:3000 --restart always kiranism/next-shadcn-dashboard-starter:latest

Technical Documentation: Kiranism/next-shadcn-dashboard-starter

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (`README.md`)

Admin Dashboard Template with Next.js & Shadcn UI

Overview

A free, open source (MIT) admin dashboard starter built with Next.js 16, shadcn/ui on Base UI primitives, TypeScript, and Tailwind CSS v4.

Every feature is a working, production-ready implementation, not static demo UI. Tables search, filter, sort, and paginate for real. Forms validate and mutate with cache invalidation.
Auth, organizations, and billing function end-to-end.

Clone it, strip what you don't need with the built-in cleanup script, and start building on patterns you'd write yourself. It works well as a base for SaaS apps, internal tools, and admin panels.

Why This Template

Most dashboard templates are static demo boilerplates: screens that look finished but need rebuilding the moment you wire in real data. This starter takes the opposite approach:

  • Everything actually works. Data tables run end-to-end: server prefetch, client-side React Query cache, and URL-synced search, filtering, sorting, and pagination via nuqs. Forms are built from reusable, composable fields with Zod validation, including advanced patterns like multi-step and dialog/sheet forms, with real create/update mutations and cache invalidation on success.
  • Industry-standard implementations. The data layer follows the official TanStack Query SSR pattern (server prefetch + HydrationBoundary + useSuspenseQuery), typed end to end, organized in a feature-based structure with a clean API layer per feature. These are patterns you copy into production code as-is, not mockups you rebuild from scratch.
  • Minimal by design. Deliberately lean, with no bloated boilerplate, so you spend your time tweaking it to your use case, not deleting someone else's code. The built-in cleanup script strips any feature you don't need in under a minute.

Tech Stack

Looking for a TanStack Start version? Here's the repo.

Features

  • Pre-built dashboard layout with sidebar, header, and content area
  • Analytics overview page with cards and charts
  • Data tables with React Query prefetch, client-side cache, search, filter, and pagination
  • Authentication and user management through Clerk
  • Multi-tenant workspaces using Clerk Organizations (create, switch, manage teams)
  • Billing and subscriptions via Clerk Billing for B2B, with plan management and feature gating
  • Client-side RBAC navigation that filters menu items by organization, permissions, and roles
  • Infobar component for tips, status messages, or contextual notes on any page
  • shadcn/ui components on Base UI primitives, styled with Tailwind CSS
  • Six-plus themes with a theme switcher
  • Feature-based folder structure
  • A starting point for SaaS dashboards, internal tools, and client admin panels

Use Cases

A few things you can build with it:

  • SaaS admin dashboards
  • Internal tools and operations panels
  • Analytics dashboards
  • Client project admin panels
  • A boilerplate for new Next.js shadcn projects

Pages

Page Notes
Signup / Signin Auth handled by Clerk, with passwordless sign-in, social logins, and enterprise SSO.
Dashboard Overview Cards and Recharts graphs. Parallel routes give each section its own loading and error state.
Product List (Table) TanStack Table plus React Query (server prefetch, client cache) with nuqs URL state for search, filter, and pagination. shallow: true keeps interactions on the client.
Create Product Form TanStack Form and Zod with useMutation for create and update. Cache is invalidated on success.
Users (Table) Same setup as Products: React Query with nuqs, server prefetch, and client-side pagination and filtering.
React Query Demo A Pokemon API example showing the server prefetch, HydrationBoundary, and useSuspenseQuery pattern with client-side cache.
Profile Clerk's account management UI for profile and security settings.
Kanban Board Drag-and-drop task board built with dnd-kit and Zustand. Column sorting, priority badges, assignees, and due dates.
Chat Messaging UI with a conversation list, message bubbles, quick replies, attachments, and an auto-reply demo. Multi-panel layout that works on mobile.
AI Chat Scripted AI chat that streams a predefined conversation through the real useChat lifecycle โ€” no model, API route, or key. Built with the shadcn chat components (MessageScroller, Bubble, Marker).
Notifications Notification center with a header badge, popover preview, and a full page with All / Unread / Read tabs. Includes mark-as-read and mark-all-as-read.
Workspaces Organization management using Clerk's <OrganizationList />. View, create, and switch between organizations.
Team Management Team management using Clerk's <OrganizationProfile />. Manage members, roles, permissions, security, and org details. Needs an active organization.
Billing & Plans Billing page using Clerk's ``. View plans, subscribe, and manage subscriptions. Needs an active organization.
Exclusive Page Plan-based access control with Clerk's <Show>. Only available to organizations on the Pro plan, with a fallback UI for everyone else.
Not Found A root-level not-found page.
Global Error A shared error page wired to Sentry for logging, reports, and session replay.

Folder Structure

text
/* Detailed source-code truncated for AI context efficiency. */

Getting Started

[!NOTE]
This starter uses Next.js 16 (App Router) with React 19 and shadcn/ui. To run it locally:

Clone the repo:

text
git clone https://github.com/Kiranism/next-shadcn-dashboard-starter.git
  • bun install
  • Copy the example env file: cp env.example.txt .env.local
  • Fill in the required variables in .env.local
  • bun run dev

Environment variables

See env.example.txt for the variables you need. They cover authentication and error tracking.

Clerk setup

For setting up Clerk auth (including organizations, workspaces, and teams), see clerk_setup.md.

The app should now be running at http://localhost:3000.

[!WARNING]
After cloning or forking, be careful when pulling the latest changes. Updates can cause merge conflicts.


Cleanup Script: Start Minimal in 60 Seconds

Most starters make you hand-delete demo pages and rip out dependencies. This one ships with a cleanup script that removes the optional features you don't need (folders, files, dependencies, docs, and env entries), leaving a minimal base to build on. Run --list to see what's removable:

bash
bun run cleanup --interactive    # interactive mode
bun run cleanup --list           # see available features
bun run cleanup --dry-run chat   # preview before removing
bun run cleanup kanban chat      # remove specific features

Run bun run cleanup --help for all options (with npm, pass flags after --: npm run cleanup -- --list). The replacement files it writes live in scripts/cleanup-templates/ as real, typechecked code. When you're done, delete scripts/cleanup.js, scripts/cleanup-templates/, and the cleanup entry in package.json.

FAQ

Is it production ready?
Yes. Every feature is a complete, working implementation: authentication, CRUD flows, table search/filter/sort/pagination, and form validation with mutations all function end-to-end. It's a starting point for real applications, not a visual mockup.

How is this different from other dashboard templates?
Most dashboard templates are static demo boilerplates: screens that look finished but need rebuilding once you wire in real data. Here the tables, forms, auth, organizations, and billing all work end-to-end, the implementations follow official TanStack and Next.js patterns, and a cleanup script keeps the base minimal so you tweak it to your use case instead of deleting code.

Is it free for commercial use?
Yes. MIT-licensed and free for both personal and commercial projects: no paid tier, no license keys.

Can I use it without Clerk?
Yes. Run bun run cleanup clerk to remove Clerk authentication (along with organizations and billing) and wire in your own auth solution.

How do I remove demo pages or features I don't need?
Run bun run cleanup --interactive and pick what to strip, or bun run cleanup --list to see what can be removed.

Does it support Next.js 16, React 19, and Tailwind CSS v4?
Yes. The template is built on Next.js 16 (App Router), React 19, and Tailwind CSS v4, with shadcn/ui on Base UI primitives, and is actively maintained to track new releases.

Can I use npm instead of Bun?
Yes. Bun is preferred, but npm works too, and the repo even ships both Node.js and Bun Dockerfiles for deployment.

Does it work with AI coding assistants?
Yes. The repo ships AGENTS.md and CLAUDE.md with the project's conventions, plus a bundled Claude Code skill (.claude/skills/kiranism-shadcn-dashboard) that teaches agents how to add pages, tables, forms, and navigation the template way. Works with Claude Code, Cursor, and any tool that reads AGENTS.md.

What data fetching pattern does it use?
TanStack React Query with the official SSR pattern: prefetchQuery on the server, HydrationBoundary with dehydrate for hydration, and useSuspenseQuery on the client, plus nuqs for URL-synced search-param state. Mutations invalidate the cache on success.

How do I deploy it?
Deploy to Vercel out of the box, or use the included Docker setups: a Node.js Dockerfile and a Bun Dockerfile, both using Next.js standalone output mode. See the deployment guide.

Deploy

Deploy to Vercel out of the box, or use the included Docker setups: a Node.js Dockerfile and a Bun Dockerfile, both using Next.js standalone output mode. Full guide: docs/deployment.md.

Support

If this template saved you some time, a star is appreciated. You can also buy me a coffee if you'd like.


Overview (`.agents/skills/vercel-composition-patterns/README.md`)

React Composition Patterns

A structured repository for React composition patterns that scale. These
patterns help avoid boolean prop proliferation by using compound components,
lifting state, and composing internals.

Structure

  • rules/ - Individual rule files (one per rule)
    • _sections.md - Section metadata (titles, impacts, descriptions)
    • _template.md - Template for creating new rules
    • area-description.md - Individual rule files
  • metadata.json - Document metadata (version, organization, abstract)
  • AGENTS.md - Compiled output (generated)

Rules

Component Architecture (CRITICAL)

  • architecture-avoid-boolean-props.md - Don't add boolean props to customize
    behavior
  • architecture-compound-components.md - Structure as compound components with
    shared context

State Management (HIGH)

  • state-lift-state.md - Lift state into provider components
  • state-context-interface.md - Define clear context interfaces
    (state/actions/meta)
  • state-decouple-implementation.md - Decouple state management from UI

Implementation Patterns (MEDIUM)

  • patterns-children-over-render-props.md - Prefer children over renderX props
  • patterns-explicit-variants.md - Create explicit component variants

Core Principles

  1. Composition over configuration โ€” Instead of adding props, let consumers
    compose
  2. Lift your state โ€” State in providers, not trapped in components
  3. Compose your internals โ€” Subcomponents access context, not props
  4. Explicit variants โ€” Create ThreadComposer, EditComposer, not Composer
    with isThread

Creating a New Rule

  1. Copy rules/_template.md to rules/area-description.md
  2. Choose the appropriate area prefix:
    • architecture- for Component Architecture
    • state- for State Management
    • patterns- for Implementation Patterns
  3. Fill in the frontmatter and content
  4. Ensure you have clear examples with explanations

Impact Levels

  • CRITICAL - Foundational patterns, prevents unmaintainable code
  • HIGH - Significant maintainability improvements
  • MEDIUM - Good practices for cleaner code

Overview (`.agents/skills/vercel-react-best-practices/README.md`)

React Best Practices

A structured repository for creating and maintaining React Best Practices optimized for agents and LLMs.

Structure

  • rules/ - Individual rule files (one per rule)
    • _sections.md - Section metadata (titles, impacts, descriptions)
    • _template.md - Template for creating new rules
    • area-description.md - Individual rule files
  • src/ - Build scripts and utilities
  • metadata.json - Document metadata (version, organization, abstract)
  • AGENTS.md - Compiled output (generated)
  • test-cases.json - Test cases for LLM evaluation (generated)

Getting Started

  1. Install dependencies:

    bash
    pnpm install
  2. Build AGENTS.md from rules:

    bash
    pnpm build
  3. Validate rule files:

    bash
    pnpm validate
  4. Extract test cases:

    bash
    pnpm extract-tests

Creating a New Rule

  1. Copy rules/_template.md to rules/area-description.md
  2. Choose the appropriate area prefix:
    • async- for Eliminating Waterfalls (Section 1)
    • bundle- for Bundle Size Optimization (Section 2)
    • server- for Server-Side Performance (Section 3)
    • client- for Client-Side Data Fetching (Section 4)
    • rerender- for Re-render Optimization (Section 5)
    • rendering- for Rendering Performance (Section 6)
    • js- for JavaScript Performance (Section 7)
    • advanced- for Advanced Patterns (Section 8)
  3. Fill in the frontmatter and content
  4. Ensure you have clear examples with explanations
  5. Run pnpm build to regenerate AGENTS.md and test-cases.json

Rule File Structure

Each rule file should follow this structure:

markdown
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description
tags: tag1, tag2, tag3
---

## Rule Title Here

Brief explanation of the rule and why it matters.

**Incorrect (description of what's wrong):**

```typescript
// Bad code example
```

Correct (description of what's right):

typescript
// Good code example

Optional explanatory text after examples.

Reference: Link

File Naming Convention

  • Files starting with _ are special (excluded from build)
  • Rule files: area-description.md (e.g., async-parallel.md)
  • Section is automatically inferred from filename prefix
  • Rules are sorted alphabetically by title within each section
  • IDs (e.g., 1.1, 1.2) are auto-generated during build

Impact Levels

  • CRITICAL - Highest priority, major performance gains
  • HIGH - Significant performance improvements
  • MEDIUM-HIGH - Moderate-high gains
  • MEDIUM - Moderate performance improvements
  • LOW-MEDIUM - Low-medium gains
  • LOW - Incremental improvements

Scripts

  • pnpm build - Compile rules into AGENTS.md
  • pnpm validate - Validate all rule files
  • pnpm extract-tests - Extract test cases for LLM evaluation
  • pnpm dev - Build and validate

Contributing

When adding or modifying rules:

  1. Use the correct filename prefix for your section
  2. Follow the _template.md structure
  3. Include clear bad/good examples with explanations
  4. Add appropriate tags
  5. Run pnpm build to regenerate AGENTS.md and test-cases.json
  6. Rules are automatically sorted by title - no need to manage numbers!

Acknowledgments

Originally created by @shuding at Vercel.


Overview (`.claude/skills/vercel-composition-patterns/README.md`)

React Composition Patterns

A structured repository for React composition patterns that scale. These
patterns help avoid boolean prop proliferation by using compound components,
lifting state, and composing internals.

Structure

  • rules/ - Individual rule files (one per rule)
    • _sections.md - Section metadata (titles, impacts, descriptions)
    • _template.md - Template for creating new rules
    • area-description.md - Individual rule files
  • metadata.json - Document metadata (version, organization, abstract)
  • AGENTS.md - Compiled output (generated)

Rules

Component Architecture (CRITICAL)

  • architecture-avoid-boolean-props.md - Don't add boolean props to customize
    behavior
  • architecture-compound-components.md - Structure as compound components with
    shared context

State Management (HIGH)

  • state-lift-state.md - Lift state into provider components
  • state-context-interface.md - Define clear context interfaces
    (state/actions/meta)
  • state-decouple-implementation.md - Decouple state management from UI

Implementation Patterns (MEDIUM)

  • patterns-children-over-render-props.md - Prefer children over renderX props
  • patterns-explicit-variants.md - Create explicit component variants

Core Principles

  1. Composition over configuration โ€” Instead of adding props, let consumers
    compose
  2. Lift your state โ€” State in providers, not trapped in components
  3. Compose your internals โ€” Subcomponents access context, not props
  4. Explicit variants โ€” Create ThreadComposer, EditComposer, not Composer
    with isThread

Creating a New Rule

  1. Copy rules/_template.md to rules/area-description.md
  2. Choose the appropriate area prefix:
    • architecture- for Component Architecture
    • state- for State Management
    • patterns- for Implementation Patterns
  3. Fill in the frontmatter and content
  4. Ensure you have clear examples with explanations

Impact Levels

  • CRITICAL - Foundational patterns, prevents unmaintainable code
  • HIGH - Significant maintainability improvements
  • MEDIUM - Good practices for cleaner code

Overview (`.claude/skills/vercel-react-best-practices/README.md`)

React Best Practices

A structured repository for creating and maintaining React Best Practices optimized for agents and LLMs.

Structure

  • rules/ - Individual rule files (one per rule)
    • _sections.md - Section metadata (titles, impacts, descriptions)
    • _template.md - Template for creating new rules
    • area-description.md - Individual rule files
  • src/ - Build scripts and utilities
  • metadata.json - Document metadata (version, organization, abstract)
  • AGENTS.md - Compiled output (generated)
  • test-cases.json - Test cases for LLM evaluation (generated)

Getting Started

  1. Install dependencies:

    bash
    pnpm install
  2. Build AGENTS.md from rules:

    bash
    pnpm build
  3. Validate rule files:

    bash
    pnpm validate
  4. Extract test cases:

    bash
    pnpm extract-tests

Creating a New Rule

  1. Copy rules/_template.md to rules/area-description.md
  2. Choose the appropriate area prefix:
    • async- for Eliminating Waterfalls (Section 1)
    • bundle- for Bundle Size Optimization (Section 2)
    • server- for Server-Side Performance (Section 3)
    • client- for Client-Side Data Fetching (Section 4)
    • rerender- for Re-render Optimization (Section 5)
    • rendering- for Rendering Performance (Section 6)
    • js- for JavaScript Performance (Section 7)
    • advanced- for Advanced Patterns (Section 8)
  3. Fill in the frontmatter and content
  4. Ensure you have clear examples with explanations
  5. Run pnpm build to regenerate AGENTS.md and test-cases.json

Rule File Structure

Each rule file should follow this structure:

markdown
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description
tags: tag1, tag2, tag3
---

## Rule Title Here

Brief explanation of the rule and why it matters.

**Incorrect (description of what's wrong):**

```typescript
// Bad code example
```

Correct (description of what's right):

typescript
// Good code example

Optional explanatory text after examples.

Reference: Link

File Naming Convention

  • Files starting with _ are special (excluded from build)
  • Rule files: area-description.md (e.g., async-parallel.md)
  • Section is automatically inferred from filename prefix
  • Rules are sorted alphabetically by title within each section
  • IDs (e.g., 1.1, 1.2) are auto-generated during build

Impact Levels

  • CRITICAL - Highest priority, major performance gains
  • HIGH - Significant performance improvements
  • MEDIUM-HIGH - Moderate-high gains
  • MEDIUM - Moderate performance improvements
  • LOW-MEDIUM - Low-medium gains
  • LOW - Incremental improvements

Scripts

  • pnpm build - Compile rules into AGENTS.md
  • pnpm validate - Validate all rule files
  • pnpm extract-tests - Extract test cases for LLM evaluation
  • pnpm dev - Build and validate

Contributing

When adding or modifying rules:

  1. Use the correct filename prefix for your section
  2. Follow the _template.md structure
  3. Include clear bad/good examples with explanations
  4. Add appropriate tags
  5. Run pnpm build to regenerate AGENTS.md and test-cases.json
  6. Rules are automatically sorted by title - no need to manage numbers!

Acknowledgments

Originally created by @shuding at Vercel.


Clerk Setup (`docs/clerk_setup.md`)

Clerk Setup Guide

This guide covers the setup and configuration of Clerk features used in this starter template.

Clerk Scopes Required

  • Authentication - User sign-in/sign-up and session management
  • Organizations - Multi-tenant workspace management (see setup below)
  • Billing - Organization-level subscription management (see setup below)

Clerk Organizations Setup (Workspaces & Teams)

This starter kit includes multi-tenant workspace management powered by Clerk Organizations. To enable this feature:

Enable Organizations in Clerk Dashboard:

  1. Go to Clerk Dashboard
  2. Navigate to configure
  3. Click Organizations settings
  4. Configure default roles if needed in the roles and permissions.

Server-Side Permission Checks:

  • Fully client-side navigation filtering using useNav hook
  • Supports requireOrg, permission, and role checks (all client-side, instant)
  • Configured in src/config/nav-config.ts with access properties
  • See docs/nav-rbac.md for detailed documentation

For more information, see:

Clerk Billing Setup (Organization Subscriptions)

This starter kit includes Clerk Billing for B2B to manage organization-level subscriptions. Plans and features are managed through the Clerk Dashboard, and the application checks access using Clerk's has() function.

[!WARNING]
Billing is currently in Beta and its APIs are experimental and may undergo breaking changes. To mitigate potential disruptions, we recommend pinning your SDK and clerk-js package versions.

Key Features:

  • Organization-level subscription management
  • Plan-based access control using the <Show> component
  • Feature-based authorization
  • Integrated Stripe payment processing
  • Server-side plan/feature checks using has() function

Billing Cost Structure:

Clerk Billing costs 0.7% per transaction, plus transaction fees which are paid directly to Stripe. Clerk Billing is not the same as Stripe Billing. Plans and pricing are managed directly through the Clerk Dashboard and won't sync with your existing Stripe products or plans. Clerk uses Stripe only for payment processing, so you don't need to set up Stripe Billing.

Setup Instructions:

1. Enable Billing:

  • Navigate to Billing Settings in the Clerk Dashboard
  • Enable billing for your application
  • Choose payment gateway:
    • Clerk development gateway: A shared test Stripe account for development instances. This allows developers to test and build Billing flows in development without needing to create and configure a Stripe account.
    • Stripe account: Use your own Stripe account for production. A Stripe account created for a development instance cannot be used for production. You will need to create a separate Stripe account for your production environment.

2. Create Plans:

  • Navigate to Plans page in the Clerk Dashboard
  • Select Plans for Organizations tab
  • Click Add Plan and create plans (e.g., free, pro, team)
  • Set pricing and billing intervals
  • Toggle Publicly available to show in `` and <OrganizationProfile /> components

3. Add Features to Plans:

  • You can add Features when creating a Plan, or add them later:
    1. Navigate to the Plans page
    2. Select the Plan you'd like to add a Feature to
    3. In the Features section, select Add Feature
  • Feature names in Clerk Dashboard should match what you check in code

4. Usage in Code:

Server-side checks using has():

typescript
// Check if organization has a Plan
const hasPremiumAccess = has({ plan: 'gold' });

// Check if organization has a Feature
const hasPremiumAccess = has({ feature: 'widgets' });

The has() method is available on the auth object and checks if the Organization has been granted a specific type of access control (Role, Permission, Feature, or Plan) and returns a boolean value.

Client-side protection using <Show>:

tsx
<Show
  when={{ plan: 'bronze' }}
  fallback={<p>Only subscribers to the Bronze plan can access this content.</p>}
>
  <h1>Exclusive Bronze Content</h1>
</Show>

Or protect by Feature:

tsx
<Show
  when={{ feature: 'premium_access' }}
  fallback={<p>Only subscribers with the Premium Access feature can access this content.</p>}
>
  <h1>Exclusive Premium Content</h1>
</Show>

Deployment (`docs/deployment.md`)

Deployment

The starter deploys to Vercel out of the box, or anywhere Docker runs. next.config.ts sets output: 'standalone', so production builds are optimized for self-hosting.

  1. Connect the repository to Vercel
  2. Add environment variables in the dashboard
  3. Deploy

For other platforms, see the Next.js deployment docs.

Environment Variables for Production

Ensure these are set in your deployment platform:

  • NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
  • CLERK_SECRET_KEY
  • All NEXT_PUBLIC_* variables for client-side access
  • SENTRY_* variables if using error tracking

Sentry source maps are uploaded automatically in CI.

Docker

Two production-ready Dockerfiles are included: Dockerfile (Node.js) and Dockerfile.bun (Bun). Pass NEXT_PUBLIC_* variables as --build-arg at build time and runtime secrets via -e at run time.

Build the image:

bash
# Node.js
docker build \
  --build-arg NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxxxx \
  -t shadcn-dashboard .

# OR Bun
docker build -f Dockerfile.bun \
  --build-arg NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxxxx \
  -t shadcn-dashboard .

Run the container:

bash
docker run -d -p 3000:3000 \
  -e NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxxxx \
  -e CLERK_SECRET_KEY=sk_live_xxxxx \
  --restart unless-stopped \
  --name shadcn-dashboard \
  shadcn-dashboard

Forms (`docs/forms.md`)

Forms

Forms follow the official shadcn TanStack Form conventions, scaled with
TanStack's own createFormHook
pattern: the doc's Field anatomy is written once per widget as a reusable
field component, and pages use them as one-liners inside form.AppField.

Architecture

File What it provides
src/lib/form-context.ts createFormHookContexts โ€” fieldContext, formContext, useFieldContext, useFieldInvalid, BaseFieldProps
src/components/forms/fields/*.tsx 16 field components, each the exact shadcn doc anatomy for its widget
src/components/forms/submit-button.tsx SubmitButton โ€” disables while submitting (form.Subscribe)
src/lib/form.ts createFormHook โ€” exports useAppForm / withForm with everything registered

The pattern

One useAppForm per form, a Zod schema validated on submit, and one
form.AppField per field rendering the matching component:

tsx
'use client';

import { useAppForm } from '@/lib/form';
import { FieldGroup } from '@/components/ui/field';
import * as z from 'zod';

const formSchema = z.object({
  title: z.string().min(5, 'Title must be at least 5 characters.'),
  severity: z.string().min(1, 'Select a severity.')
});

export function BugReportForm() {
  const form = useAppForm({
    defaultValues: { title: '', severity: '' },
    validators: { onSubmit: formSchema },
    onSubmit: ({ value }) => console.log(value)
  });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        form.handleSubmit();
      }}
    >
      <FieldGroup>
        <form.AppField
          name='title'
          children={(field) => (
            <field.TextField label='Bug Title' required placeholder='Login button broken' />
          )}
        />
        <form.AppField
          name='severity'
          children={(field) => (
            <field.SelectField
              label='Severity'
              options={[
                { value: 'low', label: 'Low' },
                { value: 'high', label: 'High' }
              ]}
            />
          )}
        />
        <form.AppForm>
          <form.SubmitButton>Submit</form.SubmitButton>
        </form.AppForm>
      </FieldGroup>
    </form>
  );
}

form.AppField's name is fully typed against defaultValues โ€” typos are
compile errors. Field-level validators/listeners go on the form.AppField
element (async checks, onChangeListenTo linked fields).

Available field components

All render inside form.AppField as field.XxxField; every one takes
label, description?, required?.

Component Value type Notes
TextField string / number Any input type (text, email, password, tel, url, time, number). Number inputs convert at the edge โ€” clearing writes undefined. Shows a spinner while async validators run.
TextareaField string showCount renders a character counter against maxLength
SelectField string options array
CheckboxField boolean Single checkbox (terms, consent)
SwitchField boolean Label/description left, switch right
RadioGroupField string FieldSet + FieldLegend semantics
SliderField number min/max/step + value readout
ComboboxField string Searchable select (Popover + Command)
DatePickerField Date | undefined Popover + Calendar, disabledDates
DateRangeField DateRange | undefined Two-month range calendar
OtpField string 6-digit code (3 + 3)
ColorField string Native picker + hex input
FileUploadField File[] Wraps FileUploader, maxSize/maxFiles
CheckboxGroupField string[] Needs mode='array' on the AppField
TagsField string[] Needs mode='array'; Enter/Add pushes, badges remove
ToggleGroupField string[] Needs mode='array'; pass ToggleGroupItems as children

One-off custom fields โ€” drop down to `form.Field`

For anything the components don't cover (object-row arrays, bespoke UI), use
the raw doc pattern directly โ€” it composes freely with the components:

tsx
<form.Field
  name='members'
  mode='array'
  children={(field) => (
    <>
      {field.state.value.map((_, i) => (
        <form.Field
          name={`members[${i}].name`}
          children={(subField) => {
            const isInvalid = subField.state.meta.isTouched && !subField.state.meta.isValid;
            return (
              <Field data-invalid={isInvalid}>
                <Input
                  value={subField.state.value}
                  onChange={(e) => subField.handleChange(e.target.value)}
                  onBlur={subField.handleBlur}
                  aria-invalid={isInvalid}
                />
                {isInvalid && <FieldError errors={subField.state.meta.errors} />}
              </Field>
            );
          }}
        />
      ))}
      <Button type='button' onClick={() => field.pushValue({ name: '' })}>Add</Button>
    </>
  )}
/>

The doc conventions inside any custom field: data-invalid on <Field>,
aria-invalid on the control, {isInvalid && <FieldError errors={โ€ฆ} />},
function validators return { message: 'โ€ฆ' } objects.

Scaling to large forms โ€” `withForm` sections

Split a big form into reusable section components with withForm (also
exported from @/lib/form). Sections receive the form instance as a prop and
keep fully typed field names โ€” a typo'd name inside a section is still a
compile error:

tsx
import { useAppForm, withForm } from '@/lib/form';

const ShippingSection = withForm({
  defaultValues: checkoutDefaults, // ties the section to the form's shape
  render: function ShippingRender({ form }) {
    return (
      <FieldGroup>
        <form.AppField
          name='shipping.street'
          children={(field) => <field.TextField label='Street' required />}
        />
        <form.AppField
          name='shipping.city'
          children={(field) => <field.TextField label='City' required />}
        />
      </FieldGroup>
    );
  }
});

// In the page:
const form = useAppForm({ defaultValues: checkoutDefaults, ... });
<ShippingSection form={form} />

Deep paths (org.billing.address.city), array sub-paths
(admins[0].prefs.notify), and union-typed leaves all stay typed, and
typechecking stays fast at 40+ fields.

Template-specific notes

Submitting with React Query. onSubmit awaits the mutation; success/error
handling lives on the mutation (see features/products/components/product-form.tsx):

tsx
onSubmit: async ({ value }) => {
  await createMutation.mutateAsync(value);
};

Sheet / Dialog forms. The submit button lives in the footer, outside the
<form> element, connected via the HTML form attribute
(features/users/components/user-form-sheet.tsx):

tsx
<form id='user-form-sheet' onSubmit={โ€ฆ}>โ€ฆ</form>
<SheetFooter>
  <Button type='submit' form='user-form-sheet'>Save</Button>
</SheetFooter>

Multi-step forms. useFormStepper(stepSchemas, { fullSchema }) from
@/hooks/use-stepper gates step navigation: Next validates the current
step's schema and paints its errors; the final submit re-validates the whole
schema and never submits invalid data. Route every submit through the gate โ€”
see features/forms/components/multi-step-product-form.tsx.

Number inputs. TextField type='number' already converts at the edge;
give required numbers a human message: z.number({ error: 'Price is required' }).

Caveats to know (verified by stress testing):

  • field.XxxField components assert their value type via
    useFieldContext<T>() โ€” the compiler checks the name path exists, but
    not that the widget matches the path's value type (a SwitchField on a
    string path compiles and renders wrong values). Match widgets to the table
    above.
  • Rendering a field component outside form.AppField throws a clear error
    (fieldContext only works when within a fieldComponentโ€ฆ) โ€” it cannot fail
    silently.
  • Two forms with identical field names mounted at once (a sheet over a page)
    produce duplicate id attributes โ€” the shadcn doc's id={field.name}
    convention. Form state stays fully isolated; only label-target ids
    collide. Rename fields or avoid simultaneous mounting if labels must stay
    clickable in both.
  • Forgetting mode='array' on an AppField using CheckboxGroupField /
    TagsField / ToggleGroupField still renders and updates โ€” but keep the
    convention: array mode gives TanStack correct per-item meta tracking.

Examples in the dashboard

Page Route Demonstrates
Basic Form /dashboard/forms/basic All 16 field components incl. array-mode groups, pickers, OTP, tags, upload
Advanced /dashboard/forms/advanced Async validation, linked fields (onChangeListenTo), nested paths, raw form.Field object-row arrays, listener side effects
Multi-Step /dashboard/forms/multi-step Per-step schemas, validation gate, review step
Sheet Form /dashboard/forms/sheet-form Sheet + Dialog forms with external submit buttons

Simplified Navigation RBAC System

Overview

This document explains the fully client-side RBAC (Role-Based Access Control) system for navigation items.

Key Insight: Navigation visibility is UX only, not security. We can check everything client-side using Clerk's hooks!

Architecture

Core Files

  1. src/hooks/use-nav.ts - Single hook that handles all filtering logic (fully client-side)
  2. src/types/index.ts - Type definitions with access property

Why Client-Side?

  • Navigation visibility is UX only - Users can't bypass security by seeing/hiding nav items
  • Clerk provides all data client-side - useOrganization() gives us membership.permissions and membership.role
  • Zero server calls - Instant filtering, no loading states, no UI flashing
  • Better performance - No network latency, no async complexity

Note: For actual security (API routes, server actions, page protection), always use server-side checks.

Performance Characteristics

All Checks Are Synchronous

โœ… requireOrg: Client-side check using useOrganization()
โœ… permission: Client-side check using membership.permissions array
โœ… role: Client-side check using membership.role
โš ๏ธ plan/feature: Requires server-side check (see below)

Zero Server Calls

  • All navigation filtering happens synchronously
  • No loading states
  • No UI flashing
  • Instant results

Usage

In `nav-config.ts`

typescript
{
  title: 'Teams',
  url: '/dashboard/workspaces/team',
  icon: 'userPen',
  // Simple: requireOrg (client-side check, instant)
  access: { requireOrg: true }
}

{
  title: 'Admin Panel',
  url: '/dashboard/admin',
  icon: 'settings',
  // All client-side checks - instant!
  access: {
    requireOrg: true,
    permission: 'org:admin:manage',  // Client-side from membership.permissions
    role: 'admin'  // Client-side from membership.role
}

In Components

typescript
import { useFilteredNavItems } from '@/hooks/use-nav';

function MyComponent() {
  const filteredItems = useFilteredNavItems(navItems);
  // filteredItems is automatically filtered based on RBAC
}

Plan/Feature Checks

Plans and features require Clerk's has() function which is server-side only. Options:

  1. Store in organization metadata (recommended for navigation):

    typescript
    // In your organization setup
    organization.publicMetadata.plan = 'pro';
    
    // In nav-config.ts
    access: {
      requireOrg: true,
      // Check metadata instead of plan
    }
  2. Show item, protect at page level (current approach):

    • Navigation item is shown
    • Page component checks server-side and redirects/shows error if needed
  3. Use server action (if you really need it):

    • Only for navigation items that absolutely need plan/feature checks
    • Most navigation items won't need this

Scalability

Adding New Items

Just add to nav-config.ts:

typescript
{
  title: 'New Feature',
  url: '/dashboard/new',
  icon: 'star',
  access: { plan: 'pro' }  // That's it!
}

The system automatically:

  • Filters it in sidebar
  • Filters it in kbar
  • Handles async checks if needed
  • Handles sync checks immediately

Adding New Access Types

  1. Add to PermissionCheck interface in src/app/actions/rbac.ts
  2. Add check logic in checkAccess() function
  3. Update use-nav.ts to handle the new type

Comparison: Before vs After

Before (Overcomplicated)

  • 4 files with complex logic
  • Multiple hooks and utilities
  • Unclear data flow
  • Potential for bugs

After (Simplified)

  • 1 main hook file
  • Clear, linear logic
  • Easy to understand
  • Easy to maintain

Best Practices

  1. Use requireOrg: true for simple cases - It's instant and requires no server call
  2. Combine checks when possible - { requireOrg: true, permission: '...' } is more efficient than separate checks
  3. Avoid unnecessary checks - Don't add access if the item should always be visible

Migration from Old System

The old visible function still works for backward compatibility:

typescript
// Old way (still works)
visible: (context) => !!context?.organization;

// New way (recommended)
access: {
  requireOrg: true;
}

Future Improvements

Potential optimizations if needed:

  1. Cache permission checks (e.g., React Query)
  2. Prefetch permissions on app load
  3. Optimistic UI updates

But for now, the current implementation is:

  • โœ… Simple
  • โœ… Fast
  • โœ… Scalable
  • โœ… Maintainable

Themes (`docs/themes.md`)

Adding New Themes

This guide explains how to add a new theme to the application. The theme system uses CSS custom properties with [data-theme] selectors for easy theme switching.

The Journey: Adding a New Theme

When adding a new theme, follow this journey:

  1. Create theme CSS file โ†’ src/styles/themes/your-theme-name.css with [data-theme='your-theme-name']
  2. Import theme โ†’ Add @import to src/styles/theme.css
  3. Register theme โ†’ Add to THEMES array in src/components/themes/theme.config.ts
  4. Add fonts (if needed) โ†’ Import fonts in src/components/themes/font.config.ts if using custom Google Fonts
  5. Set as default (optional) โ†’ Update DEFAULT_THEME in src/components/themes/active-theme.tsx

See the Step-by-Step Guide section below for detailed instructions.

Quick Start: Set Your Theme as Default

To make your new theme the default (so it loads automatically without the theme switcher):

  1. Open src/components/themes/active-theme.tsx
  2. Change line 12: const DEFAULT_THEME = 'your-theme-name';
  3. Save and restart your dev server

That's it! Your theme will now be the default for all new users.

Note: Make sure you've completed steps 1-3 above before setting a theme as default.

Theme Structure

All themes are located in src/styles/themes/ directory. Each theme is a complete, self-contained CSS file that defines all design tokens for both light and dark modes.

File Format

Each theme file must follow this structure:

text
/* Detailed source-code truncated for AI context efficiency. */

Step-by-Step Guide: Adding a New Theme

Follow these steps in order to add a new theme to your application.

Step 1: Create Theme CSS File

Create a new file in src/styles/themes/ with a descriptive name (use kebab-case):

bash
src/styles/themes/your-theme-name.css

Important: The filename should match the data-theme attribute value you'll use in the CSS.

Step 2: Define Your Theme with `[data-theme]` Attribute

Copy the structure from the "File Format" section above and fill in your color values. Use OKLCH color format for better color consistency:

css
/* Light mode tokens */
[data-theme='your-theme-name'] {
  --background: oklch(1 0 0); /* White */
  --foreground: oklch(0.145 0 0); /* Dark gray */
  --card: oklch(...);
  /* ... all other tokens */
}

/* Dark mode tokens */
[data-theme='your-theme-name'].dark {
  --background: oklch(0.145 0 0); /* Dark */
  --foreground: oklch(0.985 0 0); /* Light */
  /* ... all other tokens with dark mode values */
}

/* Theme inline mappings for Tailwind */
[data-theme='your-theme-name'] {
  @theme inline {
    /* All the mappings as shown in the File Format section */
  }
}

Color Format:

  • Use oklch() format: oklch(lightness chroma hue)
  • Example: oklch(0.852 0.199 91.936) = light green-blue
  • Lightness: 0-1 (0 = black, 1 = white)
  • Chroma: 0+ (0 = grayscale, higher = more saturated)
  • Hue: 0-360 (color wheel position)

Key Points:

  • The [data-theme='your-theme-name'] selector is what makes your theme work
  • The value 'your-theme-name' must match exactly in all places (CSS file, config, etc.)
  • Always include both light and dark mode variants
  • Include the @theme inline block for Tailwind CSS integration

Step 3: Import Theme in theme.css

Add your theme import to src/styles/theme.css:

css
@import './themes/your-theme-name.css';

This makes your theme available to the application.

Step 4: Add Theme to theme.config.ts

Add your theme to the THEMES array in src/components/themes/theme.config.ts:

typescript
export const THEMES = [
  // ... existing themes
  {
    name: 'Your Theme Name', // Display name in the UI
    value: 'your-theme-name' // Must match [data-theme] value exactly
  }
];

Important: The value field must match the data-theme attribute value from your CSS file exactly.

Step 5: Add Custom Fonts (If Needed)

Only do this step if your theme requires a custom Google Font that isn't already loaded.

If you want to use a Google Font in your theme:

File: src/components/themes/font.config.ts

  1. Import the font from next/font/google:
typescript
import { Your_Font_Name } from 'next/font/google';
  1. Configure the font with a CSS variable:
typescript
const fontYourName = Your_Font_Name({
  subsets: ['latin'],
  weight: ['400', '500', '700'], // Adjust weights as needed
  variable: '--font-your-name' // Optional: custom variable name
});
  1. Add it to the fontVariables export:
typescript
export const fontVariables = cn(
  // ... existing fonts
  fontYourName.variable
);
  1. Use the font in your theme CSS by its display name (not the CSS variable):
css
[data-theme='your-theme-name'] {
  --font-sans: 'Your Font Name', sans-serif; /* Use the actual font name */
  --font-mono: 'Your Mono Font', monospace;
}

Important Notes:

  • Use the font's display name in CSS (e.g., 'Geist', 'Architects Daughter'), not the CSS variable
  • The font must be imported in font.config.ts for it to be loaded by Next.js
  • Font variables from font.config.ts are automatically applied to the body via layout.tsx
  • You can use any Google Font available in next/font/google
  • Check existing fonts in font.config.ts before adding new ones - you might be able to reuse them

Example: The notebook theme uses Architects Daughter:

  • Imported in font.config.ts as Architects_Daughter
  • Used in notebook.css as 'Architects Daughter' (with quotes and space)

Step 6: Set as Default Theme (Optional)

If you want your theme to be the default theme that loads when users first visit the application (without needing the theme switcher), update the default theme constant:

File: src/components/themes/theme.config.ts

typescript
/**
 * Default theme that loads when no user preference is set
 * Change this value to set a different default theme
 */
export const DEFAULT_THEME = 'your-theme-name'; // Change from 'vercel' to your theme name

Note:

  • This is the single source of truth for the default theme - it's automatically used in both server-side rendering and client-side code
  • This will make your theme the default for all new users
  • Existing users who have already selected a theme will still see their saved preference (stored in cookies)
  • The default theme is applied immediately on page load (no flash of unstyled content)

Step 7: Test Your Theme

  1. Start your development server
  2. Open the theme selector in the UI
  3. Select your new theme
  4. Verify it works in both light and dark modes
  5. Test scaled variant by selecting "Your Theme Name (Scaled)"
  6. If you set it as default, clear your browser cookies and refresh to see it load automatically

Quick Reference: File Locations

When adding a new theme, you'll work with these files in this order:

  1. โœ… src/styles/themes/your-theme-name.css - Create theme file with [data-theme] attribute
  2. โœ… src/styles/theme.css - Import your theme file
  3. โœ… src/components/themes/theme.config.ts - Add theme to THEMES array
  4. โš ๏ธ src/components/themes/font.config.ts - Add fonts only if needed
  5. โš ๏ธ src/components/themes/active-theme.tsx - Set as default only if desired

Required Tokens

Minimum Required

At minimum, your theme should define these tokens:

  • --background
  • --foreground
  • --card & --card-foreground
  • --popover & --popover-foreground
  • --primary & --primary-foreground
  • --secondary & --secondary-foreground
  • --muted & --muted-foreground
  • --accent & --accent-foreground
  • --destructive & --destructive-foreground
  • --border
  • --input
  • --ring
  • --radius

Optional Tokens

These can be omitted if not needed:

  • --chart-1 through --chart-5 (defaults to primary colors)
  • --sidebar-* tokens (defaults to card colors)
  • --font-* tokens (uses system defaults)
  • --shadow-* tokens (no shadows if omitted)
  • --tracking-normal (no letter spacing if omitted)
  • --spacing (uses default)

Example: Complete Theme

See src/styles/themes/claude.css for a complete example with all tokens defined.

Example: Minimal Theme

For a minimal theme, you can copy an existing theme and modify only the colors you want to change. The system will fall back to defaults for any missing tokens.

Color Format Reference

OKLCH Format

text
oklch(lightness chroma hue)
  • Lightness: 0-1 (0 = black, 1 = white)
  • Chroma: 0+ (0 = grayscale, 0.2+ = colorful)
  • Hue: 0-360 degrees
    • 0/360 = Red
    • 60 = Yellow
    • 120 = Green
    • 180 = Cyan
    • 240 = Blue
    • 300 = Magenta

Examples

css
/* Pure white */
--background: oklch(1 0 0);

/* Pure black */
--foreground: oklch(0 0 0);

/* Bright blue */
--primary: oklch(0.7 0.2 240);

/* Muted gray */
--muted: oklch(0.5 0 0);

Scaled Variants

All themes automatically support scaled variants. When a user selects "Theme Name (Scaled)", the .theme-scaled class is applied, which adjusts spacing and text sizes. No additional CSS is needed in your theme file.

Best Practices

  1. Use descriptive theme names: Use kebab-case (e.g., ocean-blue, forest-green)
  2. Provide both light and dark modes: Always define both variants
  3. Test accessibility: Ensure sufficient contrast between foreground and background
  4. Keep tokens consistent: Use similar lightness/chroma values for related colors
  5. Document special features: If your theme has unique characteristics (like no shadows or custom fonts), add comments

Troubleshooting

Theme Not Appearing

  • Check that the file is imported in src/styles/theme.css
  • Verify the theme name matches in both CSS file and theme-selector.tsx
  • Ensure the file is saved and the dev server has reloaded

Colors Not Applying

  • Verify all required tokens are defined
  • Check that @theme inline block includes all color mappings
  • Ensure OKLCH format is correct (no typos)

Dark Mode Not Working

  • Verify .dark selector is correct: [data-theme='name'].dark
  • Check that dark mode tokens are defined
  • Ensure next-themes is properly configured

Setting a Default Theme

By default, the application uses the vercel theme. To change the default theme that loads for new users:

Change Default Theme Constant

Edit src/components/themes/theme.config.ts and update the DEFAULT_THEME constant:

typescript
/**
 * Default theme that loads when no user preference is set
 * Change this value to set a different default theme
 */
export const DEFAULT_THEME = 'your-theme-name'; // Change this value

How it works:

  • Single source of truth: DEFAULT_THEME is defined in theme.config.ts and imported everywhere it's needed
  • Server-side: Applied immediately in the HTML data-theme attribute (no flash)
  • Client-side: Used as fallback when no cookie preference exists
  • User preferences: Still respects saved user preferences (stored in cookies)
  • Automatic: No need to update multiple files - change it once and it works everywhere

Benefits of this approach:

โœ… No code duplication - defined once, used everywhere
โœ… Type-safe - TypeScript ensures consistency
โœ… Easy to change - update one line in one file
โœ… Well-documented - clear comments explain its purpose
โœ… Immediate application - no flash of unstyled content

Using Google Fonts in Themes

Note: This section provides additional details about fonts. For the complete step-by-step process, see Step 5 in the "Step-by-Step Guide" above.

When to Add Fonts

You only need to add fonts to font.config.ts if:

  • Your theme uses a Google Font that isn't already imported
  • You want to use a custom font that requires loading

Tip: Check src/components/themes/font.config.ts first - many fonts may already be available!

Font Loading Process

  1. Import the font in src/components/themes/font.config.ts:
typescript
import { Roboto, Roboto_Mono } from 'next/font/google';
  1. Configure the font with a CSS variable:
typescript
const fontRoboto = Roboto({
  subsets: ['latin'],
  weight: ['400', '500', '700'],
  variable: '--font-roboto'
});
  1. Add to fontVariables export:
typescript
export const fontVariables = cn(
  // ... existing fonts
  fontRoboto.variable
);
  1. Use in your theme CSS with the font's display name:
css
[data-theme='your-theme'] {
  --font-sans: 'Roboto', sans-serif; /* Use display name, not CSS variable */
  --font-mono: 'Roboto Mono', monospace;
}

Important Notes

  • Font names: Use the font's display name in CSS (e.g., 'Roboto', 'Open Sans'), not the CSS variable name
  • Font loading: Fonts must be imported in font.config.ts to be loaded by Next.js
  • Automatic application: Font variables are automatically applied to the body element via layout.tsx
  • Available fonts: Check Next.js Font Optimization for available Google Fonts

Example: Notebook Theme

The notebook theme uses Architects Daughter:

In font.config.ts:

typescript
import { Architects_Daughter } from 'next/font/google';

const fontArchitectsDaughter = Architects_Daughter({
  subsets: ['latin'],
  weight: '400',
  variable: '--font-architects-daughter'
});

export const fontVariables = cn(
  // ... other fonts
  fontArchitectsDaughter.variable
);

In notebook.css:

css
[data-theme='notebook'] {
  --font-sans: 'Architects Daughter', sans-serif;
}

Reference Files

  • Complete theme example: src/styles/themes/claude.css
  • Theme aggregator: src/styles/theme.css
  • Theme selector component: src/components/themes/theme-selector.tsx
  • Theme provider: src/components/themes/active-theme.tsx
  • Theme configuration (includes default theme): src/components/themes/theme.config.ts
  • Font configuration: src/components/themes/font.config.ts

--- METRICS ---