@dennisrongo/skills 0.1.3 → 0.3.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 (30) hide show
  1. package/README.md +87 -38
  2. package/package.json +1 -1
  3. package/skills/code-review/SKILL.md +270 -0
  4. package/skills/codebase-explainer/SKILL.md +112 -0
  5. package/skills/diagnose/SKILL.md +31 -1
  6. package/skills/dotnet-onion-api/SKILL.md +1 -0
  7. package/skills/nextjs-app-router/SKILL.md +229 -175
  8. package/skills/nextjs-app-router/references/anti-patterns.md +163 -99
  9. package/skills/nextjs-app-router/references/folder-layout.md +79 -46
  10. package/skills/nextjs-app-router/references/good-patterns.md +233 -99
  11. package/skills/nextjs-app-router/references/templates/api-base.md +24 -21
  12. package/skills/nextjs-app-router/references/templates/auth-slice.md +82 -78
  13. package/skills/nextjs-app-router/references/templates/ci-and-hooks.md +58 -6
  14. package/skills/nextjs-app-router/references/templates/db-client.md +48 -0
  15. package/skills/nextjs-app-router/references/templates/env-and-utils.md +90 -23
  16. package/skills/nextjs-app-router/references/templates/feature-slice.md +110 -47
  17. package/skills/nextjs-app-router/references/templates/form-with-zod.md +23 -15
  18. package/skills/nextjs-app-router/references/templates/middleware.md +69 -59
  19. package/skills/nextjs-app-router/references/templates/next-config.md +4 -14
  20. package/skills/nextjs-app-router/references/templates/nextauth-config.md +178 -0
  21. package/skills/nextjs-app-router/references/templates/package.md +35 -5
  22. package/skills/nextjs-app-router/references/templates/prisma-schema.md +162 -0
  23. package/skills/nextjs-app-router/references/templates/providers-and-store.md +35 -21
  24. package/skills/nextjs-app-router/references/templates/root-layout.md +99 -20
  25. package/skills/nextjs-app-router/references/templates/route-group-layouts.md +46 -6
  26. package/skills/nextjs-app-router/references/templates/route-handler.md +168 -0
  27. package/skills/nextjs-app-router/references/templates/testing.md +105 -31
  28. package/skills/plan-and-build/SKILL.md +45 -3
  29. package/skills/pr-review/SKILL.md +50 -3
  30. package/skills/tauri-2-app/SKILL.md +1 -0
@@ -0,0 +1,178 @@
1
+ # NextAuth (Auth.js v5) configuration
2
+
3
+ Two files, by design:
4
+
5
+ - **`src/auth.config.ts`** — Edge-safe. Providers, callbacks, page mappings. **No DB imports.** Consumed by `middleware.ts`.
6
+ - **`src/auth.ts`** — full config. Imports `auth.config.ts`, adds the Prisma adapter, adds `authorize` for Credentials (which needs `db` + `bcryptjs`). Consumed by Route Handlers and server components.
7
+
8
+ The split is mandatory because middleware runs on the Edge runtime, which can't import the Prisma client (Node-only). Collapsing them produces a confusing "module not found in edge runtime" error at build time.
9
+
10
+ ## `src/auth.config.ts` (EDGE-SAFE)
11
+
12
+ ```ts
13
+ import type { NextAuthConfig } from 'next-auth';
14
+ import Credentials from 'next-auth/providers/credentials';
15
+ // import GitHub from 'next-auth/providers/github'; // uncomment if enabled in Step 2
16
+ // import Google from 'next-auth/providers/google'; // uncomment if enabled in Step 2
17
+
18
+ export default {
19
+ providers: [
20
+ Credentials({
21
+ // The `authorize` function lives in src/auth.ts (it needs DB + bcryptjs).
22
+ // Here we only declare the credentials shape so the provider is registered for type inference.
23
+ credentials: {
24
+ email: { label: 'Email', type: 'email' },
25
+ password: { label: 'Password', type: 'password' },
26
+ },
27
+ }),
28
+ // GitHub({ clientId: process.env.AUTH_GITHUB_ID!, clientSecret: process.env.AUTH_GITHUB_SECRET! }),
29
+ // Google({ clientId: process.env.AUTH_GOOGLE_ID!, clientSecret: process.env.AUTH_GOOGLE_SECRET! }),
30
+ ],
31
+ pages: {
32
+ signIn: '/auth/login',
33
+ error: '/auth/error',
34
+ },
35
+ callbacks: {
36
+ authorized({ auth, request: { nextUrl } }) {
37
+ const isLoggedIn = !!auth?.user;
38
+ const onPublic = nextUrl.pathname.startsWith('/auth');
39
+ const onApp = nextUrl.pathname.startsWith('/app');
40
+ const onAdmin = nextUrl.pathname.startsWith('/admin');
41
+
42
+ if (onPublic) {
43
+ return isLoggedIn ? Response.redirect(new URL('/app/dashboard', nextUrl)) : true;
44
+ }
45
+ if (onApp || onAdmin) {
46
+ return isLoggedIn; // false → redirect to pages.signIn
47
+ }
48
+ return true;
49
+ },
50
+ async jwt({ token, user }) {
51
+ if (user) {
52
+ token.id = user.id as string;
53
+ // Add `role` here if the User model has one:
54
+ // if ('role' in user) token.role = user.role as string;
55
+ }
56
+ return token;
57
+ },
58
+ async session({ session, token }) {
59
+ if (token?.id && session.user) {
60
+ session.user.id = token.id as string;
61
+ // if (token.role) session.user.role = token.role as string;
62
+ }
63
+ return session;
64
+ },
65
+ },
66
+ } satisfies NextAuthConfig;
67
+ ```
68
+
69
+ **Edge-safety checklist for this file:**
70
+
71
+ - No `import { db } from '@/lib/db'`.
72
+ - No `import { PrismaAdapter } from '@auth/prisma-adapter'`.
73
+ - No `import bcrypt from 'bcryptjs'`.
74
+ - No `import { auth } from '@/auth'` (circular).
75
+
76
+ ## `src/auth.ts` (full config, Node-only)
77
+
78
+ ```ts
79
+ import NextAuth from 'next-auth';
80
+ import Credentials from 'next-auth/providers/credentials';
81
+ import { PrismaAdapter } from '@auth/prisma-adapter';
82
+ import bcrypt from 'bcryptjs';
83
+ import { z } from 'zod';
84
+ import { db } from '@/lib/db';
85
+ import authConfig from './auth.config';
86
+
87
+ const credentialsSchema = z.object({
88
+ email: z.string().email(),
89
+ password: z.string().min(1),
90
+ });
91
+
92
+ export const { handlers, auth, signIn, signOut } = NextAuth({
93
+ ...authConfig,
94
+ adapter: PrismaAdapter(db),
95
+ // JWT strategy is REQUIRED when Credentials is one of the providers.
96
+ // Database sessions don't work with Credentials by NextAuth design.
97
+ session: { strategy: 'jwt' },
98
+ providers: [
99
+ // Re-declare Credentials here with the real `authorize` (which needs db + bcrypt).
100
+ Credentials({
101
+ credentials: {
102
+ email: { label: 'Email', type: 'email' },
103
+ password: { label: 'Password', type: 'password' },
104
+ },
105
+ async authorize(rawCredentials) {
106
+ const parsed = credentialsSchema.safeParse(rawCredentials);
107
+ if (!parsed.success) return null;
108
+
109
+ const user = await db.user.findUnique({
110
+ where: { email: parsed.data.email },
111
+ select: { id: true, email: true, name: true, image: true, password: true, role: true },
112
+ });
113
+ if (!user?.password) return null;
114
+
115
+ const ok = await bcrypt.compare(parsed.data.password, user.password);
116
+ if (!ok) return null;
117
+
118
+ return {
119
+ id: user.id,
120
+ email: user.email,
121
+ name: user.name,
122
+ image: user.image,
123
+ role: user.role,
124
+ };
125
+ },
126
+ }),
127
+ // Any OAuth providers from authConfig.providers carry over via the spread above —
128
+ // re-declare them here only if they need server-only secrets that auth.config.ts couldn't see.
129
+ ],
130
+ });
131
+ ```
132
+
133
+ ## `src/app/api/auth/[...nextauth]/route.ts`
134
+
135
+ ```ts
136
+ export { GET, POST } from '@/auth';
137
+ ```
138
+
139
+ That's the entire file. NextAuth's `handlers` is `{ GET, POST }`, and `auth.ts` re-exports them. The catch-all route gets every `/api/auth/*` request (sign-in, callback, CSRF, session, etc.).
140
+
141
+ ## `src/types/next-auth.d.ts`
142
+
143
+ ```ts
144
+ import 'next-auth';
145
+ import 'next-auth/jwt';
146
+
147
+ declare module 'next-auth' {
148
+ interface Session {
149
+ user: {
150
+ id: string;
151
+ email: string;
152
+ name?: string | null;
153
+ image?: string | null;
154
+ role?: string;
155
+ };
156
+ }
157
+
158
+ interface User {
159
+ id: string;
160
+ role?: string;
161
+ }
162
+ }
163
+
164
+ declare module 'next-auth/jwt' {
165
+ interface JWT {
166
+ id: string;
167
+ role?: string;
168
+ }
169
+ }
170
+ ```
171
+
172
+ Without this file, TypeScript thinks `session.user.id` is `undefined`.
173
+
174
+ ## Forbidden in `auth.config.ts`
175
+
176
+ - Importing `@/lib/db`, `@/auth`, `@auth/prisma-adapter`, `bcryptjs`, or anything that pulls in Node-only modules.
177
+ - Defining the Credentials `authorize` here (it needs the DB).
178
+ - Any logic that calls `cookies()` or `headers()` from `next/headers` — those are server-only.
@@ -19,13 +19,27 @@ Resolve **every** version via context7 at scaffold time. Versions below are plac
19
19
  "test": "vitest run",
20
20
  "test:watch": "vitest",
21
21
  "test:e2e": "playwright test",
22
+ "db:generate": "prisma generate",
23
+ "db:migrate": "prisma migrate dev",
24
+ "db:migrate:deploy": "prisma migrate deploy",
25
+ "db:studio": "prisma studio",
26
+ "db:seed": "tsx prisma/seed.ts",
22
27
  "prepare": "husky"
23
28
  },
29
+ "prisma": {
30
+ "seed": "tsx prisma/seed.ts"
31
+ },
24
32
  "dependencies": {
25
33
  "next": "^x.y.z",
26
34
  "react": "^x.y.z",
27
35
  "react-dom": "^x.y.z",
28
36
 
37
+ "next-auth": "^x.y.z",
38
+ "@auth/prisma-adapter": "^x.y.z",
39
+ "bcryptjs": "^x.y.z",
40
+
41
+ "@prisma/client": "^x.y.z",
42
+
29
43
  "@reduxjs/toolkit": "^x.y.z",
30
44
  "react-redux": "^x.y.z",
31
45
 
@@ -54,6 +68,10 @@ Resolve **every** version via context7 at scaffold time. Versions below are plac
54
68
  "@types/node": "^x.y.z",
55
69
  "@types/react": "^x.y.z",
56
70
  "@types/react-dom": "^x.y.z",
71
+ "@types/bcryptjs": "^x.y.z",
72
+
73
+ "prisma": "^x.y.z",
74
+ "tsx": "^x.y.z",
57
75
 
58
76
  "tailwindcss": "^x.y.z",
59
77
  "postcss": "^x.y.z",
@@ -81,19 +99,31 @@ Resolve **every** version via context7 at scaffold time. Versions below are plac
81
99
  },
82
100
  "lint-staged": {
83
101
  "*.{ts,tsx}": ["prettier --write", "eslint --fix"],
84
- "*.{json,md,css,yml,yaml}": ["prettier --write"]
102
+ "*.{json,md,css,yml,yaml,prisma}": ["prettier --write"]
85
103
  }
86
104
  }
87
105
  ```
88
106
 
89
- **Do NOT include:**
107
+ ## Notes on version resolution
108
+
109
+ - **`next-auth`**: Auth.js v5 (the version with the `handlers`/`auth`/`signIn`/`signOut` exports). Check context7 for the current version line — at certain points this is published under a `@beta` tag. Quote the resolved version back to the user before installing.
110
+ - **`bcryptjs` vs `bcrypt` vs `@node-rs/argon2`**: pure-JS `bcryptjs` is the safest default — it works on every Node version and in serverless without native bindings. Switch to `@node-rs/argon2` only if the user explicitly asks for Argon2 and confirms their deploy target supports native bindings.
111
+ - **`tsx`** is for running `prisma/seed.ts` and any TypeScript scripts without a build step.
112
+
113
+ ## Do NOT include
114
+
90
115
  - `moment` (use `date-fns`)
91
- - `styled-components` / `@emotion/*` (project is Tailwind-only)
116
+ - `styled-components` / `@emotion/*` (Tailwind only)
92
117
  - `nprogress` (use `next-nprogress-bar`)
93
- - A `proxy` library at the application layer (use Next.js `rewrites()` in `next.config.ts`)
118
+ - `jsonwebtoken` or `jose` for the application layer NextAuth handles JWT signing/verification internally
119
+ - `js-cookie` — NextAuth manages the session cookie
120
+ - A second auth library (`Clerk`, `Lucia`, `Iron Session`, etc.) alongside NextAuth
121
+ - A second ORM (`Drizzle`, `Kysely`, `TypeORM`) alongside Prisma
122
+
123
+ ## Add only if the user opted in during Step 2
94
124
 
95
- **Add only if the user opted in during Step 2:**
96
125
  - `pusher-js` (real-time)
97
126
  - Storybook packages
98
127
  - `next-intl` / `next-i18next` (i18n)
99
128
  - `@sentry/nextjs` (error tracking)
129
+ - `@auth/core` providers not in the default install (e.g. specific OAuth providers shipped separately)
@@ -0,0 +1,162 @@
1
+ # Prisma schema (NextAuth + domain)
2
+
3
+ One `schema.prisma` file holds the NextAuth tables (so the Prisma adapter works) plus your domain models. Verify field shapes against the current `@auth/prisma-adapter` docs via context7 at scaffold time — the adapter's required columns evolve.
4
+
5
+ ## `prisma/schema.prisma`
6
+
7
+ ```prisma
8
+ // This file is the source of truth for the database schema.
9
+ // Every change goes through `pnpm exec prisma migrate dev --name <slug>`.
10
+
11
+ generator client {
12
+ provider = "prisma-client-js"
13
+ }
14
+
15
+ datasource db {
16
+ provider = "postgresql"
17
+ url = env("DATABASE_URL")
18
+ }
19
+
20
+ // ---------- NextAuth tables ----------
21
+ // Shape required by @auth/prisma-adapter. Verify against current adapter docs.
22
+
23
+ model User {
24
+ id String @id @default(cuid())
25
+ name String?
26
+ email String @unique
27
+ emailVerified DateTime?
28
+ image String?
29
+
30
+ // Required when the Credentials provider is enabled.
31
+ // OAuth-only users won't have a password — keep it nullable.
32
+ password String?
33
+
34
+ // Custom: optional role for (admin) gating.
35
+ role String? @default("user")
36
+
37
+ accounts Account[]
38
+ sessions Session[]
39
+
40
+ // Domain relations:
41
+ customers Customer[]
42
+
43
+ createdAt DateTime @default(now())
44
+ updatedAt DateTime @updatedAt
45
+ }
46
+
47
+ model Account {
48
+ id String @id @default(cuid())
49
+ userId String
50
+ type String
51
+ provider String
52
+ providerAccountId String
53
+ refresh_token String? @db.Text
54
+ access_token String? @db.Text
55
+ expires_at Int?
56
+ token_type String?
57
+ scope String?
58
+ id_token String? @db.Text
59
+ session_state String?
60
+
61
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
62
+
63
+ @@unique([provider, providerAccountId])
64
+ @@index([userId])
65
+ }
66
+
67
+ model Session {
68
+ // Present even when session.strategy = 'jwt' — the adapter still references it.
69
+ id String @id @default(cuid())
70
+ sessionToken String @unique
71
+ userId String
72
+ expires DateTime
73
+
74
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
75
+
76
+ @@index([userId])
77
+ }
78
+
79
+ model VerificationToken {
80
+ identifier String
81
+ token String @unique
82
+ expires DateTime
83
+
84
+ @@unique([identifier, token])
85
+ }
86
+
87
+ // ---------- Domain models ----------
88
+
89
+ model Customer {
90
+ id String @id @default(cuid())
91
+ userId String
92
+ name String
93
+ email String
94
+
95
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
96
+
97
+ createdAt DateTime @default(now())
98
+ updatedAt DateTime @updatedAt
99
+
100
+ @@index([userId])
101
+ }
102
+ ```
103
+
104
+ ## `prisma/seed.ts` (OPTIONAL — only if Credentials and a test user was requested)
105
+
106
+ ```ts
107
+ import { PrismaClient } from '@prisma/client';
108
+ import bcrypt from 'bcryptjs';
109
+
110
+ const db = new PrismaClient();
111
+
112
+ async function main() {
113
+ const password = await bcrypt.hash('test1234', 10);
114
+
115
+ await db.user.upsert({
116
+ where: { email: 'test@example.com' },
117
+ update: {},
118
+ create: {
119
+ email: 'test@example.com',
120
+ name: 'Test User',
121
+ password,
122
+ role: 'admin',
123
+ },
124
+ });
125
+
126
+ console.log('Seeded test@example.com / test1234');
127
+ }
128
+
129
+ main()
130
+ .catch((e) => {
131
+ console.error(e);
132
+ process.exit(1);
133
+ })
134
+ .finally(() => db.$disconnect());
135
+ ```
136
+
137
+ Run with `pnpm db:seed`. **Never seed production.**
138
+
139
+ ## Migration commands
140
+
141
+ ```bash
142
+ # Dev (creates a new migration + applies it):
143
+ pnpm exec prisma migrate dev --name init
144
+
145
+ # CI / prod (applies committed migrations without prompting):
146
+ pnpm exec prisma migrate deploy
147
+
148
+ # Regenerate the typed client after a schema change (migrate dev does this for you):
149
+ pnpm exec prisma generate
150
+
151
+ # Inspect the DB visually (dev only):
152
+ pnpm exec prisma studio
153
+ ```
154
+
155
+ ## Forbidden
156
+
157
+ - `prisma db push` in CI or against any shared database. Use `prisma migrate dev` (development) or `prisma migrate deploy` (everything else).
158
+ - Running `prisma migrate reset` without explicit user confirmation — it drops the DB.
159
+ - Editing migration files after they've been applied to any non-throwaway database. Create a follow-up migration instead.
160
+ - Adding domain tables that reference `Account` or `Session` directly. Those are NextAuth's tables; reference `User` instead.
161
+ - Storing OAuth tokens or sensitive secrets in domain models — they belong in `Account` (which NextAuth populates).
162
+ - Plain-text passwords. Always `bcrypt.hash(password, 10)` (or higher cost factor) before storing.
@@ -1,10 +1,11 @@
1
1
  # Providers, store, typed hooks
2
2
 
3
- ## `src/redux/providers.tsx` (CLIENT — this one must be client because of `<Provider store>`)
3
+ ## `src/redux/providers.tsx` (CLIENT)
4
4
 
5
5
  ```tsx
6
6
  'use client';
7
7
 
8
+ import { SessionProvider } from 'next-auth/react';
8
9
  import { Provider } from 'react-redux';
9
10
  import { AppProgressBar as ProgressBar } from 'next-nprogress-bar';
10
11
  import { Toaster } from '@/components/ui/toaster';
@@ -12,39 +13,47 @@ import { store } from './store';
12
13
 
13
14
  export function Providers({ children }: { children: React.ReactNode }) {
14
15
  return (
15
- <Provider store={store}>
16
- {children}
17
- <Toaster />
18
- <ProgressBar
19
- height="3px"
20
- color="hsl(var(--primary))"
21
- shallowRouting
22
- disableSameURL
23
- options={{ showSpinner: false }}
24
- />
25
- </Provider>
16
+ <SessionProvider>
17
+ <Provider store={store}>
18
+ {children}
19
+ <Toaster />
20
+ <ProgressBar
21
+ height="3px"
22
+ color="hsl(var(--primary))"
23
+ shallowRouting
24
+ disableSameURL
25
+ options={{ showSpinner: false }}
26
+ />
27
+ </Provider>
28
+ </SessionProvider>
26
29
  );
27
30
  }
28
31
  ```
29
32
 
33
+ **Order matters:** `<SessionProvider>` outside so any component (including Redux-connected ones) can `useSession()`. Redux inside so RTK Query and dispatches work normally.
34
+
30
35
  ## `src/redux/store.ts`
31
36
 
32
37
  ```ts
33
38
  import { combineReducers, configureStore, type Action } from '@reduxjs/toolkit';
34
39
  import { api } from './api/api';
35
- import { authApi } from './api/authApi';
36
- import { authSlice } from './features/authSlice';
37
40
 
38
41
  const combinedReducer = combineReducers({
39
42
  [api.reducerPath]: api.reducer,
40
- auth: authSlice.reducer,
43
+ // Add additional slices here ONLY for genuinely shared async/global state.
44
+ // Auth/session is NOT a slice — it lives in NextAuth's SessionProvider.
41
45
  });
42
46
 
43
47
  type CombinedState = ReturnType<typeof combinedReducer>;
44
48
 
49
+ const SIGN_OUT = '__signout__' as const;
50
+
51
+ export const signOutEvent = { type: SIGN_OUT } as const;
52
+
45
53
  const rootReducer = (state: CombinedState | undefined, action: Action): CombinedState => {
46
- // Wipe the entire store on logout. RTK Query cache, auth, everything.
47
- if (authApi.endpoints.logout.matchFulfilled(action)) {
54
+ // Wipe the entire store when the app dispatches signOutEvent (called from the
55
+ // baseQuery 401 handler before invoking next-auth's signOut).
56
+ if (action.type === SIGN_OUT) {
48
57
  return combinedReducer(undefined, action);
49
58
  }
50
59
  return combinedReducer(state, action);
@@ -55,8 +64,7 @@ export const store = configureStore({
55
64
  middleware: (getDefaultMiddleware) =>
56
65
  getDefaultMiddleware({
57
66
  // Keep serializableCheck ON. If a specific value is genuinely non-serializable,
58
- // add it here with a comment explaining why.
59
- // serializableCheck: { ignoredPaths: ['auth.expiresAt'] },
67
+ // add { ignoredPaths: ['...'] } here with a comment explaining why.
60
68
  }).concat(api.middleware),
61
69
  });
62
70
 
@@ -64,7 +72,7 @@ export type RootState = ReturnType<typeof store.getState>;
64
72
  export type AppDispatch = typeof store.dispatch;
65
73
  ```
66
74
 
67
- **No `@ts-ignore`. No `serializableCheck: false`. No commented-out reducers.**
75
+ **No `@ts-ignore`. No `serializableCheck: false`. No commented-out reducers. No auth slice — NextAuth owns session state.**
68
76
 
69
77
  ## `src/redux/hooks.ts`
70
78
 
@@ -76,4 +84,10 @@ export const useAppDispatch: () => AppDispatch = useDispatch;
76
84
  export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
77
85
  ```
78
86
 
79
- Components import `useAppDispatch` / `useAppSelector` — never the raw `useDispatch` / `useSelector` from `react-redux`. ESLint can enforce this with `no-restricted-imports` if desired.
87
+ Components import `useAppDispatch` / `useAppSelector` — never the raw `useDispatch` / `useSelector` from `react-redux`. ESLint can enforce this with `no-restricted-imports`.
88
+
89
+ ## Why no `authSlice`
90
+
91
+ NextAuth's `SessionProvider` is the single source of session truth. Components read it with `useSession()`. Duplicating that into a Redux slice creates two sources of truth that drift apart (the slice doesn't know when NextAuth refreshes the JWT, etc.) and doubles the work on sign-in/out.
92
+
93
+ If you need session-derived data inside a Redux slice (rare — typically you'd put it in the request itself), select from `useSession()` at the component boundary and pass it as an action payload.
@@ -1,6 +1,6 @@
1
- # Root layout, root page, error/not-found templates
1
+ # Root layout, error/not-found, login page
2
2
 
3
- ## `src/app/layout.tsx` (SERVER component)
3
+ ## `src/app/layout.tsx` (SERVER component — the only one)
4
4
 
5
5
  ```tsx
6
6
  import '@/app/globals.css';
@@ -26,35 +26,51 @@ export default function RootLayout({ children }: { children: React.ReactNode })
26
26
  }
27
27
  ```
28
28
 
29
- **NO `'use client'`. NO `useEffect`. NO `useRouter`.** The root layout is a server component.
29
+ **NO `'use client'`. NO data fetching. NO `await db.*`.** This is the only server component in the app and its job is to render `<Providers>`.
30
30
 
31
- ## `src/app/page.tsx` (SERVER component, redirect-only)
31
+ ## No `src/app/page.tsx` by default
32
32
 
33
- If the project has an authenticated landing route, the root page is a server `redirect`:
33
+ `middleware.ts` handles `/`:
34
+ - Signed-in users → `/app/dashboard` (302).
35
+ - Signed-out users → `/auth/login` (302).
36
+
37
+ Don't create an `app/page.tsx` unless the project has a public landing page. If it does, the page is `'use client'`:
34
38
 
35
39
  ```tsx
36
- import { redirect } from 'next/navigation';
40
+ // src/app/page.tsx ONLY if a marketing landing is needed
41
+ 'use client';
42
+ import Link from 'next/link';
43
+ import { Button } from '@/components/ui/button';
37
44
 
38
- export default function Home() {
39
- redirect('/dashboard');
45
+ export default function LandingPage() {
46
+ return (
47
+ <main className="flex min-h-screen flex-col items-center justify-center gap-6">
48
+ <h1 className="text-4xl font-bold">{{Project Title}}</h1>
49
+ <Button asChild>
50
+ <Link href="/auth/login">Sign in</Link>
51
+ </Button>
52
+ </main>
53
+ );
40
54
  }
41
55
  ```
42
56
 
43
- If the project has a marketing landing page, render content instead — still as a server component, no `'use client'`.
44
-
45
- **Forbidden:**
57
+ **Forbidden** (do not generate):
46
58
 
47
59
  ```tsx
48
- // ❌ DO NOT generate this.
49
- 'use client';
50
- import { useRouter } from 'next/navigation';
51
- import { useEffect } from 'react';
52
- export default function Home() {
53
- const router = useRouter();
54
- useEffect(() => router.push('/dashboard'));
60
+ // ❌ server page doing a redirect
61
+ import { redirect } from 'next/navigation';
62
+ export default function Home() { redirect('/dashboard'); }
63
+
64
+ // server page doing data fetching
65
+ export default async function Home() {
66
+ const session = await auth();
67
+ const data = await db.x.findMany();
68
+ return <Dashboard data={data} session={session} />;
55
69
  }
56
70
  ```
57
71
 
72
+ The middleware handles redirects. RTK Query handles data.
73
+
58
74
  ## `src/app/error.tsx` (CLIENT — Next requires it)
59
75
 
60
76
  ```tsx
@@ -85,9 +101,11 @@ export default function GlobalError({
85
101
  }
86
102
  ```
87
103
 
88
- ## `src/app/not-found.tsx` (SERVER component)
104
+ ## `src/app/not-found.tsx` (CLIENT, since no SSR)
89
105
 
90
106
  ```tsx
107
+ 'use client';
108
+
91
109
  import Link from 'next/link';
92
110
  import { Button } from '@/components/ui/button';
93
111
 
@@ -103,7 +121,68 @@ export default function NotFound() {
103
121
  }
104
122
  ```
105
123
 
106
- ## `src/app/globals.css` (Tailwind layer + shadcn CSS variables)
124
+ ## `src/app/(public)/auth/login/page.tsx` (CLIENT, calls NextAuth `signIn`)
125
+
126
+ ```tsx
127
+ 'use client';
128
+
129
+ import { useState } from 'react';
130
+ import { useRouter, useSearchParams } from 'next/navigation';
131
+ import { signIn } from 'next-auth/react';
132
+ import { useForm } from 'react-hook-form';
133
+ import { zodResolver } from '@hookform/resolvers/zod';
134
+ import { z } from 'zod';
135
+ import { Form } from '@/components/ui/form';
136
+ import { Button } from '@/components/ui/button';
137
+ import { FormInputField } from '@/components/forms/FormInputField';
138
+ import { FormPasswordField } from '@/components/forms/FormPasswordField';
139
+
140
+ const loginSchema = z.object({
141
+ email: z.string().email(),
142
+ password: z.string().min(1, 'Required'),
143
+ });
144
+
145
+ type LoginFormValues = z.infer<typeof loginSchema>;
146
+
147
+ export default function LoginPage() {
148
+ const router = useRouter();
149
+ const params = useSearchParams();
150
+ const returnTo = params.get('returnTo') ?? '/app/dashboard';
151
+ const [error, setError] = useState<string | null>(null);
152
+
153
+ const form = useForm<LoginFormValues>({
154
+ resolver: zodResolver(loginSchema),
155
+ defaultValues: { email: '', password: '' },
156
+ });
157
+
158
+ const onSubmit = async (values: LoginFormValues) => {
159
+ setError(null);
160
+ const res = await signIn('credentials', { ...values, redirect: false });
161
+ if (res?.error) {
162
+ setError('Invalid email or password.');
163
+ return;
164
+ }
165
+ router.replace(returnTo);
166
+ router.refresh();
167
+ };
168
+
169
+ return (
170
+ <Form {...form}>
171
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
172
+ <h1 className="text-xl font-semibold">Sign in</h1>
173
+ {error && <p className="text-sm text-destructive">{error}</p>}
174
+ <FormInputField form={form} schema={loginSchema} fieldName="email" label="Email" type="email" autoComplete="email" />
175
+ <FormPasswordField form={form} schema={loginSchema} fieldName="password" label="Password" autoComplete="current-password" />
176
+ <Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
177
+ {form.formState.isSubmitting ? 'Signing in…' : 'Sign in'}
178
+ </Button>
179
+ </form>
180
+ </Form>
181
+ );
182
+ }
183
+ ```
184
+
185
+ ## `src/app/globals.css` (Tailwind + shadcn tokens)
107
186
 
108
187
  ```css
109
188
  @tailwind base;