Architecture
The boilerplate ships flat. Net-new feature logic gets a domain folder under src/features/. Server Components stay the default; client islands are the exception. Integrations live in libs/.
Purpose: decide where each PRD feature lives before any code, on a server-first model.
Run: the plan phase of the PRD, informed by the memory bank's gap analysis.
Output: scaffolded src/features/<domain>/ folders + updated ai_rules/context_map.md.
Feeds: the build loop (Step 8) implements into this structure.
Why it exists
An agent left to its own devices invents folder structures and duplicates components that already exist. This step fixes the layout: reuse the flat core for everything the boilerplate already does, and isolate net-new feature logic into self-contained domain folders so features stay decoupled and the agent always knows where code goes.
The layout
app/ # routes (Server Components by default)
components/ # shared UI (check the README before creating)
libs/ # integrations: supabase/, resend, analytics, seo.tsx, api.ts
lib/ # small helpers
src/features/<domain>/ # NET-NEW feature logic, co-located:
├── components/ # the feature's UI
├── hooks/ # client hooks
├── actions/ # server actions
└── schema.ts # zod schema
components/ui/ # shared primitives (shadcn) — net-new features reuse thesecomponents/README.md. Before adding a utility, read libs/README.md. The inventory is the truth; "what a Next.js project usually has" is not. This is the anti-hallucination checklist in action.The RSC boundary
Server Components by default
Static and SEO content renders on the server. Data reads (Supabase) and secrets stay server-side.
Client islands only where interactive
A form, a toggle, a checkout button gets "use client" and is pushed to a leaf. Server-only logic (env reads, the Supabase server client) never leaks into a client component.
No ssr:false in a Server Component
It crashes the build. Lazy-load a client island without ssr:false, or move the dynamic() into a client file.
// crashes the build inside a Server Component
const Footer = dynamic(() => import("@/components/Footer"), { ssr: false });
// fix: no ssr:false, or move dynamic() into a client file
const Footer = dynamic(() => import("@/components/Footer"));The integration layer
External calls (Supabase, Resend, Stripe, any LLM/agent call) go through libs/, env-gated and fail-soft: when env vars are absent, factories return null and callers degrade instead of crashing. The zero-env build stays green.
Best practices baked in
- Reuse the flat core. Only net-new feature logic earns a
src/features/<domain>/folder. - Server-first; the smallest possible client island per surface.
- Env-gated, fail-soft integrations. The build is green with zero env vars.
- Update
context_map.mdand the relevant README whenever files are added or removed. @/import alias for all internal modules, never../../.