{"uuid": "e3771874-e823-4951-9aef-e4f179a624fa", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2025-29927", "type": "seen", "source": "https://gist.github.com/YusufEmad04/5eaa525959578c425d9f7350ecdefb43", "content": "---\nname: nextjs-fullstack-builder\ndescription: &gt;\n  Full-stack, modular project workflow for Next.js (App Router) + MongoDB (Mongoose)\n  + shadcn/ui + Tailwind CSS. Architecture is a modular monolith: every business\n  feature lives self-contained under `modules/`, mutations use Server Actions (never\n  API routes), and reads go through a cached data-access layer. Use when: scaffolding\n  a new Next.js project, building or extending a feature/module, writing server\n  actions, defining Mongoose models, designing caching, styling with shadcn/Tailwind,\n  handling auth, or applying security/performance best practices for this stack.\n  Triggers: \"new project\", \"next.js\", \"shadcn\", \"mongoose\", \"mongodb\", \"server\n  action\", \"server component\", \"build feature\", \"add module\", \"fullstack builder\",\n  \"caching\", \"revalidate\".\nargument-hint: Describe the feature/module to build (e.g. \"patients CRUD module\", \"auth setup\", \"appointments dashboard\")\n---\n\n# Next.js + MongoDB + shadcn/ui \u2014 Modular Full-Stack Skill\n\n## When to Use\n\n- Scaffolding a new Next.js App Router project from scratch.\n- Building a **new feature as a self-contained module** under `modules/`.\n- Extending an existing module (model, actions, data layer, UI).\n- Writing **Server Actions** for mutations or a cached data layer for reads.\n- Designing the caching strategy (request memo \u2192 data cache \u2192 route cache).\n- Applying the security &amp; performance checklists below.\n\n## Core Architecture Principles\n\nInternalize these before writing any code. Every later phase is an expression of them.\n\n1. **Modular monolith.** The app is split into independent feature modules under\n   `modules/`. A module owns its model, schema, data layer, actions, UI, and types.\n2. **Modules are black boxes.** Other modules and pages import a module **only**\n   through its public barrel `modules//index.ts`. Never deep-import another\n   module's internals (`modules/x/data/...`). This keeps refactors local.\n3. **Server Actions for all mutations.** No `app/api/*` route handlers for\n   first-party CRUD. Route handlers exist only for webhooks, OAuth callbacks, cron,\n   file streaming, or third-party integrations that require a URL.\n4. **Reads through a data layer, not actions.** Server Components call the module's\n   `data/` functions directly. Actions are for writes (and the occasional\n   client-triggered read).\n5. **Server-first.** Components are Server Components by default. `\"use client\"` is\n   pushed down to the smallest possible leaf.\n6. **Trust nothing from the client.** Every server action and data function\n   re-validates input and re-checks auth/authorization. Being \"on the server\" is not\n   a security boundary by itself.\n7. **Cache deliberately, invalidate by tag.** Every cached read is tagged; every\n   mutation revalidates the exact tags it affects.\n\n---\n\n## Phase 1 \u2014 Project Setup\n\n### 1.1 Scaffold\n\n```bash\nnpx create-next-app@latest  \\\n  --typescript --tailwind --eslint --app \\\n  --no-src-dir --import-alias \"@/*\" --yes\n```\n\n### 1.2 shadcn/ui\n\n```bash\ncd \nnpx shadcn@latest init\nnpx shadcn@latest add button badge card input label textarea select \\\n  sheet separator table dialog dropdown-menu sonner tabs skeleton form\n```\n\n&gt; **Rule:** Never edit files in `components/ui/`. They are generated. For custom\n&gt; variants, create wrapper components in `components/` outside `ui/`.\n\n### 1.3 Core dependencies\n\n```bash\nnpm install mongoose zod server-only\nnpm install @upstash/redis @upstash/ratelimit   # caching + rate limiting\n# auth: next-auth (or your provider of choice)\n```\n\n- `zod` \u2014 single source of truth for input validation.\n- `server-only` \u2014 import in any file that must never reach the client bundle.\n- `@upstash/redis` \u2014 durable, cross-instance cache layer (Layer 4 below).\n- `@upstash/ratelimit` \u2014 protect public-facing server actions.\n\n### 1.4 Environment variables\n\n`.env.local` (never commit) and `.env.example` (commit, keys only):\n\n```\nMONGODB_URI=mongodb+srv://:@cluster.mongodb.net/?retryWrites=true&amp;w=majority\nUPSTASH_REDIS_REST_URL=\nUPSTASH_REDIS_REST_TOKEN=\nAUTH_SECRET=\n```\n\nValidate env at boot \u2014 fail fast, never `process.env.X!` scattered across the code:\n\n```ts\n// lib/env.ts\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  MONGODB_URI: z.string().url(),\n  UPSTASH_REDIS_REST_URL: z.string().url(),\n  UPSTASH_REDIS_REST_TOKEN: z.string().min(1),\n  AUTH_SECRET: z.string().min(1),\n});\n\nexport const env = schema.parse(process.env);\n```\n\n&gt; **Rule:** Secrets never use the `NEXT_PUBLIC_` prefix \u2014 that prefix ships the value\n&gt; to the browser.\n\n---\n\n## Phase 2 \u2014 Folder Structure\n\n```\nproject/\n\u251c\u2500\u2500 app/                          # ROUTING ONLY \u2014 thin pages, no business logic\n\u2502   \u251c\u2500\u2500 layout.tsx\n\u2502   \u251c\u2500\u2500 page.tsx\n\u2502   \u251c\u2500\u2500 globals.css\n\u2502   \u251c\u2500\u2500 (marketing)/              # route groups for layout segmentation\n\u2502   \u251c\u2500\u2500 (app)/\n\u2502   \u2502   \u2514\u2500\u2500 /page.tsx    # imports UI from modules/\n\u2502   \u2514\u2500\u2500 api/                      # webhooks / cron / OAuth ONLY \u2014 not CRUD\n\u251c\u2500\u2500 modules/                      # \u2605 all business features live here\n\u2502   \u2514\u2500\u2500 /\n\u2502       \u251c\u2500\u2500 components/           # feature UI (server + client components)\n\u2502       \u251c\u2500\u2500 actions/              # \"use server\" mutations\n\u2502       \u251c\u2500\u2500 data/                 # cached read functions (server-only)\n\u2502       \u251c\u2500\u2500 schema/               # zod schemas + inferred types\n\u2502       \u251c\u2500\u2500 model/                # Mongoose schema/model\n\u2502       \u251c\u2500\u2500 lib/                  # feature-internal helpers\n\u2502       \u2514\u2500\u2500 index.ts              # \u2605 public API \u2014 the ONLY entry point\n\u251c\u2500\u2500 components/\n\u2502   \u251c\u2500\u2500 layout/                   # Navbar, Footer, Shell \u2014 app-wide\n\u2502   \u2514\u2500\u2500 ui/                       # shadcn primitives \u2014 DO NOT EDIT\n\u251c\u2500\u2500 lib/\n\u2502   \u251c\u2500\u2500 db.ts                     # Mongoose connection singleton\n\u2502   \u251c\u2500\u2500 redis.ts                  # Upstash client\n\u2502   \u251c\u2500\u2500 ratelimit.ts              # rate-limiter factory\n\u2502   \u251c\u2500\u2500 auth.ts                   # session config\n\u2502   \u251c\u2500\u2500 dal.ts                    # verifySession / requireUser / requireRole\n\u2502   \u251c\u2500\u2500 action.ts                 # typed action result helpers\n\u2502   \u251c\u2500\u2500 env.ts                    # validated env\n\u2502   \u2514\u2500\u2500 utils.ts                  # cn() and shared helpers\n\u2514\u2500\u2500 public/\n```\n\n**Rules:**\n- `app/` is routing glue. A `page.tsx` should be ~10 lines: fetch via a module\n  `data/` function, render a module component.\n- Cross-module dependency = import from `modules/x` (the barrel), never deeper.\n- Don't create a file unless necessary \u2014 ask \"can this go in an existing file?\"\n\n---\n\n## Phase 3 \u2014 Anatomy of a Module\n\nA module is a vertical slice. Building a feature = filling these files **in order**.\nWorked example below uses a `patients` module.\n\n```\nmodules/patients/\n\u251c\u2500\u2500 model/patient.ts        # 1. Mongoose model\n\u251c\u2500\u2500 schema/patient.ts       # 2. zod schemas (create/update) + inferred types\n\u251c\u2500\u2500 data/patient.ts         # 3. cached read functions  (server-only)\n\u251c\u2500\u2500 actions/patient.ts      # 4. \"use server\" mutations\n\u251c\u2500\u2500 components/\n\u2502   \u251c\u2500\u2500 PatientList.tsx     # 5a. server component (renders data)\n\u2502   \u2514\u2500\u2500 PatientForm.tsx     # 5b. client component (form + action)\n\u2514\u2500\u2500 index.ts                # 6. public API barrel\n```\n\nThe module's `index.ts` exports only what the rest of the app may use:\n\n```ts\n// modules/patients/index.ts\nexport { getPatients, getPatientById } from \"./data/patient\";\nexport { createPatient, updatePatient, deletePatient } from \"./actions/patient\";\nexport { PatientList } from \"./components/PatientList\";\nexport { PatientForm } from \"./components/PatientForm\";\nexport type { Patient, PatientInput } from \"./schema/patient\";\n```\n\n&gt; Everything not exported here is private to the module.\n\n---\n\n## Phase 4 \u2014 Building a Feature (step-by-step recipe)\n\nThis is the canonical workflow. Follow the six steps in order for every feature.\n\n### Step 1 \u2014 Model (`model/patient.ts`)\n\n```ts\nimport mongoose, { Schema, model, models, type Document } from \"mongoose\";\n\nexport interface IPatient extends Document {\n  name: string;\n  phone: string;\n  status: \"active\" | \"archived\";\n  ownerId: mongoose.Types.ObjectId;   // for ownership/tenant checks\n  createdAt: Date;\n  updatedAt: Date;\n}\n\nconst PatientSchema = new Schema(\n  {\n    name: { type: String, required: true, maxlength: 200, trim: true },\n    phone: { type: String, required: true, maxlength: 30, trim: true },\n    status: { type: String, enum: [\"active\", \"archived\"], default: \"active\" },\n    ownerId: { type: Schema.Types.ObjectId, ref: \"User\", required: true, index: true },\n  },\n  { timestamps: true }\n);\n\n// Index every field combination you actually query/sort on (Phase 6).\nPatientSchema.index({ ownerId: 1, status: 1, createdAt: -1 });\n\nexport const Patient =\n  (models.Patient as mongoose.Model) ||\n  model(\"Patient\", PatientSchema);\n```\n\n### Step 2 \u2014 Schema (`schema/patient.ts`)\n\nzod is the validation contract. Define it once; infer TypeScript types from it.\n\n```ts\nimport { z } from \"zod\";\n\nexport const patientInputSchema = z.object({\n  name: z.string().trim().min(1, \"Name is required\").max(200),\n  phone: z.string().trim().min(5).max(30),\n  status: z.enum([\"active\", \"archived\"]).default(\"active\"),\n});\n\nexport const patientUpdateSchema = patientInputSchema.partial();\n\nexport type PatientInput = z.infer;\nexport type Patient = PatientInput &amp; { id: string; createdAt: string };\n```\n\n### Step 3 \u2014 Data layer (`data/patient.ts`) \u2014 cached reads\n\n`server-only` guarantees this file is never bundled to the client. Reads are wrapped\nin the cache layers from Phase 7 and tagged for precise invalidation.\n\n```ts\nimport \"server-only\";\nimport { unstable_cache } from \"next/cache\";\nimport { connectDB } from \"@/lib/db\";\nimport { requireUser } from \"@/lib/dal\";\nimport { Patient } from \"../model/patient\";\n\n/** All patients for the current user, newest first. Cached + tagged. */\nexport async function getPatients() {\n  const user = await requireUser();               // auth at the data boundary\n\n  return unstable_cache(\n    async () =&gt; {\n      await connectDB();\n      const docs = await Patient.find({ ownerId: user.id })\n        .select(\"name phone status createdAt\")     // projection \u2014 no over-fetch\n        .sort({ createdAt: -1 })\n        .limit(100)                                // never unbounded\n        .lean();                                   // plain objects, faster\n      return docs.map(serializePatient);\n    },\n    [\"patients\", user.id],                         // cache key parts\n    { tags: [`patients:${user.id}`], revalidate: 300 }\n  )();\n}\n\nexport async function getPatientById(id: string) {\n  const user = await requireUser();\n  await connectDB();\n  if (!isValidObjectId(id)) return null;\n  const doc = await Patient.findOne({ _id: id, ownerId: user.id }).lean();\n  return doc ? serializePatient(doc) : null;       // ownership check = IDOR defense\n}\n```\n\n&gt; **Rule:** Auth check happens *inside* the data function, close to the data \u2014 not\n&gt; only in middleware (middleware can be bypassed; see Phase 9).\n\n### Step 4 \u2014 Server Actions (`actions/patient.ts`) \u2014 mutations\n\n```ts\n\"use server\";\n\nimport { revalidateTag } from \"next/cache\";\nimport { connectDB } from \"@/lib/db\";\nimport { requireUser } from \"@/lib/dal\";\nimport { ratelimit } from \"@/lib/ratelimit\";\nimport { ok, fail, type ActionResult } from \"@/lib/action\";\nimport { Patient } from \"../model/patient\";\nimport { patientInputSchema, patientUpdateSchema } from \"../schema/patient\";\n\nexport async function createPatient(input: unknown): Promise&gt; {\n  // 1. Authn/Authz \u2014 every action is a PUBLIC endpoint; never assume a caller.\n  const user = await requireUser();\n\n  // 2. Rate limit \u2014 actions are abusable like any POST endpoint.\n  const { success } = await ratelimit.limit(`createPatient:${user.id}`);\n  if (!success) return fail(\"Too many requests. Try again shortly.\");\n\n  // 3. Validate \u2014 parse, don't trust. Reject extra fields.\n  const parsed = patientInputSchema.safeParse(input);\n  if (!parsed.success) return fail(\"Invalid input\", parsed.error.flatten());\n\n  // 4. Mutate \u2014 write only explicitly-validated fields (no mass assignment).\n  await connectDB();\n  const doc = await Patient.create({ ...parsed.data, ownerId: user.id });\n\n  // 5. Invalidate \u2014 revalidate the exact tags this write affects.\n  revalidateTag(`patients:${user.id}`);\n\n  return ok({ id: String(doc._id) });\n}\n\nexport async function updatePatient(id: string, input: unknown): Promise {\n  const user = await requireUser();\n  const parsed = patientUpdateSchema.safeParse(input);\n  if (!parsed.success) return fail(\"Invalid input\", parsed.error.flatten());\n\n  await connectDB();\n  // Scope by ownerId \u2014 prevents updating another user's record (IDOR).\n  const res = await Patient.updateOne({ _id: id, ownerId: user.id }, parsed.data);\n  if (res.matchedCount === 0) return fail(\"Not found\");\n\n  revalidateTag(`patients:${user.id}`);\n  return ok();\n}\n\nexport async function deletePatient(id: string): Promise {\n  const user = await requireUser();\n  await connectDB();\n  const res = await Patient.deleteOne({ _id: id, ownerId: user.id });\n  if (res.deletedCount === 0) return fail(\"Not found\");\n  revalidateTag(`patients:${user.id}`);\n  return ok();\n}\n```\n\nShared result helpers \u2014 actions **return** typed results, they don't throw across\nthe network boundary:\n\n```ts\n// lib/action.ts\nexport type ActionResult =\n  | { ok: true; data: T }\n  | { ok: false; error: string; details?: unknown };\n\nexport const ok = (data: T = null as T) =&gt; ({ ok: true as const, data });\nexport const fail = (error: string, details?: unknown) =&gt;\n  ({ ok: false as const, error, details });\n```\n\n### Step 5 \u2014 UI components\n\n**5a. Server component \u2014 renders cached data:**\n\n```tsx\n// modules/patients/components/PatientList.tsx\nimport { getPatients } from \"../data/patient\";\n\nexport async function PatientList() {\n  const patients = await getPatients();\n  if (patients.length === 0) return \nNo patients yet.;\n  return (\n    \n\n      {patients.map((p) =&gt; (\n        \n\n          {p.name}\n          {p.phone}\n        \n      ))}\n    \n  );\n}\n```\n\n**5b. Client component \u2014 form bound to a server action via `useActionState`:**\n\n```tsx\n\"use client\";\n\nimport { useActionState } from \"react\";\nimport { useFormStatus } from \"react-dom\";\nimport { createPatient } from \"../actions/patient\";\nimport { ok } from \"@/lib/action\";\n\nexport function PatientForm() {\n  const [state, formAction] = useActionState(\n    async (_prev: unknown, formData: FormData) =&gt;\n      createPatient(Object.fromEntries(formData)),\n    null\n  );\n\n  return (\n    \n\n      \n      \n      {state &amp;&amp; !state.ok &amp;&amp; \n{state.error}}\n      \n    \n  );\n}\n\nfunction SubmitButton() {\n  const { pending } = useFormStatus();   // pending state without manual useState\n  return {pending ? \"Saving\u2026\" : \"Add patient\"};\n}\n```\n\nFor instant UI feedback on lists, use `useOptimistic` to render the new item before\nthe action resolves, then reconcile with the revalidated server state.\n\n### Step 6 \u2014 Public barrel + wire into a route\n\n```ts\n// modules/patients/index.ts  (see Phase 3)\n```\n\n```tsx\n// app/(app)/patients/page.tsx \u2014 thin routing glue only\nimport { Suspense } from \"react\";\nimport { PatientList, PatientForm } from \"@/modules/patients\";\n\nexport default function PatientsPage() {\n  return (\n    \n\n      \n      }&gt;\n        \n      \n    \n  );\n}\n```\n\n---\n\n## Phase 5 \u2014 Database Layer\n\n### 5.1 Connection singleton (`lib/db.ts`)\n\n```ts\nimport mongoose from \"mongoose\";\nimport { env } from \"./env\";\n\nlet cached = (global as any).mongoose as\n  | { conn: typeof mongoose | null; promise: Promise | null }\n  | undefined;\nif (!cached) cached = (global as any).mongoose = { conn: null, promise: null };\n\nexport async function connectDB() {\n  if (cached!.conn) return cached!.conn;\n  if (!cached!.promise) {\n    cached!.promise = mongoose.connect(env.MONGODB_URI, {\n      bufferCommands: false,\n      maxPoolSize: 10,        // bound the connection pool for serverless\n      minPoolSize: 1,\n      serverSelectionTimeoutMS: 5000,\n    });\n  }\n  cached!.conn = await cached!.promise;\n  return cached!.conn;\n}\n```\n\n### 5.2 Query best practices\n\n```ts\n// \u2705 .lean() for read-only \u2014 plain JS objects, ~3-5x faster, less memory\nconst items = await Patient.find(filter).lean();\n\n// \u2705 Project only fields you render \u2014 never SELECT *\nconst items = await Patient.find().select(\"name status createdAt\").lean();\n\n// \u2705 Always bound results\nconst items = await Patient.find().limit(100).lean();\n\n// \u2705 Cursor pagination for large/infinite lists \u2014 skip() is O(n) and degrades\nconst page = await Patient.find({ _id: { $gt: lastId }, ownerId })\n  .sort({ _id: 1 }).limit(20).lean();\n\n// \u2705 Validate ObjectId before querying\nimport { isValidObjectId } from \"mongoose\";\nif (!isValidObjectId(id)) return null;\n\n// \u2705 Avoid N+1 \u2014 use aggregation/$lookup or a single populate, not a loop of finds\nconst withDoctor = await Patient.find().populate(\"doctorId\", \"name\").lean();\n\n// \u2705 Verify index usage in dev\nawait Patient.find(filter).explain(\"executionStats\"); // expect IXSCAN, not COLLSCAN\n```\n\n### 5.3 Indexing rules\n\n- Index every field used in a query filter, sort, or join.\n- Compound indexes follow **ESR**: Equality fields, then Sort fields, then Range\n  fields \u2014 in that order.\n- A compound index that covers all projected fields = a *covering index* (no\n  document fetch). Aim for these on hot read paths.\n- Don't over-index \u2014 every index slows writes and consumes RAM.\n- For multi-tenant apps, the tenant/owner field is the **first** key of nearly\n  every compound index.\n\n---\n\n## Phase 6 \u2014 Server Actions: Rules &amp; Patterns\n\nServer Actions are convenient but they are **public, unauthenticated HTTP POST\nendpoints** until *you* secure them. Treat every action with the same rigor as a\npublic API.\n\n**Every action, in order:**\n1. **Authenticate** \u2014 `requireUser()` / `requireRole()`. Never assume a caller.\n2. **Authorize** \u2014 confirm this user may act on this specific resource (ownership\n   / tenant / role). Scope DB writes by `ownerId` to defend against IDOR.\n3. **Rate limit** \u2014 keyed by user or IP for anything a client can spam.\n4. **Validate** \u2014 `schema.safeParse(input)`; reject unknown fields.\n5. **Mutate** \u2014 write only explicitly-validated fields.\n6. **Revalidate** \u2014 `revalidateTag` / `revalidatePath` for exactly what changed.\n7. **Return** a typed `ActionResult` \u2014 don't throw raw errors to the client; don't\n   leak stack traces or DB messages.\n\n**Do / Don't:**\n- \u2705 Co-locate actions in `modules//actions/`. Export via the barrel.\n- \u2705 Use `useActionState` for form state, `useFormStatus` for pending UI,\n  `useOptimistic` for instant feedback.\n- \u274c Don't create `app/api/*` routes for first-party CRUD.\n- \u274c Don't pass a Mongoose document or `ObjectId` to a client component \u2014 serialize\n  to plain JSON (string ids, ISO dates).\n- \u274c Don't put non-mutation reads in actions; reads belong in `data/`.\n\n---\n\n## Phase 7 \u2014 Caching Strategy (3 layers + durable store)\n\nCache top-down; invalidate by tag. The three Next.js layers plus an optional\ndurable store:\n\n| Layer | Mechanism | Scope | Use for |\n|---|---|---|---|\n| **1. Request memoization** | `React.cache()` | One render pass | Dedupe the same query called by multiple components in one request |\n| **2. Data Cache** | `unstable_cache(...)` *or* `\"use cache\"` + `cacheTag`/`cacheLife` | Cross-request, cross-user | Tagged DB reads \u2014 the primary app cache |\n| **3. Full Route Cache** | `export const revalidate = N` (ISR) | Cross-request, per route | Static/public pages |\n| **4. Durable store** *(optional)* | Upstash Redis | Cross-instance, cross-deploy | Sessions, counters, rate limits, data that must survive redeploys |\n\n**Layer 1 \u2014 request memoization:** wrap a read so repeated calls in one render hit\nthe DB once.\n\n```ts\nimport { cache } from \"react\";\nexport const getCurrentUser = cache(async () =&gt; { /* ...one DB hit per request */ });\n```\n\n**Layer 2 \u2014 data cache (the workhorse):** tag every cached read so a mutation can\ninvalidate precisely (see `getPatients` in Step 3). On Next 15+ you may use the\n`\"use cache\"` directive with `cacheTag()` and `cacheLife()` instead of\n`unstable_cache` \u2014 pick one style per project and stay consistent.\n\n**Layer 3 \u2014 route cache / ISR:**\n\n```ts\nexport const revalidate = 60;            // public, mostly-static pages\nexport const dynamic = \"force-dynamic\";  // per-request/admin pages \u2014 no route cache\n```\n\n**Layer 4 \u2014 durable store:** Next's data cache is per-deployment and can be cold.\nUse Redis for state that must be shared across instances or survive redeploys.\n\n**Invalidation rules:**\n- Tag scheme: `\":\"` for lists, `\":\"` for items.\n- Every mutation calls `revalidateTag` for **every** tag it touched \u2014 no more.\n- `revalidatePath` only when a whole route's content changed.\n- Need read-your-writes immediately after a mutation in the same request? Use\n  `updateTag` (Next 15.x) so the refreshed value is available before the response.\n- If a list and a detail view share data, invalidate both tags.\n\n---\n\n## Phase 8 \u2014 Frontend Patterns\n\n### 8.1 Server Components first\n\nDefault to Server Components. Add `\"use client\"` only for: `useState`/`useEffect`,\nbrowser APIs, event handlers, `useRouter`/`useSearchParams`/`usePathname`. Push the\nboundary to the **smallest leaf** \u2014 a button, not a whole page \u2014 to keep the client\nbundle small.\n\n### 8.2 Streaming with Suspense\n\nWrap slow data in `` so the shell renders instantly and slow parts stream\nin. Use multiple granular boundaries rather than one page-level spinner.\n\n```tsx\n}&gt;\n}&gt;\n```\n\n### 8.3 Route-level states\n\n- `loading.tsx` \u2014 skeleton for the whole route segment.\n- `error.tsx` \u2014 `\"use client\"` error boundary with a `reset()` button. Show a safe\n  message, never the raw error.\n- `not-found.tsx` \u2014 for `notFound()` calls.\n\n### 8.4 Avoid waterfalls\n\n```ts\n// \u2705 parallel \u2014 independent fetches start together\nconst [patients, doctors] = await Promise.all([getPatients(), getDoctors()]);\n```\n\nUse the preload pattern (call a `data/` function early without `await`) to warm the\ncache before a child component needs it.\n\n### 8.5 Images &amp; fonts\n\n- `next/image` always \u2014 explicit `width`/`height`, or `fill` + `sizes`. `priority`\n  on above-the-fold images.\n- `next/font` for self-hosted fonts \u2014 no layout shift, no extra network request.\n\n### 8.6 Forms &amp; feedback\n\n- `useActionState` for action state, `useFormStatus` for pending UI,\n  `useOptimistic` for instant list updates.\n- Validate on the client for UX **and** on the server for safety \u2014 never client-only.\n\n### 8.7 Accessibility\n\n- Semantic HTML (``, `\n`, `\n`); `` tied to every input.\n- Visible focus states; full keyboard operability.\n- Meaningful `alt` text; sufficient color contrast; `aria-*` only where semantics\n  fall short.\n\n### 8.8 Bundle hygiene\n\n- `next/dynamic` for heavy, below-the-fold, or rarely-used client components.\n- Don't import a 50-fn library for one helper; prefer per-function imports.\n- Run `@next/bundle-analyzer` when the client bundle grows.\n\n---\n\n## Phase 9 \u2014 Security\n\n| Threat | Mitigation |\n|---|---|\n| **Unsecured server actions** | Every action is a public POST endpoint \u2014 run authn + authz + validation inside *every* action, not just middleware. |\n| **Middleware auth bypass** | Never rely on middleware as the *only* auth gate (it has been bypassable, e.g. CVE-2025-29927). Enforce auth in the data layer / actions, close to the data. |\n| **Broken access control / IDOR** | Scope every read and write by `ownerId`/tenant. Never `findById(id)` alone for user data \u2014 `findOne({ _id: id, ownerId })`. |\n| **NoSQL injection / mass assignment** | Never pass `input`/`body` straight to a model. `zod.safeParse` then write only named fields. |\n| **XSS** | JSX auto-escapes. Never `dangerouslySetInnerHTML` with user input; sanitize if unavoidable. |\n| **CSRF** | Server Actions check `Origin`; keep `SameSite` cookies. Don't disable these defenses. |\n| **Sensitive data exposure** | Project away secrets (`.select(\"-password -__v\")`). Serialize before sending to the client. |\n| **Secret leakage** | Secrets only in `.env.local`; never `NEXT_PUBLIC_`; import `server-only` in any secret-touching module. |\n| **Rate-limit abuse** | `@upstash/ratelimit` on public actions and auth endpoints, keyed by user/IP. |\n| **Error leakage** | Return generic messages to the client; log full details server-side only. |\n| **Insecure headers** | Set CSP, `X-Frame-Options`, HSTS, `Referrer-Policy` via `headers()` in `next.config`. |\n| **Dependency risk** | `npm audit` in CI; keep Next.js patched (auth-related CVEs ship in patch releases). |\n\n```ts\n// \u2705 explicit, validated write\nconst parsed = patientInputSchema.safeParse(input);\nif (!parsed.success) return fail(\"Invalid input\");\nawait Patient.create({ ...parsed.data, ownerId: user.id });\n\n// \u274c never \u2014 NoSQL injection + mass assignment\nawait Patient.create(input);\n```\n\n**The DAL pattern (`lib/dal.ts`):** centralize auth so every module calls the same\nverified helpers.\n\n```ts\nimport \"server-only\";\nimport { cache } from \"react\";\n\nexport const verifySession = cache(async () =&gt; {\n  const session = await getSession();           // your provider\n  return session?.user ?? null;\n});\n\nexport async function requireUser() {\n  const user = await verifySession();\n  if (!user) throw new Error(\"UNAUTHENTICATED\");\n  return user;\n}\n\nexport async function requireRole(role: string) {\n  const user = await requireUser();\n  if (user.role !== role) throw new Error(\"FORBIDDEN\");\n  return user;\n}\n```\n\n---\n\n## Phase 10 \u2014 Performance Checklist\n\n- DB: indexes match query+sort (ESR); `.lean()` + projection on reads; bounded\n  `.limit()`; cursor pagination on large lists; no N+1; `.explain()` shows `IXSCAN`.\n- Caching: hot reads wrapped in Layer 2 + tagged; routes use ISR where static;\n  mutations revalidate only affected tags.\n- Rendering: Server Components default; `\"use client\"` at leaves; ``\n  streams slow data; independent fetches run in `Promise.all`.\n- Assets: `next/image` + `sizes`; `next/font`; `priority` above the fold.\n- Bundle: `next/dynamic` for heavy client code; analyzer checked when bundle grows.\n- Connection: Mongoose pool bounded (`maxPoolSize`); connection reused via singleton.\n\n---\n\n## Phase 11 \u2014 Styling Conventions\n\n- Use **project design tokens** \u2014 not raw Tailwind palette (`gray-900`, etc.).\n- Radius: `rounded-[4px]` for buttons, cards, inputs. Not `rounded-xl`/`2xl`.\n- No gradients or shadows except intentional, designed effects.\n- Container: `max-w-[1280px] mx-auto px-4 md:px-8`.\n- Navigation: if the shadcn `Button` lacks `asChild`, style a `` directly.\n- Custom variants: wrapper components in `components/`, never edit `components/ui/`.\n\n```tsx\n// components/PrimaryButton.tsx\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport function PrimaryButton({ className, ...props }: React.ComponentProps) {\n  return ;\n}\n```\n\n---\n\n## Phase 12 \u2014 Coding Conventions\n\n**TypeScript:** explicit types, no `any`. Infer types from zod schemas as the single\nsource of truth. Use `unknown` for action inputs, then `safeParse`.\n\n**File naming:**\n\n| Type | Convention |\n|---|---|\n| Pages / routes | lowercase (`page.tsx`, `route.ts`) |\n| Components | PascalCase (`PatientCard.tsx`) |\n| Models | lowercase file, PascalCase export (`patient.ts` \u2192 `Patient`) |\n| Utilities / actions / data | camelCase (`formatPrice.ts`) |\n| Module folders | lowercase, plural noun (`patients/`) |\n\n**Import order:** external packages \u2192 internal aliases (`@/lib`, `@/modules`) \u2192\ntypes. Cross-module imports come from the module barrel only.\n\n---\n\n## Phase 13 \u2014 Git Workflow\n\n```\nfeat: add patients module          fix: correct patient pagination\nrefactor: extract PatientForm      docs: update developer guide\n```\n\nBranches: `main` (production, protected) \u2190 `dev` \u2190 `feat/*` / `fix/*`.\n\n---\n\n## Phase 14 \u2014 Documentation After Every Iteration\n\nA feature is not done until docs reflect it.\n\n| Change | Doc to update |\n|---|---|\n| New module | `README.md` route table + `DEVELOPER-GUIDE.md` module list |\n| New server action | `DEVELOPER-GUIDE.md` \u2014 name, input shape, result, side effects |\n| New model | JSDoc on schema fields |\n| New cache tag | `DEVELOPER-GUIDE.md` caching section \u2014 tag name + invalidators |\n| New env var | `.env.example` |\n| New dependency | `README.md` tech stack |\n\nInline: every exported function gets a one-line `/** ... */`; non-obvious logic gets\na *why* comment; every module `index.ts` lists its public surface.\n\n---\n\n## Pre-Merge Checklist\n\n**Architecture &amp; modules**\n```\n\u25a1 Feature lives entirely under modules// \u2014 model, schema, data, actions, components\n\u25a1 Other code imports the module only via modules//index.ts (no deep imports)\n\u25a1 app/ pages are thin routing glue \u2014 no business logic\n\u25a1 No app/api/* route for first-party CRUD (webhooks/cron/OAuth only)\n```\n\n**Server actions &amp; data layer**\n```\n\u25a1 Every action: authenticate \u2192 authorize \u2192 rate-limit \u2192 validate \u2192 mutate \u2192 revalidate \u2192 return\n\u25a1 Inputs typed as `unknown`, validated with zod safeParse; unknown fields rejected\n\u25a1 Writes scoped by ownerId/tenant \u2014 no mass assignment, no raw input to a model\n\u25a1 Actions return a typed ActionResult \u2014 no thrown errors, no leaked DB messages to client\n\u25a1 Reads live in data/ (server-only), not in actions\n\u25a1 Mongoose docs/ObjectIds serialized to plain JSON before crossing to the client\n```\n\n**Caching &amp; performance**\n```\n\u25a1 Hot reads wrapped in the data cache and tagged (\":\")\n\u25a1 Every mutation revalidates exactly the tags it affects \u2014 no more, no less\n\u25a1 Public/static routes use ISR (revalidate); dynamic routes opt out correctly\n\u25a1 DB queries: .lean() + .select() projection, bounded .limit(), indexes match query+sort\n\u25a1 Large lists use cursor pagination, not skip()\n\u25a1 Independent fetches run in Promise.all \u2014 no waterfalls\n```\n\n**Security**\n```\n\u25a1 Auth enforced in the data layer / actions \u2014 not middleware alone\n\u25a1 Every resource read/write scoped by ownership (IDOR-safe)\n\u25a1 server-only imported in every file with secrets or DB access\n\u25a1 No secrets in NEXT_PUBLIC_; .env.local gitignored; env validated at boot\n\u25a1 Rate limiting on public-facing actions and auth endpoints\n\u25a1 No dangerouslySetInnerHTML with user input; ObjectId validated before queries\n\u25a1 Security headers (CSP etc.) set in next.config; npm audit clean\n```\n\n**Frontend &amp; quality**\n```\n\u25a1 Server Components default; \"use client\" pushed to leaves\n\u25a1 Slow data wrapped in granular ; loading.tsx / error.tsx / not-found.tsx present\n\u25a1 next/image with width/height or fill+sizes; next/font for fonts\n\u25a1 Forms use useActionState / useFormStatus; validated on client AND server\n\u25a1 Accessible: semantic HTML, labels, focus states, alt text, contrast\n\u25a1 No `any`; types inferred from zod schemas\n\u25a1 Design tokens used (no raw Tailwind palette); rounded-[4px]\n\u25a1 Docs updated \u2014 README, DEVELOPER-GUIDE, .env.example, JSDoc\n```", "creation_timestamp": "2026-05-19T02:12:59.000000Z"}