Project Structure
A map of the Next Starter directory layout: the folders and key files in this Next.js SaaS kit, plus conventions for where new pages, components, and actions go.
A map of the repo, annotated where the filename doesn't already tell you what a file does. The conventions below cover where new code goes.
Directory tree
app/
├── layout.tsx # Root layout: HTML shell, fonts, ThemeProvider, Toaster
├── globals.css # Global stylesheet (Tailwind v4, theme tokens)
├── error.tsx
├── not-found.tsx
├── manifest.ts
├── robots.ts
├── sitemap.ts
│
├── (site)/ # Public marketing pages (route group, no URL segment)
│ ├── layout.tsx # Marketing layout (header, footer, skip link)
│ ├── page.tsx # Homepage; renders the sections below in order
│ ├── hero.tsx # Homepage sections, co-located ↓
│ ├── logo-cloud.tsx # Marquee of the logos in customers-data.tsx
│ ├── features.tsx # + features-data.ts; rendered twice on the homepage
│ ├── testimonials.tsx # + testimonials-data.ts
│ ├── bento.tsx
│ ├── process.tsx # + process-data.ts
│ ├── customers.tsx # + customers-data.tsx (also exports CustomerLogo)
│ ├── expandable-grid.tsx # Show-more hook + button for testimonials and customers
│ ├── faq.tsx # + faq-data.ts
│ ├── call-to-action.tsx
│ ├── page-hero.tsx # Shared banner for the inner pages below
│ ├── not-found.tsx # 404 rendered inside the marketing layout
│ ├── pricing/
│ │ ├── page.tsx
│ │ └── quote.tsx # Pulls one testimonial from testimonials-data.ts
│ ├── contact/
│ │ ├── page.tsx
│ │ └── contact-form.tsx
│ ├── about/page.tsx
│ ├── privacy/page.tsx
│ └── terms/page.tsx
│
├── auth/ # Authentication flows (each page co-locates its client component)
│ ├── layout.tsx # Split-screen auth layout
│ ├── sign-in/ # page.tsx + sign-in-form.tsx
│ ├── register/ # page.tsx + register-form.tsx
│ ├── forgot-password/ # page.tsx + forgot-password-form.tsx
│ ├── reset-password/ # page.tsx + reset-password-form.tsx
│ ├── verify-email/ # page.tsx + verify-email-form.tsx (6-digit OTP)
│ └── error/ # page.tsx + error-content.tsx
│
├── onboarding/ # First-run wizard (gates dashboard until complete)
│ ├── layout.tsx
│ ├── page.tsx
│ ├── onboarding-wizard.tsx
│ ├── profile-step.tsx
│ ├── plan-step.tsx
│ └── complete/ # page.tsx + complete-content.tsx
│
├── dashboard/ # Protected user area (session required)
│ ├── layout.tsx # Sidebar, breadcrumb, mobile header; sends
│ │ # unfinished accounts back to /onboarding
│ ├── page.tsx # + dashboard-content.tsx, loading.tsx
│ ├── billing/ # page.tsx + billing-content.tsx +
│ │ # current-plan-card.tsx + loading.tsx
│ ├── settings/ # page.tsx + settings-form.tsx + loading.tsx
│ ├── profile/ # page.tsx + profile-form.tsx + loading.tsx
│ │ ├── change-email/ # page.tsx + change-email-form.tsx + loading.tsx
│ │ └── change-password/ # page.tsx + change-password-form.tsx + loading.tsx
│ └── (admin)/ # Admin-only routes (route group, no URL segment)
│ ├── layout.tsx # Enforces admin role check
│ ├── users/ # page.tsx + loading.tsx → /dashboard/users
│ └── files/ # page.tsx + files-content.tsx + loading.tsx
│
├── actions/ # Server Actions
│ ├── contact.ts # Contact form submission
│ ├── files.ts # Browse, upload, create folder, delete
│ ├── onboarding.ts # Onboarding wizard completion
│ ├── settings.ts # Account settings update
│ └── user.ts # Avatar upload/removal + admin user creation
│
└── api/
├── auth/[...all]/route.ts # Better Auth + Stripe webhook handler
└── health/route.ts
components/
├── ui/ # shadcn/ui primitives, plus a few composed on top
│ # (avatar-upload.tsx, data-table.tsx)
├── pricing/pricing-table.tsx
├── dashboard/ # sidebar, breadcrumb, mobile-header,
│ # past-due-banner, plan-card
├── settings/ # api-key-card + dialogs/ (show, revoke)
├── users/ # columns, users-table, user-avatar, dialogs/
├── auth/google-signin-button.tsx
├── captcha-widget.tsx # Turnstile widget + useTurnstile hook (contact form)
├── animate-on-scroll.tsx
├── logo.tsx # Light/dark logo pair
├── not-found-content.tsx # Shared 404 body for both not-found.tsx files
├── header.tsx
├── footer.tsx
├── mobile-nav.tsx
├── theme-toggle.tsx
├── theme-provider.tsx
└── subscription-provider.tsx # Subscription context (wraps dashboard)
lib/
├── auth.ts # Better Auth server config + Stripe plugin
├── auth-client.ts # Better Auth client hooks + subscription methods
├── config.ts # APP_CONFIG, generateMeta, JSON-LD schemas
├── pricing.ts # Pricing tiers for display (plan limits also live in auth.ts)
├── db.ts # Prisma client singleton
├── logger.ts # Pino structured logger
├── utils.ts # cn(), formatDateLong(), isPathSafe()
├── file-utils.ts # formatBytes(), storage key builders, slug sanitizing
├── client/ # Browser-safe utilities (avatar.ts, navigation.ts:
│ # NAV_LINKS, USER_MENU_ITEMS, useSignOut)
├── email/ # SMTP2Go + React Email templates
│ ├── index.tsx # sendEmail + async template helpers
│ ├── layout.tsx # Shared EmailLayout component
│ ├── *.tsx # One template per email (verification, password-reset,
│ │ # subscription-started, payment-failed, …)
│ └── blocked-domains.json # Disposable-email blocklist
├── validations/ # Zod schemas (auth, contact, env, files, settings, user);
│ # env.ts parses on import, so a bad value throws at once
└── server/ # Server-only code
├── auth-helpers.ts # getSession(), cached per request
├── s3.ts # S3-compatible storage helpers (R2, S3, …)
└── turnstile.ts # Cloudflare Turnstile verification
hooks/use-mobile.ts
types/api.ts
prisma/
├── schema.prisma # User, Session, Account, Verification, Subscription, Apikey
└── migrations/
generated/prisma/ # Prisma client output (generated, do not edit)
public/
├── og-image.png, screenshot.png
├── logo.svg, logo-dark.svg, icon.svg
├── icon-192.png, icon-512.png, icon-mask.png, apple-touch-icon.png
├── testimonials/ # Avatars used by testimonials.tsx and pricing/quote.tsx
└── images/ # bento, features, hero, logos, team
e2e/ # Playwright specs (auth, dashboard, contact, settings, seo, smoke)
docs/ # Product guides: remove Stripe, add a blog, i18n, analytics, …
proxy.ts # Runs before /dashboard/*, bounces visitors with
# no session cookie to /auth/sign-in
next.config.ts # Security headers and CSP, image hosts, React CompilerThe repo root also holds the usual config files: prisma.config.ts, playwright.config.ts, biome.json, components.json, postcss.config.mjs, docker-compose.yml, Dockerfile, .lintstagedrc.mjs, and .env.example. .husky/ installs two Git hooks, and they catch people out. Committing runs Biome over the staged files via lint-staged, and pushing runs the whole build script.
Conventions
Route groups. A folder in parentheses shares a layout without adding a URL segment, so app/(site)/pricing/page.tsx serves /pricing and app/dashboard/(admin)/users/page.tsx serves /dashboard/users. (admin) nests inside dashboard/, which means it inherits the dashboard's session check and layers a role check on top.
Co-location. A component used by one page lives next to that page, which is why app/(site)/bento.tsx and every *-form.tsx sit where they do. Move one into components/ when a second page needs it, not before.
Server vs. client. Server Components are the default here. They query Prisma directly and read the session with getSession() from lib/server/auth-helpers.ts. Client Components use useSession() from lib/auth-client.ts instead, and must never import lib/db.ts or anything under lib/server/. Writes belong in Server Actions under app/actions/, except the ones Better Auth owns (sign-in, profile name, email, password), which call its client methods directly.
The proxy file. proxy.ts is where Next.js 16 runs code before a request reaches a route. There is no middleware.ts here, so don't add one. It bounces cookie-less visitors off /dashboard/* as a speed optimization and isn't a security boundary. The layouts and the Server Actions do the real enforcement. See Authentication.
Validation. Every form has a Zod schema in lib/validations/, wired into React Hook Form through zodResolver, and most Server Actions re-parse it on the server before writing. Auth forms are the exception. They post straight to Better Auth rather than to an action, so their schemas run in the browser only and Better Auth applies its own rules server-side. See Forms.
Where to add things
| You want to add… | Put it here |
|---|---|
| A marketing page | app/(site)/<route>/page.tsx |
| A protected page | app/dashboard/<route>/page.tsx |
| An admin-only page | app/dashboard/(admin)/<route>/page.tsx |
| A mutation (form submit, write) | a Server Action in app/actions/ + a Zod schema in lib/validations/ |
| A shared component | components/ (else co-locate next to its page) |
| An email | a template in lib/email/, wired through sendEmail in lib/email/index.tsx |
| A server-only helper | lib/server/ |
Get Started
Set up Next Starter locally: clone the repo, set environment variables, start PostgreSQL in Docker, run Prisma migrations, and start the Next.js dev server.
Build Your First SaaS Feature
Build your first SaaS feature in Next.js: a Prisma model, a migration, a validated Server Action, and an auth-protected dashboard page for a notes feature.