Mahesh Kakunuri/14 min read/

How to Build a SaaS Product with Next.js: From Idea to Launch

A comprehensive blueprint for building and launching a SaaS product using Next.js 15, Stripe, authentication, and modern deployment.

SaaSNext.jsStripeAuthenticationFull-Stack
Ad Space

Building a SaaS product from scratch can feel overwhelming. Between authentication, payments, databases, and deployment, there are dozens of decisions to make before you write a single line of code.

This guide provides a complete blueprint for building a SaaS with Next.js 15.

The Architecture

Here's the high-level architecture for a modern SaaS:

Client (Browser)
    ↕
Next.js 15 (App Router)
    ↕
API Routes / Server Actions
    ↕
Authentication ←→ Database ←→ Payment Provider
    (NextAuth)   (PostgreSQL)   (Stripe)
    ↕
Third-party Services (Resend, Uploadthing, etc.)

1. Project Setup

npx create-next-app@latest mysaas --typescript --tailwind --app
npm install @prisma/client next-auth @stripe/stripe-js @stripe/react-stripe-js
npm install -D prisma

2. Authentication with NextAuth

Set up authentication with multiple providers:

// lib/auth.ts
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
import Google from 'next-auth/providers/google'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from './prisma'

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    GitHub({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
  callbacks: {
    session({ session, user }) {
      session.user.id = user.id
      return session
    },
  },
  pages: {
    signIn: '/auth/signin',
  },
})

Protecting Routes with Middleware

// middleware.ts
import { auth } from '@/lib/auth'
import { NextResponse } from 'next/server'

export default auth((req) => {
  const isLoggedIn = !!req.auth
  const isOnDashboard = req.nextUrl.pathname.startsWith('/dashboard')

  if (isOnDashboard && !isLoggedIn) {
    return NextResponse.redirect(new URL('/auth/signin', req.url))
  }

  return NextResponse.next()
})

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}

3. Database Schema

Here's the Prisma schema for a typical SaaS:

model User {
  id             String    @id @default(cuid())
  name           String?
  email          String    @unique
  emailVerified  DateTime?
  image          String?
  customerId     String?   @unique // Stripe customer ID
  subscriptionId String?   @unique // Stripe subscription ID
  subscribed     Boolean   @default(false)
  createdAt      DateTime  @default(now())
  updatedAt      DateTime  @updatedAt
}

model Subscription {
  id            String   @id @default(cuid())
  userId        String   @unique
  user          User     @relation(fields: [userId], references: [id])
  stripeId      String   @unique
  status        String   // active, canceled, past_due, etc.
  priceId       String
  currentPeriodStart DateTime
  currentPeriodEnd   DateTime
  createdAt     DateTime @default(now())
}

4. Stripe Integration

Checkout Session

// app/api/stripe/checkout/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'

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

export async function POST(request: Request) {
  const body = await request.json()
  const { priceId, userId, userEmail } = body

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    payment_method_types: ['card'],
    customer_email: userEmail,
    client_reference_id: userId,
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard?success=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing?canceled=true`,
    metadata: { userId },
  })

  return NextResponse.json({ url: session.url })
}

Webhook Handler

// app/api/stripe/webhook/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
import { prisma } from '@/lib/prisma'

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

export async function POST(request: Request) {
  const body = await request.text()
  const signature = request.headers.get('stripe-signature')!

  let event: Stripe.Event

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    )
  } catch {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session
      const userId = session.metadata?.userId

      if (userId) {
        await prisma.user.update({
          where: { id: userId },
          data: {
            customerId: session.customer as string,
            subscriptionId: session.subscription as string,
            subscribed: true,
          },
        })
      }
      break
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription
      const customerId = subscription.customer as string

      await prisma.user.update({
        where: { customerId },
        data: {
          subscribed: false,
          subscriptionId: null,
        },
      })
      break
    }
  }

  return NextResponse.json({ received: true })
}

5. Pricing Page

Create a pricing page that shows available plans:

// app/pricing/page.tsx
const plans = [
  {
    name: 'Starter',
    price: '$19',
    features: ['5 Projects', '10GB Storage', 'Basic Support'],
    priceId: 'price_starter_id',
  },
  {
    name: 'Pro',
    price: '$49',
    features: ['Unlimited Projects', '100GB Storage', 'Priority Support', 'API Access'],
    priceId: 'price_pro_id',
    popular: true,
  },
  {
    name: 'Enterprise',
    price: '$99',
    features: ['Everything in Pro', 'Unlimited Storage', 'Dedicated Support', 'Custom Integrations'],
    priceId: 'price_enterprise_id',
  },
]

6. Dashboard with Subscription Status

// app/dashboard/page.tsx
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
  const session = await auth()
  if (!session?.user?.id) redirect('/auth/signin')

  const user = await prisma.user.findUnique({
    where: { id: session.user.id },
    select: { subscribed: true, subscriptionId: true },
  })

  if (!user?.subscribed) {
    return (
      <div>
        <h1>Upgrade to access the dashboard</h1>
        <a href="/pricing">View Plans</a>
      </div>
    )
  }

  return (
    <div>
      <h1>Dashboard</h1>
      {/* Subscription-based content */}
    </div>
  )
}

7. Email Integration

Send transactional emails with Resend:

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: 'SaaS <hello@yourdomain.com>',
    to: email,
    subject: 'Welcome to SaaS!',
    html: `<h1>Welcome, ${name}!</h1><p>Thanks for signing up.</p>`,
  })
}

8. Deployment Checklist

Before launching, ensure:

  • Environment variables configured in Vercel
  • Database migrations run in production
  • Stripe webhook endpoints set up
  • Custom domain configured
  • SSL certificate active
  • Error monitoring (Sentry) configured
  • Analytics (PostHog or Plausible) set up
  • Terms of Service and Privacy Policy published
  • Rate limiting on API routes
  • Backup strategy for database

9. Monitoring and Analytics

// app/providers.tsx
'use client'

import posthog from 'posthog-js'
import { PostHogProvider } from 'posthog-js/react'

if (typeof window !== 'undefined') {
  posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
    api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
  })
}

export function Providers({ children }: { children: React.ReactNode }) {
  return <PostHogProvider client={posthog}>{children}</PostHogProvider>
}

Conclusion

Building a SaaS with Next.js is more accessible than ever. The App Router, Server Components, and the ecosystem of tools like NextAuth, Prisma, and Stripe provide a solid foundation.

Start with the MVP — authentication, a single subscription tier, and the core feature. Launch fast, gather feedback, and iterate. The architecture outlined here scales from your first user to your millionth.

Ad Space

Related Articles