← Back to articles

How to Build a SaaS with Next.js (2026 Guide)

This is the practical, step-by-step guide to building a SaaS with Next.js. Not theory — the actual decisions, tools, and code patterns you need. From zero to launch.

The Architecture

Next.js App Router
├── (marketing)     → Landing, pricing, blog (public)
├── (auth)          → Sign in, sign up (Clerk)
├── (dashboard)     → Protected app pages
├── api/
│   ├── webhooks/   → Stripe, Clerk webhooks
│   └── ...         → API routes
├── lib/
│   ├── db.ts       → Drizzle + Neon
│   ├── stripe.ts   → Stripe client
│   └── auth.ts     → Clerk helpers
└── components/     → shadcn/ui components

Step 1: Project Setup (15 minutes)

npx create-next-app@latest my-saas --typescript --tailwind --eslint --app --src-dir
cd my-saas

# UI components
npx shadcn@latest init
npx shadcn@latest add button card dialog input form table

# Core dependencies
npm install @clerk/nextjs @neondatabase/serverless drizzle-orm stripe resend
npm install -D drizzle-kit

Step 2: Database (30 minutes)

Set up Neon

  1. Create account at neon.tech (free)
  2. Create a project → copy the connection string
  3. Add to .env.local: DATABASE_URL=your-connection-string

Define Schema with Drizzle

// src/lib/db/schema.ts
import { pgTable, text, timestamp, boolean, integer } from 'drizzle-orm/pg-core'

export const users = pgTable('users', {
  id: text('id').primaryKey(), // Clerk user ID
  email: text('email').notNull().unique(),
  name: text('name'),
  stripeCustomerId: text('stripe_customer_id'),
  plan: text('plan').default('free').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
})

export const projects = pgTable('projects', {
  id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
  name: text('name').notNull(),
  userId: text('user_id').references(() => users.id).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
})

Database Client

// src/lib/db/index.ts
import { neon } from '@neondatabase/serverless'
import { drizzle } from 'drizzle-orm/neon-http'
import * as schema from './schema'

const sql = neon(process.env.DATABASE_URL!)
export const db = drizzle(sql, { schema })

Run Migrations

npx drizzle-kit generate
npx drizzle-kit migrate

Step 3: Authentication (20 minutes)

Set up Clerk

  1. Create account at clerk.com (free up to 10K MAU)
  2. Copy API keys to .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...

Add Middleware

// src/middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isProtectedRoute = createRouteMatcher(['/dashboard(.*)'])

export default clerkMiddleware((auth, req) => {
  if (isProtectedRoute(req)) auth().protect()
})

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}

Sync Users to Database

// src/app/api/webhooks/clerk/route.ts
import { WebhookEvent } from '@clerk/nextjs/server'
import { db } from '@/lib/db'
import { users } from '@/lib/db/schema'

export async function POST(req: Request) {
  const event: WebhookEvent = await req.json()
  
  if (event.type === 'user.created') {
    await db.insert(users).values({
      id: event.data.id,
      email: event.data.email_addresses[0].email_address,
      name: `${event.data.first_name} ${event.data.last_name}`,
    })
  }
  
  return new Response('OK')
}

Step 4: Payments with Stripe (45 minutes)

Create Checkout Session

// src/app/api/stripe/checkout/route.ts
import { auth } from '@clerk/nextjs/server'
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(req: Request) {
  const { userId } = auth()
  if (!userId) return new Response('Unauthorized', { status: 401 })
  
  const { priceId } = await req.json()
  
  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    payment_method_types: ['card'],
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard?success=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
    metadata: { userId },
  })
  
  return Response.json({ url: session.url })
}

Handle Webhooks

// src/app/api/webhooks/stripe/route.ts
import Stripe from 'stripe'
import { db } from '@/lib/db'
import { users } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(req: Request) {
  const body = await req.text()
  const sig = req.headers.get('stripe-signature')!
  const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
  
  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object
      await db.update(users)
        .set({ plan: 'pro', stripeCustomerId: session.customer as string })
        .where(eq(users.id, session.metadata!.userId!))
      break
    }
    case 'customer.subscription.deleted': {
      const sub = event.data.object
      await db.update(users)
        .set({ plan: 'free' })
        .where(eq(users.stripeCustomerId, sub.customer as string))
      break
    }
  }
  
  return new Response('OK')
}

Step 5: Dashboard Layout

// src/app/(dashboard)/layout.tsx
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
import { Sidebar } from '@/components/sidebar'

export default async function DashboardLayout({ children }) {
  const { userId } = auth()
  if (!userId) redirect('/sign-in')
  
  return (
    <div className="flex h-screen">
      <Sidebar />
      <main className="flex-1 overflow-auto p-8">{children}</main>
    </div>
  )
}

Step 6: Email with Resend (15 minutes)

// src/lib/email.ts
import { Resend } from 'resend'

const resend = new Resend(process.env.RESEND_API_KEY)

export async function sendWelcomeEmail(email: string, name: string) {
  await resend.emails.send({
    from: 'YourApp <hello@yourapp.com>',
    to: email,
    subject: 'Welcome to YourApp!',
    html: `<h1>Welcome, ${name}!</h1><p>Get started by creating your first project.</p>`,
  })
}

Step 7: Deploy (5 minutes)

# Push to GitHub, then:
npx vercel --prod

Or connect your GitHub repo to Vercel for automatic deployments.

The Pricing Page Pattern

const plans = [
  {
    name: 'Free',
    price: '$0',
    features: ['3 projects', 'Basic analytics', 'Community support'],
    priceId: null,
  },
  {
    name: 'Pro',
    price: '$19/mo',
    features: ['Unlimited projects', 'Advanced analytics', 'Priority support', 'API access'],
    priceId: 'price_xxx',
    popular: true,
  },
]

Common Patterns

Check User Plan (Server Component)

const user = await db.query.users.findFirst({
  where: eq(users.id, userId),
})

if (user?.plan === 'free' && projectCount >= 3) {
  return <UpgradePrompt />
}

Rate Limiting

import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'),
})

const { success } = await ratelimit.limit(userId)
if (!success) return new Response('Too many requests', { status: 429 })

Launch Checklist

  • Auth working (sign up, sign in, protected routes)
  • Stripe payments working (checkout, webhooks, plan upgrades)
  • Core feature working (the thing users pay for)
  • Email sending (welcome, receipts)
  • Error monitoring (Sentry)
  • Analytics (PostHog or Vercel Analytics)
  • Custom domain
  • Terms of service and privacy policy
  • Pricing page
  • Landing page with clear value proposition

FAQ

How long does it take to build a SaaS?

With this stack, 2-4 weeks from zero to MVP. Auth + payments + database takes 1-2 days. The rest is your core feature.

Should I use a boilerplate?

Boilerplates (Shipfast, Supastarter) save 1-2 weeks of setup. Worth it if you want to skip directly to building your core feature.

When should I start charging?

Day one. Even if it's $5/month. Paying users give real feedback. Free users give opinions.

How do I handle multi-tenancy?

Shared database with userId or orgId on every table. Clerk's Organizations feature handles team management.

Bottom Line

Next.js + Clerk + Stripe + Neon + Vercel. That's the stack. You can go from zero to a deployed SaaS with auth and payments in a single weekend. The hard part isn't the tech — it's building something people will pay for. Ship fast, iterate faster.

Get AI tool guides in your inbox

Weekly deep-dives on the best AI coding tools, automation platforms, and productivity software.