### Clerk Setup # 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](https://dashboard.clerk.com) 2. Navigate to **configure** 3. Click **Organizations settings** 4. Configure default roles if needed in the roles and permissions. ### Server-Side Permission Checks: - This starter follows [Clerk's recommended patterns](https://clerk.com/blog/how-to-build-multitenant-authentication-with-clerk) ### Navigation RBAC System: - 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 Organizations documentation](https://clerk.com/docs/organizations/overview) - [Multi-tenant authentication guide](https://clerk.com/blog/how-to-build-multitenant-authentication-with-clerk) ## 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 `` 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](https://dashboard.clerk.com/~/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](https://dashboard.clerk.com/~/billing/plans) 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 `` components #### 3. Add Features to Plans: - You can add Features when creating a Plan, or add them later: 1. Navigate to the [Plans](https://dashboard.clerk.com/~/billing/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 ``:** ```tsx Only subscribers to the Bronze plan can access this content.

} >

Exclusive Bronze Content

``` Or protect by Feature: ```tsx Only subscribers with the Premium Access feature can access this content.

} >

Exclusive Premium Content

``` --- ### Deployment # 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. ## Vercel (Recommended) 1. Connect the repository to Vercel 2. Add environment variables in the dashboard 3. Deploy For other platforms, see the [Next.js deployment docs](https://nextjs.org/docs/app/getting-started/deploying). ## 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 # Forms Forms follow the **official shadcn TanStack Form conventions**, scaled with TanStack's own [`createFormHook`](https://tanstack.com/form/latest/docs/framework/react/guides/form-composition) 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`. - [shadcn: TanStack Form](https://ui.shadcn.com/docs/forms/tanstack) — the anatomy inside every field component - [TanStack Form docs](https://tanstack.com/form/latest) — validators, listeners, arrays, async validation ## 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 (
{ e.preventDefault(); form.handleSubmit(); }} > ( )} /> ( )} /> Submit
); } ``` `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 `ToggleGroupItem`s 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 ( <> {field.state.value.map((_, i) => ( { const isInvalid = subField.state.meta.isTouched && !subField.state.meta.isValid; return ( subField.handleChange(e.target.value)} onBlur={subField.handleBlur} aria-invalid={isInvalid} /> {isInvalid && } ); }} /> ))} )} /> ``` The doc conventions inside any custom field: `data-invalid` on ``, `aria-invalid` on the control, `{isInvalid && }`, 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 ( } /> } /> ); } }); // In the page: const form = useAppForm({ defaultValues: checkoutDefaults, ... }); ``` 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 `
` element, connected via the HTML `form` attribute (`features/users/components/user-form-sheet.tsx`): ```tsx …
``` **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()` — 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 | --- ### Nav Rbac # 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 # 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: ``` /* 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'; ``` 2. **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 }); ``` 3. **Add it to the `fontVariables` export**: ```typescript export const fontVariables = cn( // ... existing fonts fontYourName.variable ); ``` 4. **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 ``` 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'; ``` 2. **Configure the font** with a CSS variable: ```typescript const fontRoboto = Roboto({ subsets: ['latin'], weight: ['400', '500', '700'], variable: '--font-roboto' }); ``` 3. **Add to fontVariables export**: ```typescript export const fontVariables = cn( // ... existing fonts fontRoboto.variable ); ``` 4. **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](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) 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` --- ### README

Admin Dashboard Template with Next.js & Shadcn UI

Free, open source admin dashboard starter built with Next.js 16, shadcn/ui, Tailwind CSS, and TypeScript

Shadcn Dashboard Cover

GitHub stars Forks MIT License Next.js Sponsored by Clerk

## 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](#cleanup-script-start-minimal-in-60-seconds) strips any feature you don't need in under a minute. ### Tech Stack - Framework - [Next.js 16](https://nextjs.org/16) - Language - [TypeScript](https://www.typescriptlang.org) - Auth - [Clerk](https://go.clerk.com/ILdYhn7) - Error tracking - [Sentry](https://sentry.io/for/nextjs/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy26q2-nextjs&utm_content=github-banner-project-tryfree) - Styling - [Tailwind CSS v4](https://tailwindcss.com) - Components - [shadcn/ui](https://ui.shadcn.com) on [Base UI](https://base-ui.com) primitives - Charts - [Recharts](https://recharts.org) • [Evil Charts](https://evilcharts.com/) - Schema validation - [Zod](https://zod.dev) - Data fetching - [TanStack React Query](https://tanstack.com/query) - State management - [Zustand](https://zustand-demo.pmnd.rs) - Search param state - [Nuqs](https://nuqs.47ng.com/) - Tables - [TanStack Data Tables](https://ui.shadcn.com/docs/components/data-table) • [Dice Table](https://www.diceui.com/docs/components/data-table) - Forms - [TanStack Form](https://tanstack.com/form) + [Zod](https://zod.dev) - Command+K interface - [kbar](https://kbar.vercel.app/) - Linter / Formatter - [OxLint](https://oxc.rs/docs/guide/usage/linter) • [Oxfmt](https://oxc.rs/docs/guide/usage/formatter) - Pre-commit hooks - [Husky](https://typicode.github.io/husky/) - Themes - [tweakcn](https://tweakcn.com/) _Looking for a TanStack Start version? Here's the [repo](https://git.new/tanstack-start-dashboard)._ ## 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](https://go.clerk.com/ILdYhn7) | Auth handled by Clerk, with passwordless sign-in, social logins, and enterprise SSO. | | [Dashboard Overview](https://shadcn-dashboard.kiranism.dev/dashboard) | Cards and Recharts graphs. Parallel routes give each section its own loading and error state. | | [Product List (Table)](https://shadcn-dashboard.kiranism.dev/dashboard/product) | 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](https://shadcn-dashboard.kiranism.dev/dashboard/product/new) | TanStack Form and Zod with `useMutation` for create and update. Cache is invalidated on success. | | [Users (Table)](https://shadcn-dashboard.kiranism.dev/dashboard/users) | Same setup as Products: React Query with nuqs, server prefetch, and client-side pagination and filtering. | | [React Query Demo](https://shadcn-dashboard.kiranism.dev/dashboard/react-query) | A Pokemon API example showing the server prefetch, `HydrationBoundary`, and `useSuspenseQuery` pattern with client-side cache. | | [Profile](https://shadcn-dashboard.kiranism.dev/dashboard/profile) | Clerk's account management UI for profile and security settings. | | [Kanban Board](https://shadcn-dashboard.kiranism.dev/dashboard/kanban) | Drag-and-drop task board built with dnd-kit and Zustand. Column sorting, priority badges, assignees, and due dates. | | [Chat](https://shadcn-dashboard.kiranism.dev/dashboard/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](https://shadcn-dashboard.kiranism.dev/dashboard/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](https://shadcn-dashboard.kiranism.dev/dashboard/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](https://shadcn-dashboard.kiranism.dev/dashboard/workspaces) | Organization management using Clerk's ``. View, create, and switch between organizations. | | [Team Management](https://shadcn-dashboard.kiranism.dev/dashboard/workspaces/team) | Team management using Clerk's ``. Manage members, roles, permissions, security, and org details. Needs an active organization. | | [Billing & Plans](https://shadcn-dashboard.kiranism.dev/dashboard/billing) | Billing page using Clerk's ``. View plans, subscribe, and manage subscriptions. Needs an active organization. | | [Exclusive Page](https://shadcn-dashboard.kiranism.dev/dashboard/exclusive) | Plan-based access control with Clerk's ``. Only available to organizations on the Pro plan, with a fallback UI for everyone else. | | [Not Found](https://shadcn-dashboard.kiranism.dev/dashboard/notfound) | A root-level not-found page. | | [Global Error](https://sentry.io/for/nextjs/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy26q2-nextjs&utm_content=github-banner-project-tryfree) | A shared error page wired to Sentry for logging, reports, and session replay. | ## Folder Structure ``` /* 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: ``` 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](./docs/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](./docs/deployment.md). ## 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](./docs/deployment.md). ### Support If this template saved you some time, a star is appreciated. You can also [buy me a coffee](https://buymeacoffee.com/kir4n) if you'd like. [](https://buymeacoffee.com/kir4n) ---