@dennisrongo/skills 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/README.md +23 -13
  2. package/package.json +1 -1
  3. package/skills/nextjs-app-router/SKILL.md +228 -175
  4. package/skills/nextjs-app-router/references/anti-patterns.md +163 -99
  5. package/skills/nextjs-app-router/references/folder-layout.md +79 -46
  6. package/skills/nextjs-app-router/references/good-patterns.md +233 -99
  7. package/skills/nextjs-app-router/references/templates/api-base.md +24 -21
  8. package/skills/nextjs-app-router/references/templates/auth-slice.md +82 -78
  9. package/skills/nextjs-app-router/references/templates/ci-and-hooks.md +58 -6
  10. package/skills/nextjs-app-router/references/templates/db-client.md +48 -0
  11. package/skills/nextjs-app-router/references/templates/env-and-utils.md +90 -23
  12. package/skills/nextjs-app-router/references/templates/feature-slice.md +110 -47
  13. package/skills/nextjs-app-router/references/templates/form-with-zod.md +23 -15
  14. package/skills/nextjs-app-router/references/templates/middleware.md +69 -59
  15. package/skills/nextjs-app-router/references/templates/next-config.md +4 -14
  16. package/skills/nextjs-app-router/references/templates/nextauth-config.md +178 -0
  17. package/skills/nextjs-app-router/references/templates/package.md +35 -5
  18. package/skills/nextjs-app-router/references/templates/prisma-schema.md +162 -0
  19. package/skills/nextjs-app-router/references/templates/providers-and-store.md +35 -21
  20. package/skills/nextjs-app-router/references/templates/root-layout.md +99 -20
  21. package/skills/nextjs-app-router/references/templates/route-group-layouts.md +46 -6
  22. package/skills/nextjs-app-router/references/templates/route-handler.md +168 -0
  23. package/skills/nextjs-app-router/references/templates/testing.md +105 -31
@@ -2,42 +2,79 @@
2
2
 
3
3
  Each entry below explains **what** the pattern looks like in the wild, **why** it's bad, and **what to do instead**. The skill must not emit any of these. If a user explicitly requests one, push back with the rationale before complying.
4
4
 
5
- ## 1. `'use client'` on the root page with a `useEffect(() => router.push(...))` redirect
5
+ > **About the SPA-style choice.** This project is deliberately API-driven, not SSR. Pages are `'use client'`. Data goes through RTK Query → Route Handlers. That choice eliminates a whole class of "is this server or client?" bugs, makes mocking trivial in tests, and matches how the team thinks about the app (frontend + backend with a clean HTTP seam). Several anti-patterns below exist *because* of that choice — they're not absolute rules in every Next.js project, but they are absolute rules in this one.
6
+
7
+ ## 1. `fetch()` or `await db.*` inside a server component
6
8
 
7
9
  ```tsx
8
- // app/page.tsx — BAD
9
- 'use client';
10
- import { useRouter } from 'next/navigation';
11
- import { useEffect } from 'react';
12
- export default function Home() {
13
- const router = useRouter();
14
- useEffect(() => { router.push('/dashboard'); });
10
+ // app/(app)/dashboard/page.tsx — BAD
11
+ export default async function DashboardPage() {
12
+ const stats = await db.stats.findMany();
13
+ return <Dashboard stats={stats} />;
15
14
  }
16
15
  ```
17
16
 
18
- **Why bad:**
19
- - The page renders blank HTML on first load (no SSR content), so Open Graph crawlers, SEO, and link-preview tools see nothing.
20
- - Causes a visible flash (empty page JS hydrate navigate).
21
- - Costs an extra client navigation that could have been a 308 from the server.
22
- - Defeats Next.js's built-in `redirect()` machinery.
17
+ **Why bad:** the app is API-driven by deliberate choice. SSR data fetching breaks the one-way data flow (UI → RTK Query → /api → DB), produces a different code path in production than in dev (server fetch vs. browser fetch), defeats RTK Query's caching/devtools/invalidation, and makes integration tests harder because you now have two ways to load data.
18
+
19
+ **Do instead:** make the page `'use client'` and call `useGetStatsQuery()` from `src/redux/api/statsApi.ts`. The matching `src/app/api/stats/route.ts` does the `db.*` call.
20
+
21
+ ## 2. `'use server'` directive (Server Actions)
22
+
23
+ ```tsx
24
+ // BAD
25
+ 'use server';
26
+ export async function createCustomer(formData: FormData) { ... }
27
+ ```
28
+
29
+ **Why bad:** Server Actions are a second mutation path competing with RTK Query mutations. The project picks one. Mixed projects develop "do we do this with an Action or a mutation?" cargo-culting, two error-handling stories, two loading-state stories, and two places to forget the auth check.
23
30
 
24
- **Do instead:**
31
+ **Do instead:** mutations are RTK Query mutations calling a Route Handler. The handler does `await auth()` + Zod validation + Prisma write.
32
+
33
+ ## 3. `async function Page` in any `page.tsx`
34
+
35
+ Pages are `'use client'` and synchronous. The body uses RTK Query hooks. If you see `export default async function ...Page(...)` you've reintroduced SSR data fetching by accident.
36
+
37
+ ## 4. `redirect()` from a server `page.tsx` as the auth gate
25
38
 
26
39
  ```tsx
27
- // app/page.tsx — GOOD
40
+ // BAD
41
+ import { auth } from '@/auth';
28
42
  import { redirect } from 'next/navigation';
29
- export default function Home() {
30
- redirect('/dashboard');
43
+ export default async function AppLayout({ children }) {
44
+ const session = await auth();
45
+ if (!session) redirect('/auth/login');
46
+ return <>{children}</>;
31
47
  }
32
48
  ```
33
49
 
34
- Or handle it in `middleware.ts` if the redirect target depends on auth/session.
50
+ **Why bad:** belt-and-suspenders auth fragmented across layout files. The gate is `middleware.ts`. A layout-level `redirect` runs *after* the page is requested (more work for nothing) and gives no consistent behavior across routes.
51
+
52
+ **Do instead:** `middleware.ts` re-exports (or wraps) `auth` from `src/auth.config.ts`. Unauthenticated requests to protected paths are 302'd before any layout renders. Layouts assume they're authenticated when they render.
35
53
 
36
- ## 2. `'use client'` on `app/layout.tsx` or route-group `layout.tsx`
54
+ ## 5. Custom JWT-in-`httpOnly`-cookie schemes alongside NextAuth
55
+
56
+ ```ts
57
+ // BAD — competing with NextAuth
58
+ cookies().set('session', signToken(user), { httpOnly: true, ... });
59
+ ```
37
60
 
38
- Marking a layout `'use client'` opts the entire subtree into client rendering and disables streaming, server data fetches, and `metadata` exports from descendants. Layouts should stay server components; push the client-only chrome (`AppShell`, `Sidebar`) into a `'use client'` child component imported from `_components/`.
61
+ **Why bad:** two auth systems in one app. NextAuth already manages the session cookie; rolling your own next to it gives you race conditions on logout (which cookie wins?), two refresh stories, two CSRF stories, and a guaranteed bug when the user signs out from one but not the other.
39
62
 
40
- ## 3. `serializableCheck: false` (or `immutableCheck: false`)
63
+ **Do instead:** NextAuth is the only auth system. If you need extra state on the session (role, org), add a `jwt` callback in `src/auth.ts` that augments the token.
64
+
65
+ ## 6. Decoding the NextAuth JWT manually on the client
66
+
67
+ ```tsx
68
+ // BAD
69
+ const token = document.cookie.match(/next-auth.session-token=([^;]+)/)?.[1];
70
+ const claims = jwtDecode(token!);
71
+ ```
72
+
73
+ **Why bad:** the cookie is `httpOnly` by default (it shouldn't even be readable from JS). Even if it weren't, decoding ≠ verifying. Use the official hook — it handles refresh, expiry, and SSR-safe rendering.
74
+
75
+ **Do instead:** `const { data: session } = useSession()` from `next-auth/react`. Server-side (Route Handlers): `const session = await auth()`.
76
+
77
+ ## 7. `serializableCheck: false` (or `immutableCheck: false`)
41
78
 
42
79
  ```ts
43
80
  // store.ts — BAD
@@ -51,74 +88,60 @@ middleware: (getDefaultMiddleware) =>
51
88
 
52
89
  ```ts
53
90
  serializableCheck: {
54
- ignoredPaths: ['auth.expiresAt'],
91
+ ignoredPaths: ['someFeature.someDate'],
55
92
  ignoredActions: ['someApi/executeMutation/fulfilled'],
56
93
  },
57
94
  ```
58
95
 
59
96
  …and leave a comment naming what's stored there and why it can't be a plain value.
60
97
 
61
- ## 4. `@ts-ignore` / `as any` in the store, providers, or API layer
98
+ ## 8. `@ts-ignore` / `as any` in the store, providers, API layer, or Route Handlers
62
99
 
63
100
  These are the load-bearing files of the entire app. If TypeScript is unhappy with them, the types are wrong — fix them. Use `satisfies`, narrow the inferred type, or extract a typed helper. `@ts-ignore` here hides real type breakage from refactors.
64
101
 
65
- ## 5. Commented-out reducers, endpoints, imports
102
+ ## 9. Commented-out reducers, endpoints, imports
66
103
 
67
- `store.ts` that imports `authApi` and `dashboardApi` but commented out the lines that register their reducers in `combineReducers` is a half-finished refactor pretending to be code. Either wire it up or delete it. The git history is the right place for "we used to do this."
104
+ `store.ts` that imports `authApi` and `dashboardApi` but commented out the lines that register their reducers is a half-finished refactor pretending to be code. Either wire it up or delete it. Git history is the right place for "we used to do this."
68
105
 
69
- ## 6. `moment` alongside `date-fns`
106
+ ## 10. `moment` alongside `date-fns`
70
107
 
71
- `moment` is in maintenance mode, mutable, and ships ~300 KB minified. `date-fns` is tree-shakeable, immutable, and ESM. Mixing them means two date libraries to learn, two timezone behaviors to reconcile, and two bundles in the user's tab. Pick `date-fns` for new projects.
108
+ `moment` is in maintenance mode, mutable, and ships ~300 KB minified. `date-fns` is tree-shakeable, immutable, and ESM. Pick `date-fns`.
72
109
 
73
- ## 7. `styled-components` (or Emotion) in a Tailwind project
110
+ ## 11. `styled-components` (or Emotion) in a Tailwind project
74
111
 
75
- The stack is Tailwind utilities + Radix primitives + `class-variance-authority` for variants + `cn()` for merging. Adding a CSS-in-JS runtime gives you two styling systems, two specificity stories, and a hydration cost. If a `styled-components` import sneaks in (often from copied legacy code), remove it.
112
+ The stack is Tailwind utilities + Radix primitives + `class-variance-authority` for variants + `cn()` for merging. Adding a CSS-in-JS runtime gives you two styling systems, two specificity stories, and a hydration cost.
76
113
 
77
- ## 8. Duplicate-by-case folders or files
114
+ ## 12. Duplicate-by-case folders or files
78
115
 
79
- `src/models/stateProjectReports/` and `src/models/stateprojectreport/` work on Windows (case-insensitive) and break on Linux (case-sensitive). Imports resolve to one or the other depending on which the bundler sees first, producing "works on my machine" bugs that surface only in CI or production. Enforce one canonical casing.
116
+ `src/models/StateReports/` and `src/models/statereports/` work on Windows (case-insensitive) and break on Linux (case-sensitive). Enforce one canonical casing.
80
117
 
81
- ## 9. `dangerouslySetInnerHTML` without server-side sanitization
118
+ ## 13. `dangerouslySetInnerHTML` without server-side sanitization
82
119
 
83
- ```tsx
84
- <div dangerouslySetInnerHTML={{ __html: apiResponse.body }} />
85
- ```
86
-
87
- If `apiResponse.body` is anything other than provably safe (e.g., a static template you control), this is an XSS vector. The fix is layered:
120
+ If `apiResponse.body` is anything other than provably safe, this is an XSS vector. Fix layered:
88
121
 
89
122
  1. Prefer rendering as JSX / Markdown via a strict renderer (no raw HTML).
90
- 2. If raw HTML is genuinely required, sanitize at the source — server-side allowlist, or DOMPurify on the client with a configured profile.
91
- 3. Add an inline comment naming the sanitization point so the next reviewer knows it isn't accidental.
123
+ 2. If raw HTML is genuinely required, sanitize at the source — server-side allowlist in the Route Handler, or DOMPurify on the client with a configured profile.
124
+ 3. Inline comment naming the sanitization point.
92
125
 
93
- A scaffolded project should not contain any `dangerouslySetInnerHTML` calls; if a feature genuinely needs one, add it with the comment above.
126
+ A scaffolded project should not contain any `dangerouslySetInnerHTML` calls.
94
127
 
95
- ## 10. `sessionStorage` / `localStorage` for non-transient state
128
+ ## 14. `sessionStorage` / `localStorage` for non-transient state
96
129
 
97
- `sessionStorage.setItem('wizardStep', JSON.stringify(state))` is a shortcut that almost always grows into a bug:
98
130
  - Not SSR-safe (`ReferenceError: sessionStorage is not defined`).
99
- - Survives tab refreshes but not new tabs — confusing for users.
100
- - Easy to forget to clear, leading to stale state in later sessions.
131
+ - Survives tab refreshes but not new tabs.
132
+ - Easy to forget to clear.
101
133
 
102
- **Do instead:**
103
- - Multi-step wizard state → URL search params (`useSearchParams`) so it's shareable and back-button-safe, or Redux if it must be shared across routes.
104
- - Token persistence → `httpOnly` cookies (server-set), never `localStorage`.
105
- - Truly transient UI state → component state.
134
+ **Do instead:** URL search params for shareable state, Redux for cross-route state, component state for transient UI. Tokens never go in `localStorage` — NextAuth's cookie is `httpOnly`.
106
135
 
107
- ## 11. JWT in a JS-accessible cookie or `localStorage`
136
+ ## 15. JWT in a JS-accessible cookie or `localStorage`
108
137
 
109
- A token in `js-cookie` or `localStorage` is reachable from any XSS payload that lands on the page. The mitigation is `httpOnly` cookies (the browser sends them with requests, but JS cannot read them):
138
+ A token in `localStorage` is reachable from any XSS payload. NextAuth's session cookie is `httpOnly` by default — leave it that way. Do not add `js-cookie` and write parallel auth state to a readable cookie.
110
139
 
111
- - The login endpoint should set the cookie via a server response header.
112
- - `middleware.ts` reads the cookie on the server to gate routes.
113
- - RTK Query sends the cookie automatically via `credentials: 'include'` on `fetch`.
140
+ ## 16. A `src/proxy.ts` (or equivalent) standing in for `middleware.ts`
114
141
 
115
- If the chosen backend cannot set cookies and forces a `Authorization: Bearer` flow with a client-readable token, document the XSS surface in the project README and treat every `dangerouslySetInnerHTML`, `eval`, or untrusted third-party script as a P1 risk.
142
+ Next.js has exactly one place that intercepts requests: `middleware.ts` at the project root. A `src/proxy.ts` that imports `next/server` and exports a function but isn't wired into the framework is dead code that *looks* like a guard. Either move its logic into `middleware.ts` or delete it.
116
143
 
117
- ## 12. A `src/proxy.ts` (or equivalent) standing in for `middleware.ts`
118
-
119
- Next.js has exactly one place that intercepts requests at the network boundary: `middleware.ts` at the project root. A file named `src/proxy.ts` that imports `next/server` and exports a function but isn't wired into the framework is dead code that *looks* like a guard. Either move its logic into `middleware.ts` or delete it.
120
-
121
- ## 13. Inline `fetch` in components
144
+ ## 17. Inline `fetch` in components
122
145
 
123
146
  ```tsx
124
147
  useEffect(() => {
@@ -126,78 +149,119 @@ useEffect(() => {
126
149
  }, []);
127
150
  ```
128
151
 
129
- This bypasses the cache, retries, error handling, devtools integration, and tag-based invalidation that RTK Query gives you for free. It also produces inconsistent loading/error UX across the app. All backend calls go through RTK Query. If a single specific case genuinely needs raw `fetch` (e.g., streaming a file download), justify it in a comment.
152
+ This bypasses the cache, retries, error handling, devtools, and tag-based invalidation that RTK Query gives you for free. **All** backend calls go through RTK Query.
130
153
 
131
- ## 14. Redux for local / URL / form state
154
+ ## 18. Redux for local / URL / form state
132
155
 
133
156
  If a piece of state is:
134
157
  - Only read inside one component → `useState` / `useReducer`.
135
158
  - Shareable via URL (filters, page numbers, selected tab) → `useSearchParams`.
136
159
  - Form values → React Hook Form's internal state.
160
+ - Session/user info → `useSession()` from `next-auth/react`.
137
161
 
138
- Reach for Redux only when state is genuinely shared across routes/components and isn't already covered by an RTK Query cache. `directoryResultsSlice`-style slices that hold the last list-view filter set are usually a smell.
162
+ Reach for Redux only when state is genuinely shared across routes and isn't already covered by an RTK Query cache or NextAuth.
139
163
 
140
- ## 15. Many independent `createApi(...)` instances
164
+ ## 19. Many independent `createApi(...)` instances
141
165
 
142
166
  ```ts
143
167
  export const authApi = createApi({ reducerPath: 'auth', ... });
144
168
  export const customersApi = createApi({ reducerPath: 'customers', ... });
145
- export const invoicesApi = createApi({ reducerPath: 'invoices', ... });
146
169
  ```
147
170
 
148
- Each instance has its own reducer path, middleware, cache, and tag-type set. They cannot invalidate each other's queries by tag. The right pattern is one base `api` with `injectEndpoints` per domain.
171
+ Each instance has its own cache and tag-type set; they can't invalidate each other. One base `api` with `injectEndpoints` per domain.
172
+
173
+ ## 20. `window.location.href = '/auth/login'` from inside `baseQuery` for 401
174
+
175
+ Loses unsaved form state, full page reload, race conditions when multiple in-flight requests 401 at once.
176
+
177
+ **Do instead:** call `signOut({ callbackUrl: '/auth/login', redirect: true })` from `next-auth/react` inside the base query's 401 branch. NextAuth clears the cookie and routes cleanly.
178
+
179
+ ## 21. Multiple `PrismaClient` instances
180
+
181
+ ```ts
182
+ // some-route.ts — BAD
183
+ const prisma = new PrismaClient();
184
+ ```
185
+
186
+ Every dev hot-reload spawns a new client and burns through Postgres connections. Use the singleton:
187
+
188
+ ```ts
189
+ // src/lib/db.ts
190
+ import { PrismaClient } from '@prisma/client';
191
+ const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
192
+ export const db = globalForPrisma.prisma ?? new PrismaClient();
193
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
194
+ ```
195
+
196
+ …and every handler imports `db` from there.
197
+
198
+ ## 22. Route Handlers without `await auth()`
199
+
200
+ ```ts
201
+ // app/api/customers/route.ts — BAD
202
+ export async function GET() {
203
+ const customers = await db.customer.findMany();
204
+ return Response.json(customers);
205
+ }
206
+ ```
207
+
208
+ Anyone with the URL can read every customer. **Every** handler (except `/api/auth/[...nextauth]` and explicit public endpoints like `/api/health`) calls `requireSession()` (or `await auth()`) first and 401s on absence. Use the helper in `src/lib/api-auth.ts`.
209
+
210
+ ## 23. Route Handlers that trust client-sent user IDs for ownership
211
+
212
+ ```ts
213
+ // BAD
214
+ const { userId, ...data } = await req.json();
215
+ await db.customer.create({ data: { ...data, userId } });
216
+ ```
217
+
218
+ A logged-in user can write under any other user's ID by lying in the request body. Read the user ID from the session:
219
+
220
+ ```ts
221
+ const session = await requireSession();
222
+ await db.customer.create({ data: { ...validated, userId: session.user.id } });
223
+ ```
224
+
225
+ Ownership reads use `where: { id, userId: session.user.id }` so a stranger's record returns 404 / null, not someone else's data.
149
226
 
150
- ## 16. `window.location.href = '/auth/login'` from inside `baseQuery` for 401
227
+ ## 24. `prisma db push` in CI or against shared databases
151
228
 
152
- It works, but:
153
- - Loses unsaved form state.
154
- - Forces a full page reload (re-downloads JS, re-hydrates Redux).
155
- - Race conditions when multiple in-flight requests all hit 401 at once.
229
+ `db push` skips the migration history. Two engineers running `db push` against different branches produce divergent schemas that aren't represented in `prisma/migrations/`. Use `prisma migrate dev` in development and `prisma migrate deploy` in CI. `db push` is only for local prototyping you're about to throw away.
156
230
 
157
- **Do instead:** dispatch a `logout` action; let the root reducer reset state cleanly; let `middleware.ts` (which now sees no session) handle the redirect on the next request, or call `router.replace('/auth/login')` from a single coordinator. Keep the hard `window.location` redirect only as a last-resort fallback.
231
+ ## 25. `prisma migrate reset` without explicit user confirmation
158
232
 
159
- ## 17. Missing `error.tsx` / `loading.tsx` / `not-found.tsx`
233
+ Drops the database. The skill never runs this unprompted. If the user asks for it, confirm and pause for a verbal "yes."
160
234
 
161
- The `(app)` group needs at minimum:
162
- - `loading.tsx` — shown by Suspense while server components fetch.
163
- - `error.tsx` — caught for runtime errors and RTK Query failures bubbling through `unwrap()`.
164
- - `not-found.tsx` — for 404s from `notFound()` calls and unmatched routes.
235
+ ## 26. Missing `error.tsx` / `loading.tsx` on the `(app)` group
165
236
 
166
- Without these, the user sees a blank screen or the global error boundary, both of which are worse UX than a typed empty state.
237
+ Without these the user sees a blank screen during RTK Query loads and the global error boundary on any unwrap failure.
167
238
 
168
- ## 18. No tests at all
239
+ ## 27. No tests at all
169
240
 
170
- A `package.json` with no `test` script tells you nobody runs anything before merging. Even a single reducer test, a single form-renders test, and a single Playwright login spec give CI something to enforce and break loudly when something regresses. The bar is "smoke tests exist," not "100% coverage."
241
+ A `package.json` with no `test` script means nothing gets enforced before merge. The bar is "smoke tests exist": one schema test, one component test, one E2E auth flow.
171
242
 
172
- ## 19. ESLint extending only `next/core-web-vitals`
243
+ ## 28. ESLint extending only `next/core-web-vitals`
173
244
 
174
- The Next preset is good but minimal. Add at least:
175
- - `@typescript-eslint/recommended` (or `recommended-type-checked` if your CI has the budget).
176
- - `plugin:react-hooks/recommended` — catches the missing-deps and Rules-of-Hooks bugs that `next/core-web-vitals` does not.
177
- - `plugin:jsx-a11y/recommended` — accessibility lint.
178
- - Project rules: `no-console: ['warn', { allow: ['warn', 'error'] }]`, `@typescript-eslint/no-explicit-any: 'error'`, `react-hooks/exhaustive-deps: 'error'`, `prefer-const: 'error'`.
245
+ Add `@typescript-eslint/recommended`, `react-hooks/recommended`, `jsx-a11y/recommended`, plus project rules (`no-console: warn`, `no-explicit-any: error`, `exhaustive-deps: error`, `prefer-const: error`).
179
246
 
180
- ## 20. Over-extended Tailwind config
247
+ ## 29. Over-extended Tailwind config
181
248
 
182
- `tailwind.config.ts` that adds 50+ custom spacing tokens, half of them commented out, and three custom animation timing functions, is design-system debt waiting to bite. Extend Tailwind for:
183
- - Brand colors (named with a project prefix).
184
- - Custom fonts.
185
- - Genuine design tokens you reuse.
249
+ 50+ custom spacing tokens, three custom animation timings design-system debt. Extend Tailwind only for brand colors, custom fonts, and genuine design tokens.
186
250
 
187
- Use the defaults for everything else.
251
+ ## 30. Per-feature client guards as the only auth check
188
252
 
189
- ## 21. Per-feature client guards as the only auth check
253
+ A `<AuthExpirationHandler>` running on an interval is **not** the primary guard. The primary guard is `middleware.ts`. Client guards exist to handle session expiry mid-app — belt-and-suspenders, not the gate.
190
254
 
191
- A `<AuthExpirationHandler>` that the app shell mounts and that runs `if (!token) router.push('/auth/login')` on an interval is a belt; it is not the only line of defense. The primary auth check is `middleware.ts`, server-side, before the route renders. Client guards exist for session expiry while the user is already inside the app.
255
+ ## 31. PM2 / `ecosystem.config.js` without considering simpler hosting
192
256
 
193
- ## 22. PM2 / `ecosystem.config.js` without considering simpler hosting
257
+ If the target is Vercel / Cloudflare / a container orchestrator, PM2 is unused weight. Add only when the user explicitly asks for VM-style hosting.
194
258
 
195
- If the target deploy is Vercel / Cloudflare / Netlify / any container orchestrator, the PM2 process file is unused weight. Add it only when the user explicitly asks for a VM-style `node`/`next start` deployment.
259
+ ## 32. Importing the Prisma adapter (or `@/lib/db`) inside `src/auth.config.ts`
196
260
 
197
- ## 23. Hand-rolled custom `proxy.ts` config when Next.js rewrites would do
261
+ `auth.config.ts` is consumed by `middleware.ts`, which runs on the Edge runtime. Edge cannot import the Prisma client (Node-only). If you accidentally import `@/lib/db` into `auth.config.ts`, the production build fails with a confusing "module not found in edge runtime" error.
198
262
 
199
- If the goal is "the frontend calls `/api/...` and Next.js forwards to a backend," use `next.config.ts` `rewrites()` not a custom request-interception file.
263
+ **Rule:** `auth.config.ts` has providers + callbacks only. `auth.ts` imports `auth.config.ts`, adds the adapter, and exports the runtime objects. Middleware imports from `auth.config.ts` (or from a thin Edge-safe shim).
200
264
 
201
- ## 24. Mixing client navigation libraries
265
+ ## 33. `'use server'` anywhere
202
266
 
203
- `next-nprogress-bar` is fine for the top loading bar; don't also pull in `nprogress` or a second router-event listener. Pick one progress UI.
267
+ Listed again because it matters. Grep `src/` for `'use server'` before reporting "done" any hit is a bug.
@@ -4,7 +4,7 @@ This is the source-of-truth tree for a new project. Every file/folder listed her
4
4
 
5
5
  ```
6
6
  {{project-name}}/
7
- ├── .env.example # committed; documents required env vars
7
+ ├── .env.example # committed; documents required env vars (no secrets)
8
8
  ├── .eslintrc.json # or eslint.config.mjs — strict TS + a11y + hooks
9
9
  ├── .gitignore
10
10
  ├── .husky/
@@ -12,10 +12,11 @@ This is the source-of-truth tree for a new project. Every file/folder listed her
12
12
  ├── .prettierrc
13
13
  ├── README.md
14
14
  ├── components.json # shadcn config; rsc: true, tsx: true
15
- ├── middleware.ts # AUTH GUARD. Wired via Next.js convention.
15
+ ├── docker-compose.yml # OPTIONAL only when user picked "local Docker" DB host
16
+ ├── middleware.ts # NextAuth-driven route gate. Project-root, framework convention.
16
17
  ├── next.config.ts
17
18
  ├── next-env.d.ts # generated; do not edit
18
- ├── package.json # scripts: dev, build, start, lint, typecheck, test, test:e2e
19
+ ├── package.json # scripts: dev, build, start, lint, typecheck, test, test:e2e, db:*
19
20
  ├── playwright.config.ts
20
21
  ├── postcss.config.js
21
22
  ├── tailwind.config.ts
@@ -24,50 +25,74 @@ This is the source-of-truth tree for a new project. Every file/folder listed her
24
25
 
25
26
  ├── .github/
26
27
  │ └── workflows/
27
- │ └── ci.yml # install → lint → typecheck → test → build
28
+ │ └── ci.yml # install → lint → typecheck → test → build (with Postgres service)
29
+
30
+ ├── prisma/
31
+ │ ├── schema.prisma # NextAuth tables (User/Account/Session/VerificationToken) + domain models
32
+ │ ├── migrations/ # generated; CHECKED IN — these are the source of truth
33
+ │ │ └── <timestamp>_init/
34
+ │ │ └── migration.sql
35
+ │ └── seed.ts # OPTIONAL — seeds test user(s) for Credentials provider
28
36
 
29
37
  ├── public/ # static assets only (favicons, robots.txt, og images)
30
38
  │ └── favicon.ico
31
39
 
32
40
  ├── src/
41
+ │ ├── auth.ts # NextAuth (Auth.js v5) full config — adapter + providers + callbacks
42
+ │ ├── auth.config.ts # Edge-safe config (no DB / no adapter imports) — for middleware
43
+ │ │
33
44
  │ ├── app/
34
- │ │ ├── layout.tsx # SERVER. <html><body><Providers>{children}
35
- │ │ ├── page.tsx # SERVER. redirect('/dashboard') or render landing.
36
- │ │ ├── error.tsx # global error boundary
45
+ │ │ ├── layout.tsx # SERVER — the ONE server component in the app. <html><body><Providers>
46
+ │ │ ├── error.tsx # global error boundary ('use client')
37
47
  │ │ ├── not-found.tsx
38
48
  │ │ ├── globals.css
39
49
  │ │ ├── favicon.ico # (optional duplicate of public/)
40
50
  │ │ │
51
+ │ │ ├── api/
52
+ │ │ │ ├── auth/
53
+ │ │ │ │ └── [...nextauth]/
54
+ │ │ │ │ └── route.ts # export { GET, POST } from NextAuth handlers
55
+ │ │ │ ├── <domain>/
56
+ │ │ │ │ ├── route.ts # GET (list) + POST (create) — both call requireSession()
57
+ │ │ │ │ └── [id]/
58
+ │ │ │ │ └── route.ts # GET + PATCH + DELETE
59
+ │ │ │ └── health/
60
+ │ │ │ └── route.ts # OPTIONAL — unauthenticated readiness probe
61
+ │ │ │
41
62
  │ │ ├── (public)/ # unauthenticated routes
42
63
  │ │ │ ├── layout.tsx # SERVER. minimal chrome (centered card).
43
64
  │ │ │ └── auth/
44
65
  │ │ │ ├── login/
45
- │ │ │ │ ├── page.tsx
46
- │ │ │ │ └── _components/LoginForm.tsx # 'use client'
47
- │ │ │ ├── signup/
48
- │ │ │ └── forgot-password/
66
+ │ │ │ │ ├── page.tsx # 'use client'
67
+ │ │ │ │ └── _components/LoginForm.tsx # 'use client'; calls signIn('credentials', {...})
68
+ │ │ │ ├── signup/ # OPTIONAL — only when Credentials + signup enabled
69
+ │ │ │ │ ├── page.tsx # 'use client'
70
+ │ │ │ │ └── _components/SignupForm.tsx
71
+ │ │ │ └── error/
72
+ │ │ │ └── page.tsx # NextAuth /auth/error landing
49
73
  │ │ │
50
74
  │ │ ├── (app)/ # authenticated routes
51
- │ │ │ ├── layout.tsx # SERVER. reads session, renders shell.
75
+ │ │ │ ├── layout.tsx # SERVER. renders <AppShell> client child. No DB/fetch here.
52
76
  │ │ │ ├── loading.tsx
53
77
  │ │ │ ├── error.tsx
54
78
  │ │ │ ├── _components/
55
79
  │ │ │ │ ├── AppShell.tsx # 'use client'; Sidebar + Header
56
- │ │ │ │ └── Sidebar.tsx
57
- │ │ │ ├── _hooks/
58
- │ │ │ │ └── useSessionExpiryWarning.ts
80
+ │ │ │ │ ├── Sidebar.tsx # 'use client'
81
+ │ │ │ │ └── UserMenu.tsx # 'use client'; uses useSession() + signOut()
59
82
  │ │ │ ├── dashboard/
60
- │ │ │ │ ├── page.tsx
83
+ │ │ │ │ ├── page.tsx # 'use client'; uses RTK Query hooks
61
84
  │ │ │ │ ├── loading.tsx
62
85
  │ │ │ │ └── _components/
63
86
  │ │ │ └── <feature>/
64
- │ │ │ ├── page.tsx
87
+ │ │ │ ├── page.tsx # 'use client'
88
+ │ │ │ ├── new/page.tsx # 'use client'; renders <FeatureForm mode="create" />
89
+ │ │ │ ├── [id]/page.tsx # 'use client'; renders <FeatureForm mode="edit" />
65
90
  │ │ │ ├── loading.tsx
66
- │ │ │ ├── schema.ts
91
+ │ │ │ ├── schema.ts # Zod — SHARED with the matching Route Handler
67
92
  │ │ │ ├── _components/
68
93
  │ │ │ └── _hooks/
69
94
  │ │ │
70
- │ │ └── (admin)/ # OPTIONAL: role-gated. middleware enforces role.
95
+ │ │ └── (admin)/ # OPTIONAL: role-gated. middleware enforces role claim.
71
96
  │ │ └── layout.tsx
72
97
  │ │
73
98
  │ ├── components/
@@ -86,24 +111,22 @@ This is the source-of-truth tree for a new project. Every file/folder listed her
86
111
  │ │ │ ├── FormSelectField.tsx
87
112
  │ │ │ ├── FormDatePickerField.tsx
88
113
  │ │ │ └── FormTextareaField.tsx
89
- │ │ ├── Notifications.tsx # toast-driven notification surface
90
- │ │ ├── UnsavedChangesWarning.tsx # in-app + beforeunload guard
114
+ │ │ ├── Notifications.tsx
115
+ │ │ ├── UnsavedChangesWarning.tsx
91
116
  │ │ └── AlertMessage.tsx
92
117
  │ │
93
118
  │ ├── redux/
94
- │ │ ├── store.ts # typed; combined reducer; logout resets state.
95
- │ │ ├── providers.tsx # 'use client'; <Provider> + <Toaster> + <ProgressBar>
119
+ │ │ ├── store.ts # typed; combined reducer; signOut wipes cache via NextAuth event.
120
+ │ │ ├── providers.tsx # 'use client'; <SessionProvider><Provider><Toaster><ProgressBar>
96
121
  │ │ ├── hooks.ts # typed useAppDispatch, useAppSelector
97
- │ │ ├── api/
98
- │ │├── api.ts # base createApi + auth-aware baseQuery
99
- │ │├── tags.ts # tag-type union
100
- │ │ │ ├── authApi.ts # injectEndpoints
101
- │ │ │ └── <domain>Api.ts
102
- │ │ └── features/
103
- │ │ ├── authSlice.ts
104
- │ │ └── <other>Slice.ts # only for shared async/global state
122
+ │ │ └── api/
123
+ │ │ ├── api.ts # base createApi; baseUrl: '/api'; credentials: 'include'
124
+ │ │ ├── tags.ts # tag-type union
125
+ │ │ └── <domain>Api.ts # one file per domain — injectEndpoints
105
126
  │ │
106
127
  │ ├── lib/
128
+ │ │ ├── db.ts # PrismaClient SINGLETON (HMR-safe)
129
+ │ │ ├── api-auth.ts # requireSession() + ownership helpers for Route Handlers
107
130
  │ │ ├── utils.ts # cn() — clsx + tailwind-merge
108
131
  │ │ ├── zod-utils.ts # getDefaultValuesFromSchema, unwrapZodEffects, getMaxLengthsFromSchema
109
132
  │ │ ├── formatters/
@@ -114,38 +137,48 @@ This is the source-of-truth tree for a new project. Every file/folder listed her
114
137
  │ │
115
138
  │ ├── config/
116
139
  │ │ └── env.ts # Zod-parsed runtime env. Fail fast at startup.
140
+ │ │ # Required: AUTH_SECRET, DATABASE_URL. Plus OAuth keys if chosen.
117
141
  │ │
118
- │ └── types/ # cross-cutting TS types only.
119
- └── api.ts # shared API envelope types if any
142
+ │ └── types/
143
+ ├── next-auth.d.ts # module augmentation adds id, role to Session.user
144
+ │ └── api.ts # shared API envelope types (if any)
120
145
 
121
146
  └── tests/
122
147
  ├── unit/
123
- │ ├── auth.slice.test.ts
124
- │ ├── login-form.test.tsx
148
+ │ ├── auth-helpers.test.ts # tests requireSession() with mocked auth()
149
+ │ ├── <feature>.schema.test.ts
125
150
  │ └── zod-utils.test.ts
126
151
  ├── e2e/
127
- │ └── auth.spec.ts
152
+ │ └── auth.spec.ts # log in (Credentials), land on dashboard, sign out
128
153
  └── setup.ts # @testing-library/jest-dom, vi globals
129
154
  ```
130
155
 
131
156
  ## Rules that govern the layout
132
157
 
133
- 1. **Server-by-default in `app/`.** A file gets `'use client'` only if it itself uses a client API. A page can be a server component that renders a `'use client'` child from `_components/`.
158
+ 1. **The root `app/layout.tsx` is the only server component that matters.** Every `page.tsx` is `'use client'`. Group layouts (`(app)/layout.tsx`, `(public)/layout.tsx`) stay server components but only because they render zero data they pass children through to a client shell. **No file under `src/app/**` outside `route.ts` calls `await db.*` or `fetch()` for data.**
159
+
160
+ 2. **`src/app/api/**/route.ts` is the backend.** Every handler imports from `@/auth` (or `@/lib/api-auth`) and `@/lib/db`. Every handler authenticates first. Inputs validated with the feature's Zod schema. Responses are JSON.
161
+
162
+ 3. **`_components/` and `_hooks/` are private.** Anything inside `_<name>/` may only be imported by files in the same route folder or its descendants. If two routes need the same component, lift it to `src/components/`.
163
+
164
+ 4. **One RTK Query base; many injected slices.** `redux/api/api.ts` is the only file that calls `createApi(...)`. All other `*Api.ts` files import that `api` and call `api.injectEndpoints(...)`. `baseUrl` is `'/api'` (same-origin) and `credentials: 'include'` lets the NextAuth session cookie ride along.
165
+
166
+ 5. **`middleware.ts` is the only auth gate at the network boundary.** It re-exports (or wraps) `auth` from `src/auth.config.ts` (Edge-safe — no DB imports). No `src/proxy.ts` or `src/middleware/*.ts` standing in for it.
134
167
 
135
- 2. **`_components/` and `_hooks/` are private.** Anything inside `_<name>/` may only be imported by files in the same route folder or its descendants. If two routes need the same component, lift it to `src/components/`.
168
+ 6. **`src/auth.ts` vs `src/auth.config.ts`.** Two files because the Edge runtime (where `middleware.ts` runs) can't import the Prisma adapter or any Node-only code. `auth.config.ts` holds providers + callbacks that are Edge-safe. `auth.ts` extends that config with the adapter and re-exports `{ handlers, auth, signIn, signOut }`. The split is mandatory for v5 + Prisma — don't collapse it.
136
169
 
137
- 3. **One RTK Query base; many injected slices.** `redux/api/api.ts` is the only file that calls `createApi(...)`. All other `*Api.ts` files import that `api` and call `api.injectEndpoints(...)`. Multiple `createApi` instances are forbidden — they fragment the cache and complicate invalidation.
170
+ 7. **Prisma migrations are committed.** `prisma/migrations/**` is checked in. CI runs `prisma migrate deploy`, not `prisma db push`. Never `prisma db push` in CI.
138
171
 
139
- 4. **Domain models live with their feature.** A `Customer` type used only by `(app)/customers/` lives in `(app)/customers/types.ts` or is inferred from the Zod schema. Only cross-cutting types belong in `src/types/`. Avoid a global `src/models/` dumping ground.
172
+ 8. **Domain models live with their feature.** A `Customer` type used only by `(app)/customers/` lives in `(app)/customers/types.ts` or is inferred from the Zod schema. Cross-cutting types in `src/types/`.
140
173
 
141
- 5. **`middleware.ts` is the only auth gate at the network boundary.** No `src/proxy.ts` or `src/middleware/*.ts` standing in for it. Client guards exist only to handle session expiry during an active session.
174
+ 9. **Feature schemas are local AND shared with handlers.** `(app)/<feature>/schema.ts` exports Zod schemas owned by that feature. The matching `app/api/<feature>/route.ts` imports the same schema for request validation. Same Zod, two sides.
142
175
 
143
- 6. **`tests/` mirrors `src/` loosely, not strictly.** Tests are organized by what they test, not by 1:1 directory parity.
176
+ 10. **`tests/` mirrors `src/` loosely.** Tests organized by what they test, not 1:1 parity.
144
177
 
145
- 7. **`public/` holds only static assets.** No code. Image components import from there via the standard `next/image` workflow.
178
+ 11. **`public/` holds only static assets.** No code.
146
179
 
147
- 8. **No duplicate-by-case paths.** Linux CI will break what works on Windows/macOS. The linter cannot catch this; the scaffolder must.
180
+ 12. **No duplicate-by-case paths.** Linux CI breaks what works on Windows/macOS.
148
181
 
149
- 9. **Feature schemas are local.** `(app)/<feature>/schema.ts` exports Zod schemas owned by that feature. They are not re-exported from `src/lib/`.
182
+ 13. **`components/ui/` is owned, not imported.** shadcn primitives are generated *into* the project.
150
183
 
151
- 10. **`components/ui/` is owned, not imported.** shadcn-style primitives are generated *into* the project and modified as needed. They are not a node_modules dependency.
184
+ 14. **No `'use server'` directive anywhere.** This project doesn't use Server Actions. Mutations go through RTK Query Route Handlers.