# Build — Stack Reference > How Daniel Howells builds software. Personal development reference distilled from 15 active projects. > Source: https://github.com/howells/docs > Website: https://build.danielhowells.com ## Table of Contents - stack-overview.md - version-requirements.md - new-project-checklist.md - production-checklist.md - project-structure.md - dependencies.md - scripts.md - ruler.md - integrations/clerk-auth.md - integrations/drizzle-neon.md - integrations/trpc.md - integrations/env-validation.md - integrations/zustand.md - integrations/forms.md - integrations/resend-email.md - integrations/openrouter.md - integrations/fal-ai.md - integrations/voyage-embeddings.md - integrations/uploadthing.md - integrations/biome-ultracite.md - rules/code-style.md - rules/typescript.md - rules/react.md - rules/nextjs.md - rules/tailwind.md - rules/git.md - rules/testing.md - rules/env.md - rules/turborepo.md - rules/integrations.md - rules/interface/typography.md - rules/interface/animation.md - rules/interface/forms.md - rules/interface/interactions.md - rules/interface/layout.md - rules/interface/design.md - rules/interface/performance.md - rules/interface/content-accessibility.md --- ================================================================================ ## stack-overview.md ================================================================================ # Stack Overview The recommended production stack for new projects (January 2026). > **Note:** This document lists the *current recommended versions* for a complete, compatible stack. > For strict **minimum version requirements** enforced by agents, see **[rules/versions.md](./rules/versions.md)**. ## Core Framework | Layer | Technology | Version | Notes | |-------|------------|---------|-------| | Framework | Next.js | 16.1.x | App Router, Server Components | | React | React | 19.2.x | ref-as-prop, no forwardRef | | Language | TypeScript | 5.9.x | Strict mode | | Runtime | Node.js | >=20.9.0 | Required for Next.js 16 | ## Styling | Tool | Version | Notes | |------|---------|-------| | Tailwind CSS | 4.x | Config-free, PostCSS only | | @tailwindcss/postcss | 4.x | Required PostCSS plugin | | tailwind-merge | 3.x | Class merging utility | | tw-animate-css | 1.x | Animation utilities | | class-variance-authority | 0.7.x | Component variants | | clsx | 2.x | Conditional classes | ### Tailwind v4 Key Changes - No `tailwind.config.js`—use CSS-based config - Use `@source` directives for content paths - PostCSS plugin: `@tailwindcss/postcss` - Use `gray-*` palette only (not slate/zinc/neutral) ## Database | Tool | Version | Purpose | |------|---------|---------| | Drizzle ORM | 0.45.x | Type-safe ORM | | drizzle-kit | 0.31.x | Migrations/studio | | @neondatabase/serverless | 1.x | Serverless PostgreSQL | | postgres | 3.x | Node PostgreSQL driver | ### Alternative: Supabase For projects needing Supabase features (Auth, Realtime, Storage), use `@supabase/supabase-js` + `@supabase/ssr` instead of Neon. ## Authentication | Tool | Version | Notes | |------|---------|-------| | @clerk/nextjs | 6.x | Primary auth solution | | @clerk/testing | 1.x | Test utilities | | better-auth | 1.x | Self-hosted alternative | ## API Layer | Tool | Version | Purpose | |------|---------|---------| | @trpc/server | 11.x | Type-safe API | | @trpc/client | 11.x | Client bindings | | @trpc/tanstack-react-query | 11.x | React integration (new projects) | | zod | 4.x | Schema validation | | superjson | 2.x | Serialization | ## Data Fetching | Tool | Version | Notes | |------|---------|-------| | @tanstack/react-query | 5.90.x | Server state | | @tanstack/react-query-devtools | 5.x | Dev tools | ## UI Components | Tool | Version | Purpose | |------|---------|---------| | @base-ui/react | 1.x | Unstyled primitives (preferred) | | @radix-ui/* | Various | Accessible primitives (legacy) | | shadcn | 3.x | Component scaffolding | | lucide-react | 0.56x | Icons | | sonner | 2.x | Toasts | | cmdk | 1.x | Command palette | | vaul | 1.x | Drawer component | ### Component Strategy **Use shadcn for scaffolding, prefer Base UI primitives.** shadcn generates component code into your project—you own and customize it. When adding components: 1. **Check Base UI first** — Base UI components are unstyled and more flexible than Radix 2. **Fall back to Radix** — Only when Base UI doesn't have the primitive you need 3. **Customize freely** — shadcn components are yours to modify, not a dependency Base UI advantages over Radix: - Truly unstyled (no CSS reset battles) - More composable API - Smaller bundle size - Better TypeScript inference Example: For a new dropdown, use `@base-ui/react` Menu instead of `@radix-ui/react-dropdown-menu`. ## Animation | Tool | Version | Notes | |------|---------|-------| | framer-motion | 12.x | Full animation library | | motion | 12.x | Lighter alternative | ## AI/ML | Tool | Version | Purpose | |------|---------|---------| | ai (Vercel AI SDK) | 6.x | AI integration | | @openrouter/ai-sdk-provider | 1.x | Multi-model routing | | @fal-ai/client | 1.x | Image generation | | voyage-ai-provider | 3.x | Embeddings | ### Default Models | Use Case | Model | Provider | |----------|-------|----------| | Text/Chat | `google/gemini-2.5-flash` | OpenRouter | | Image Generation | `fal-ai/gpt-image-1.5` | fal.ai | | Image Generation (alt) | `fal-ai/gemini-25-flash-image` | fal.ai | | Text Embeddings | `voyage-3-large` | Voyage AI | | Image Embeddings | `voyage-multimodal-3` | Voyage AI | ## Code Quality | Tool | Version | Purpose | |------|---------|---------| | @biomejs/biome | 2.3.x | Formatting + linting | | ultracite | 7.x | Biome preset | | husky | 9.x | Git hooks | | lint-staged | 16.x | Staged file linting | | knip | 5.x | Unused code detection | ## Testing | Tool | Version | Purpose | |------|---------|---------| | @playwright/test | 1.57.x | E2E testing | | vitest | 4.x | Unit testing | | @testing-library/react | 16.x | Component testing | | jsdom | 27.x | DOM environment | ## Build Tools | Tool | Version | Purpose | |------|---------|---------| | turbo | 2.7.x | Monorepo orchestration | | tsx | 4.x | TypeScript execution | | tsdown | 0.15.x | Package bundling | ## File Uploads | Tool | Version | Notes | |------|---------|-------| | uploadthing | 7.x | File upload service | | @uploadthing/react | 7.x | React components | ## Misc Utilities | Tool | Purpose | |------|---------| | nanoid | ID generation | | dotenv / dotenv-cli | Environment variables | | next-themes | Theme switching | | zustand | Client state (when needed) | ================================================================================ ## version-requirements.md ================================================================================ # Version Requirements Minimum and mandatory versions for new projects as of January 2026. ## Mandatory Versions **The authoritative list of minimum version requirements is maintained in [rules/versions.md](./rules/versions.md).** Refer to that file for the strict "MUST" constraints enforced by AI agents. The section below explains the *reasoning* behind these choices. ### Rationale & Key Drivers | Package | Why Update? | |---------|-------------| | **next** | Turbopack stable, async params/headers, `use cache` directive | | **react** | `ref` as prop (no `forwardRef`), `use()` hook, Server Components | | **typescript** | Config inheritance, improved inference, performance | | **node** | Required runtime for Next.js 16+ | | **tailwindcss** | v4 is config-free, CSS-first, and significantly faster | | **zod** | v4 introduces breaking inference changes and better optionality handling | | **biome** | Replaces ESLint/Prettier with a single, faster tool | ## Breaking Changes to Know ### Next.js 15 → 16 - `next/headers` and `next/cookies` are now async (must await) - `params` and `searchParams` in page/layout are now Promises - Turbopack is default for dev (`next dev` uses Turbopack) - New `next.config.ts` support (TypeScript config) - **`proxy.ts` replaces `middleware.ts`** - Security-driven rename, Node.js runtime only - **`use cache` directive** - New explicit opt-in caching model - Edge runtime removed for proxy functions ```tsx // Old (Next 15) export default function Page({ params }: { params: { id: string } }) { return
{params.id}
} // New (Next 16) export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params return
{id}
} ``` ### React 18 → 19 - No more `forwardRef`—ref is a regular prop - `use()` hook for reading promises and context - Actions for form handling (`useActionState`, `useFormStatus`) - `` replaces `` ```tsx // Old (React 18) const Input = forwardRef((props, ref) => ( )) // New (React 19) function Input({ ref, ...props }: Props & { ref?: React.Ref }) { return } ``` ### Tailwind v3 → v4 - No `tailwind.config.js`—all config in CSS - Use `@theme` for design tokens, `@source` for content paths - PostCSS plugin changed to `@tailwindcss/postcss` - Only `gray-*` palette (not slate/zinc/neutral) - Native CSS cascade layers ```css /* app/globals.css - Tailwind v4 */ @import 'tailwindcss'; @source '../components/**/*.tsx'; @source '../app/**/*.tsx'; @theme { --font-sans: 'Inter', sans-serif; --color-primary: oklch(0.7 0.15 250); } ``` ### Zod 3 → 4 - `z.interface()` for key-optional vs value-optional distinction - Recursive types with getters (no more `z.lazy()` workarounds) - Unified error customization API - Breaking: `.catch()` and `.default()` on optional properties always return caught values ```ts // Zod 4 key optionality const schema = z.interface({ "name?": z.string(), // key optional: { name?: string } email: z.string().optional() // value optional: { email: string | undefined } }); // Recursive types with getters const Category = z.interface({ name: z.string(), get subcategories() { return z.array(Category); }, }); ``` ### tRPC 10 → 11 - New `initTRPC` API - React Query 5 integration required - Simplified client setup - Breaking: procedure builder changes ## Recommended Versions (Strongly Preferred) | Package | Version | Notes | |---------|---------|-------| | turbo | 2.7+ | Improved caching | | drizzle-orm | 0.45+ | Latest schema API | | @clerk/nextjs | 6+ | Next.js 16 support | | framer-motion | 12+ | Smaller bundle, new API | | playwright | 1.57+ | Latest browser engines | | vitest | 4+ | Faster, better ESM | ## Version Checking Commands ```bash # Check outdated packages pnpm outdated # Update all packages interactively pnpm update -i --latest # Check specific package version pnpm why next # Audit for vulnerabilities pnpm audit ``` ## Package.json Template ```json { "engines": { "node": ">=20.9.0" }, "packageManager": "pnpm@10.11.0" } ``` ## Upgrading Existing Projects Priority order for upgrades: 1. **React 19** - Foundation for everything else 2. **Next.js 16** - Requires React 19 3. **Tailwind v4** - Independent, can do anytime 4. **Zod 4** - May require schema updates 5. **tRPC 11** - Requires Zod 4, React Query 5 ### Codemod for Next.js Upgrade ```bash # Run Next.js codemods (includes middleware → proxy.ts migration) npx @next/codemod@latest upgrade latest ``` ### Codemod for React 19 ```bash # Remove forwardRef usage npx codemod react/19/replace-forward-ref ``` ================================================================================ ## new-project-checklist.md ================================================================================ # New Project Checklist Quick-start guide for bootstrapping a new project with your standard stack. ## Decision: Monorepo or Single App? | Criteria | Single App | Monorepo | |----------|------------|----------| | Timeline | Fast prototype | Long-term project | | Sharing | No code sharing needed | Multiple apps or packages | | Team | Solo or small team | Larger team or future growth | | Complexity | Simple domain | Complex domain | ## Option A: Single App (Fastest Start) ### 1. Create Next.js App ```bash pnpm create next-app@latest project-name --typescript --tailwind --app --src-dir --import-alias "@/*" cd project-name ``` ### 2. Install Core Dependencies ```bash # Styling pnpm add tailwind-merge class-variance-authority clsx tw-animate-css pnpm add -D @tailwindcss/postcss@4 # Database pnpm add drizzle-orm @neondatabase/serverless pnpm add -D drizzle-kit # Auth pnpm add @clerk/nextjs # Data fetching pnpm add @tanstack/react-query zod # UI pnpm add @base-ui/react lucide-react shadcn # Code quality pnpm add -D @biomejs/biome ultracite husky lint-staged @playwright/test vitest ``` ### 3. Configure Biome ```bash pnpm exec ultracite init ``` Or create `biome.json`: ```json { "$schema": "https://biomejs.dev/schemas/2.0.5/schema.json", "extends": ["ultracite"] } ``` ### 4. Setup Pre-commit Hooks ```bash pnpm add -D husky lint-staged pnpm exec husky init ``` Update `package.json`: ```json { "scripts": { "prepare": "husky", "typecheck": "tsc --noEmit" }, "lint-staged": { "*.{js,jsx,ts,tsx}": ["pnpm exec ultracite fix"], "*.{json,jsonc,css,md,mdx}": ["pnpm exec biome format --write"] } } ``` Update `.husky/pre-commit`: ```bash #!/bin/sh pnpm lint-staged pnpm typecheck ``` ### 5. Configure Tailwind v4 Create `postcss.config.mjs`: ```js export default { plugins: { "@tailwindcss/postcss": {} } }; ``` Update `src/app/globals.css`: ```css @import "tailwindcss"; @source "../**/*.{ts,tsx}"; ``` ### 6. Configure Drizzle Create `drizzle.config.ts`: ```ts import { defineConfig } from "drizzle-kit"; export default defineConfig({ schema: "./src/db/schema.ts", out: "./drizzle", dialect: "postgresql", dbCredentials: { url: process.env.DATABASE_URL!, }, }); ``` ### 7. Add Scripts Update `package.json`: ```json { "scripts": { "dev": "next dev --turbopack --port 4000", "build": "next build", "start": "next start", "lint": "biome check src --write", "format": "biome format src --write", "typecheck": "tsc --noEmit", "test": "vitest run", "test:e2e": "playwright test", "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio" } } ``` ### 8. Environment Variables Create `.env.local`: ```bash DATABASE_URL=postgresql://... NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_... CLERK_SECRET_KEY=sk_... ``` --- ## Option B: Monorepo (Better-T-Stack) The fastest way to scaffold a monorepo with your preferred stack: ```bash bunx create-better-t-stack project-name ``` Select options: - Database: Neon + Drizzle - Auth: Clerk - API: tRPC - Linting: Ultracite - Other: Turborepo ### Post-Setup ```bash cd project-name bun install bun run db:push bun run dev ``` --- ## File Templates ### CLAUDE.md (Project Rules) Create at project root—this tells AI assistants your conventions: ```markdown # Project Rules ## Code Style - Use Biome for formatting (no Prettier) - Use double quotes, semicolons, trailing commas - Prefer `for...of` over `.forEach()` - Use kebab-case filenames ## Canonical Code Principle - No backward compatibility layers - Update ALL usage sites when changing patterns - The current code IS the way ## TypeScript - Use `interface` for objects, `type` for unions - Never use `any`—prefer `unknown` with narrowing - Use `import type` for type-only imports ## React - Server Components by default - Extract business logic to hooks - Use @base-ui/react primitives - Use ref-as-prop (React 19) ## Database - Drizzle ORM + drizzle-kit - `db:push` for development - `db:generate` + `db:migrate` for production ## Testing - Playwright for E2E - Vitest for unit tests - Run tests before merging ``` ### .gitignore Additions ```gitignore # Environment .env .env.local .env*.local # Drizzle drizzle/ # Turbo .turbo # Next.js .next/ # Testing playwright-report/ test-results/ ``` ### tsconfig.json Paths ```json { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } } ``` --- ## Verification Checklist Before starting development: - [ ] `pnpm dev` starts without errors - [ ] `pnpm typecheck` passes - [ ] `pnpm lint` passes - [ ] Pre-commit hooks work (make a test commit) - [ ] Database connection works (`pnpm db:studio`) - [ ] Auth flow works (sign in/out) - [ ] CLAUDE.md created with project rules --- ## Updating Existing Projects ### Upgrade to Tailwind v4 1. Update dependencies: ```bash pnpm add -D tailwindcss@4 @tailwindcss/postcss@4 ``` 2. Replace `tailwind.config.js` with CSS config in `globals.css`: ```css @import "tailwindcss"; @source "../**/*.{ts,tsx}"; ``` 3. Update PostCSS config: ```js export default { plugins: { "@tailwindcss/postcss": {} } }; ``` 4. Remove old config files: ```bash rm tailwind.config.js tailwind.config.ts ``` ### Upgrade to React 19 1. Update dependencies: ```bash pnpm add react@19 react-dom@19 pnpm add -D @types/react@19 @types/react-dom@19 ``` 2. Replace `forwardRef` with ref-as-prop: ```tsx // Before const Button = forwardRef((props, ref) => { return