A production-ready, opinionated Next.js frontend template by Hybrid Interactive. Mirrors the structure of the FastAPI template on the frontend.
Conventions (read first): CLAUDE.md — the guardrail, plus the anti-patterns list
The rules, one per file: docs/rules/
How it fits together: docs/FRONTEND_ARCHITECTURE_GUIDE_V3.md
All documentation: docs/README.md
Working with an AI assistant? Point it at CLAUDE.md — Claude Code reads it
automatically, and any other tool can be told to read it from the repo.
| Concern | Library | Version |
|---|---|---|
| Framework | Next.js (App Router) | 16.x |
| UI Library | React | 19.x |
| Component Primitives | shadcn/ui | latest |
| Server State | TanStack Query | 5.x |
| Client State | Zustand | 5.x |
| Styling | Tailwind CSS (v4, CSS-native) | 4.x |
| Forms | React Hook Form + Zod | RHF 7 + Zod 3 |
| Icons | Lucide React | latest |
| Toasts | Sonner | 2.x |
| Dark Mode | next-themes | 0.4.x |
| Animations | Framer Motion | 12.x |
| Decimal math | big.js | 7.x |
| Auth | BFF pattern (httpOnly cookies) | — |
| RBAC | Built-in permissions system | — |
| Unit tests | Vitest + Testing Library | 4.x |
| Browser tests | Playwright | 1.x |
Click "Use this template" on GitHub → "Create a new repository" → name your repo → click "Create repository".
First time? Go to the template repo and click the green "Use this template" button.
git clone https://github.com/<your-org>/<your-repo>.git
cd <your-repo>npm installnode ncube.js init # or: node ncube.js init my-app-nameThis does three things in one step:
- Sets the project name in
package.json - Creates
.envfrom.env.example(with your app name pre-filled) - Installs all shadcn/ui components into
src/components/ui/
# Edit .env — at minimum set:
NEXT_PUBLIC_API_URL="http://localhost:8000"npm run devOpen http://localhost:3000.
If you prefer to work from a local copy of the template instead of GitHub's "Use this template":
# From inside the nextjs-template directory:
node ncube.js create my-app [--variant base|rbac|full]
cd ../my-app
npm install
npm run dev
createis deprecated in favour of the template flow above. It still works but will show a notice.
src/
├── app/
│ ├── (auth)/ # Unauthenticated pages
│ │ └── login/page.tsx
│ ├── (dashboard)/ # Authenticated pages — permission-gated
│ │ ├── config.ts # PermissionedNavItem[], ROUTES — pure data, no JSX
│ │ ├── layout.tsx # Seeds auth + role stores, renders the shell
│ │ └── dashboard/
│ │ ├── layout.tsx # export const dynamic = "force-dynamic"
│ │ ├── error.tsx # A page can crash without taking the shell
│ │ ├── loading.tsx # Route skeleton
│ │ └── <feature>/page.tsx # Server component
│ ├── api/auth/ # BFF route handlers — the only code holding cookies
│ ├── error.tsx global-error.tsx not-found.tsx
│ ├── globals.css # Design tokens — the single source of truth
│ └── layout.tsx # Root layout + provider stack
│
├── components/
│ ├── data-view/ # The list system: toolbar, table, paging, bulk actions
│ ├── ui/ # shadcn primitives + Modal, StatusBadge, PageHeader
│ ├── shared/ # DataTable, ReferencePicker, Field/DetailRow, lazy
│ ├── layout/ # DashboardShell, PageLayout
│ ├── loading/ # Global blocking overlay
│ ├── auth/ # SessionExpiredDialog
│ └── providers/ # QueryProvider
│
├── hooks/ # useMediaQuery, useDebounce, useOlderPages
│
├── lib/
│ ├── api-client.ts # The one HTTP client — stateless, same-origin
│ ├── auth/ # BFF auth + session-expiry store
│ ├── permissions/ # RBAC: strict Permission union, hooks, mapping
│ ├── loading/ # useBlockingMutation + the overlay store
│ ├── numeric/ # Money & quantities on big.js — decimal strings
│ ├── date-utils.ts timezone.ts # Instants vs business dates
│ ├── forms/ # isFormDirty, useResetOnOpen
│ ├── reference/ # Ungated dropdown feeds
│ ├── utilities/ # Downloads, logger
│ ├── hooks/ # useTabState, useZustandTabSync
│ └── <domain>/ # types → transformers → api → hooks → index
│
├── proxy.ts # Route protection + the API rewrite to the backend
└── types/index.ts # AppError, NavItem, shared shapes
docs/
├── README.md # Documentation index
├── rules/ # One rule per file — the detail behind CLAUDE.md
├── OPTIONAL_PARTS.md # What you can delete, and how
└── FRONTEND_ARCHITECTURE_GUIDE_V3.md
The template ships more than most projects use. Dead code is worse than absent code — it gets read, maintained and copied into new modules. Take it out early, while it is easy.
node ncube.js remove --listSix subsystems can go cleanly: permissions/RBAC, decimal money, reference pickers, the
blocking overlay, DataView, and dark mode. The command deletes the files, strips the
imports and barrel exports, drops the dependencies, removes the matching rule doc, and
then runs tsc --noEmit and tells you honestly whether it worked.
node ncube.js remove permissions --dry-run # see what it would touch
node ncube.js remove permissions # do itCommit first — that is what makes it revertible. Details:
docs/OPTIONAL_PARTS.md.
Four commands. All four must pass before a change is done — CI runs the same set.
npm run type-check # tsc --noEmit
npm run lint # eslint . — currently clean; keep it that way
npm test # Vitest: pure modules + component logic, ~1s
npm run build # catches what type-check alone cannotAnd the browser suite, before a PR:
npm run test:e2e # Playwright: the shared systems, desktop and mobileTwo layers on purpose. If an assertion depends on a real layout, a real animation or a
real navigation, jsdom cannot see it — those live in e2e/. It starts its own mock
backend, so it runs with nothing else running.
When you use "Use this template" on GitHub, you always get the full variant — everything included. No selection needed.
| Variant | Includes | How to get it |
|---|---|---|
full (default via template) |
Auth + full RBAC + access-control admin panel | Use GitHub "Use this template" |
rbac |
Auth + full RBAC, no admin panel | Use template → delete src/app/(dashboard)/access-control/ |
base |
Auth + layout shell, no RBAC | Use template → delete src/lib/permissions/ and src/app/(dashboard)/access-control/ |
For local bootstrapping (deprecated), the
createcommand still supports--variant base|rbac|full.
The ncube.js CLI mirrors the FastAPI fcube.py module generator. It scaffolds complete feature domains following the architecture conventions.
# Post-clone setup (name, .env, shadcn) — run once after cloning
node ncube.js init [my-app-name]
# Scaffold a new domain
node ncube.js startdomain Product
node ncube.js startdomain LeadManagement
node ncube.js startdomain InvoiceItem
# List existing domains
node ncube.js listdomains
# Install shadcn/ui components (included in init, but can run standalone)
node ncube.js setup
# (Deprecated) Bootstrap locally from the template directory
node ncube.js create my-app [--variant base|rbac|full]Running node ncube.js startdomain Product creates:
src/lib/product/
├── types.ts # Backend* + Frontend types + Zod schemas + status constants
├── transformers.ts # snake_case ↔ camelCase conversions
├── api.ts # Service functions via apiClient
├── hooks.ts # React Query hooks + query key factory
├── store.ts # Zustand UI state (modals, filters)
└── index.ts # Barrel exports
src/components/product/
├── product-list.tsx # DataTable with search + create button
├── product-form.tsx # Dialog with create/edit/view modes
└── index.ts # Barrel exports
src/app/(dashboard)/dashboard/product/
└── page.tsx # Feature page → /dashboard/product
After running, follow the printed checklist to:
- Add your backend field types to
types.ts - Map fields in
transformers.ts - Add form fields in
product-form.tsx - Register the route in
(dashboard)/config.ts
□ 1. node ncube.js startdomain <Name>
□ 2. Add backend + frontend types to src/lib/<name>/types.ts
□ 3. Complete transformers in src/lib/<name>/transformers.ts (asEnum on every enum)
□ 4. Add form fields in src/components/<name>/<name>-form.tsx (RHF + zod)
□ 5. Pass isDirty on the detail modal, and clear it on open
□ 6. Add PermissionedNavItem + ROUTES to src/app/(dashboard)/config.ts
□ 7. Add permission keys to src/lib/permissions/types.ts and helpers.ts
□ 8. If anything else picks this module by id, add it to REFERENCE_RESOURCES
□ 9. npm run type-check && npm run lint && npm test && npm run build
The full version, with the reasoning behind each step, is the New-feature checklist at
the bottom of CLAUDE.md.
Authentication uses the BFF (Backend-for-Frontend) pattern:
- Tokens are stored in httpOnly cookies — JavaScript never reads them
- The
/api/auth/*route handlers proxy auth to the backend and set cookies src/proxy.tsinjectsAuthorization: Bearer <token>for/api/v1/*routes- On 401:
apiClientauto-refreshes and retries once, then redirects to/login
| Route | Method | Description |
|---|---|---|
/api/auth/login |
POST | Proxy login, set httpOnly cookies (access: 2hr, refresh: 7d) |
/api/auth/me |
GET | Return current user, silently refresh if expired |
/api/auth/refresh |
POST | Rotate tokens, set new cookies |
/api/auth/logout |
POST | Clear cookies |
Built-in RBAC with 4 default roles. Customize in src/lib/permissions/config.ts.
// Permission format: "resource.action"
const canManage = usePermission("content.manage");
const canViewOrManage = useAnyPermission(["content.view", "content.manage"]);
// Gate a nav item
{ name: "Settings", href: "/dashboard/settings", permission: "settings.view" }Default roles: super_admin → admin → member → viewer
super_admin always passes all permission checks.
All design tokens live in src/app/globals.css — the single source of truth for colors, radius, and spacing. Tailwind v4 uses CSS-native configuration (no tailwind.config.js).
/* Customize in globals.css */
:root {
--primary: oklch(0.205 0 0); /* your brand color */
--radius: 0.625rem;
}Use semantic tokens in components — never hardcode hex colors:
className="bg-background text-foreground border-border"
className="text-primary bg-muted text-muted-foreground"All mutations use useBlockingMutation instead of raw useMutation. This automatically shows a global loading overlay during async operations.
// In hooks.ts — use this instead of useMutation
export function useCreateProduct() {
return useBlockingMutation(
{ mutationFn: productApi.createProduct, onSuccess: () => toast.success("Created") },
{ source: "mutation", label: "Creating product…" },
);
}| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_APP_NAME |
No | App display name |
NEXT_PUBLIC_APP_URL |
No | App URL (default: http://localhost:3000) |
NEXT_PUBLIC_API_URL |
Yes | Backend API base URL (e.g., http://localhost:8000) |
To brand the template for a specific project, update the CSS variables in src/app/globals.css:
:root {
--primary: oklch(0.6 0.2 250); /* your brand primary */
--radius: 0.75rem; /* border radius */
}
.dark {
--primary: oklch(0.7 0.2 250);
}Use oklch.com to find your brand colors in the oklch color space.
Three documents, three jobs:
| Document | What it is for |
|---|---|
CLAUDE.md |
The guardrail. Every convention in one page, plus a numbered anti-patterns list. Claude Code reads it automatically. |
docs/rules/ |
The rules, one per file. What each rule is, how it works here, what was deliberately left undone, and the commands that prove the file still matches the code. |
docs/FRONTEND_ARCHITECTURE_GUIDE_V3.md |
The narrative. How the pieces fit together, and the life of a request end to end. |
Each rule has exactly one home, in docs/rules/. Everything else states it briefly and links
there — so when a rule changes there is one file to edit.