One rule explains the whole repository: thin routes, thick modules. Files under src/app/ only wire HTTP and UI to business logic; the logic itself lives in feature modules under src/modules/. This is what makes the codebase predictable for both humans and coding agents.
The module shape
Every feature module follows the same anatomy — src/modules/auth/ is the canonical example to copy:
src/modules/<feature>/
├── schema.ts # Drizzle tables this module owns
├── <feature>.ts # server logic: typed, JSDoc'd functions
├── <feature>-client.ts# only if the browser needs an API
├── components/ # feature-specific React components
└── index.ts # the public server API (barrel)Server code imports across modules through the barrel (@/modules/billing); client components import -client files directly. Modules in the box: auth, ai, billing, email, ratelimit, rag, blog, docs, admin.
Database workflow
- Define tables in the module's
schema.ts. - Re-export them from
src/db/schema.ts. pnpm db:generate— writes a SQL migration todrizzle/(works offline).pnpm db:migrate— applies it.
Columns are snake_case in Postgres, camelCase in TypeScript.
Environment
src/lib/env.ts declares and zod-validates every variable at startup — server-only, never import it into client code. New variable? Add it to the schema there and to .env.example with a placeholder.
The Next.js 16 gotcha
Middleware is now src/proxy.ts (exporting proxy(request)). It does an optimistic cookie check only; real session validation happens in the dashboard layout via requireSession(). When touching unfamiliar App Router APIs, the current docs ship inside node_modules/next/dist/docs/.
Conventions worth knowing
- TypeScript strict plus
noUncheckedIndexedAccess; noanyanywhere. - Every exported function carries a JSDoc block.
- Files are
kebab-case; componentsPascalCase; DBsnake_case. - After changes:
pnpm typecheck && pnpm lint, pluspnpm buildfor route/RSC changes.