After building multiple SaaS products, this is the file structure that scales from MVP to production.
src/
├── app/
│ ├── (app)/ # Authenticated pages
│ │ ├── layout.tsx # Sidebar + nav
│ │ ├── dashboard/page.tsx
│ │ ├── settings/page.tsx
│ │ └── billing/page.tsx
│ ├── (auth)/ # Auth pages (centered layout)
│ │ ├── login/page.tsx
│ │ └── register/page.tsx
│ ├── (marketing)/ # Public pages
│ │ ├── pricing/page.tsx
│ │ └── blog/page.tsx
│ ├── api/
│ │ ├── auth/[...nextauth]/ # Auth handler
│ │ ├── stripe/ # Billing webhooks
│ │ └── ai/chat/ # AI endpoint
│ ├── layout.tsx # Root layout
│ └── page.tsx # Landing page
├── components/
│ ├── ui/ # Button, Input, Card, Badge
│ └── ai/ # Chat interface
├── lib/
│ ├── auth.ts # Auth.js v5 config
│ ├── db.ts # Prisma singleton
│ ├── stripe.ts # Stripe + plans
│ └── utils.ts # cn(), helpers
├── prisma/
│ └── schema.prisma
└── types/
└── next-auth.d.ts
Why route groups
Parenthesized folders (app), (auth), (marketing) share layouts without affecting URLs. Dashboard pages get the sidebar. Auth pages get a centered layout. Marketing pages get the marketing nav. URLs stay clean.
Why lib/ has exactly 4 files
- auth.ts — single source of truth for auth
- db.ts — Prisma singleton (prevents connection pool exhaustion in dev)
- stripe.ts — client + plan definitions
- utils.ts — cn() and shared helpers
Every page imports from lib/. No duplicated auth or DB logic.
Why components/ui/ stays small
Button, Input, Card, Badge. Four components with variants cover dashboards, settings, billing, and landing pages. Add more when you actually need them.
What NOT to add early
- middleware.ts — only when you need route-level auth
- i18n/ — only when you need multiple languages
- hooks/ — most are premature abstractions
This structure ships as LaunchKit with everything wired up.
Top comments (0)