Why Next.js for SaaS
Next.js has become the default choice for modern SaaS because it handles the full stack in one framework β server-side rendering for SEO on marketing pages, API routes for backend logic, and React for interactive UI. You don't need separate frontend and backend codebases.
For Indian startup teams that are often 1β3 people, this unification is practically valuable. One deployment, one codebase, one set of skills required.
Project Structure That Scales
/app
/(marketing) # Public pages (landing, pricing, blog)
/(app) # Authenticated app routes
/dashboard
/settings
/api # API routes
/(components)
/ui # Generic UI components
/app # App-specific components
/lib
/auth.ts # Auth utilities
/db.ts # Database client
/validations.ts # Zod schemas
/hooks # React hooks
/types # TypeScript types
The key decision: route groups (marketing) and (app) let you apply different layouts to public and authenticated pages without affecting URL structure.
Authentication
For most SaaS products, use NextAuth.js (Auth.js v5) or Clerk.
NextAuth: Open source, flexible, more complex to configure. Good if you need custom logic or want to self-host auth.
Clerk: Hosted auth with a generous free tier. Pre-built UI components. Takes 30 minutes to implement vs. a day for NextAuth. Worth the $25/month at scale for the time saved.
Middleware for route protection:
export { auth as middleware } from './lib/auth'
export const config = {
matcher: ['/(app)/:path*']
}
Database Layer
For most SaaS: Supabase (managed PostgreSQL) or Turso (SQLite at the edge, extremely cheap).
Use Drizzle ORM over Prisma for Next.js SaaS β it generates better query code and has better edge runtime support.
// lib/db.ts
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
const client = postgres(process.env.DATABASE_URL!)
export const db = drizzle(client)
Multi-Tenancy Pattern
For B2B SaaS, add organizationId (or tenantId) to every table. Create middleware that validates the current user has access to the requested organization:
// Every API route checks tenant membership
const org = await validateOrgAccess(userId, orgId)
if (!org) return Response.json({ error: 'Forbidden' }, { status: 403 })
Never skip this check. One missing authorization check is a data breach.
Payments Integration
For Indian customers: Razorpay (see our integration guide). For international: Stripe or Paddle.
Create a webhooks/route.ts endpoint that handles all payment events:
POST /api/webhooks/razorpay
POST /api/webhooks/stripe
Store every webhook event before processing β invaluable for debugging.
Environment Variables
# .env.local
DATABASE_URL=
NEXTAUTH_SECRET=
NEXTAUTH_URL=
RAZORPAY_KEY_ID=
RAZORPAY_KEY_SECRET=
RAZORPAY_WEBHOOK_SECRET=
Never commit .env.local. Use different values for development, staging, and production.
Deployment
Vercel: Zero-config, great DX, expensive at scale. Good for early stage.
Coolify on Hetzner: Self-hosted, ~β¬10/month for multiple apps, no per-deployment charges. Better for cost-conscious teams. (Our recommended approach for Indian startups once you have steady traffic.)
Build command: npm run build
Start command: npm start
Node version: 20+
Performance Checklist
- Use
next/imagefor all images (automatic optimization) - Use React Server Components for data fetching by default (not useEffect)
- Add
loading.tsxfiles for skeleton states - Cache database queries with Next.js
unstable_cachewhere appropriate - Use CDN for static assets
The Mistake to Avoid
Over-engineering V1. You don't need microservices, message queues, or advanced caching for your first 1,000 customers. A well-structured monolith on Next.js scales further than most founders think before requiring architectural changes.