How to Build a SaaS App with Next.js and Supabase in 2026
SaaS & Startups

How to Build a SaaS App with Next.js and Supabase in 2026

Building a SaaS application from scratch requires careful architectural decisions. In this practical guide, I walk through the complete stack — Next.js App Router, Supabase for auth and database, Stripe for payments, and Vercel for deployment — based on real production experience.

MSP
Muhammad Shams Paracha
March 13, 202613 min read95 views
#Next.js#Supabase#SaaS#TypeScript#Stripe#PostgreSQL

How to Build a SaaS App with Next.js and Supabase in 2026

Building a Software-as-a-Service application from scratch in 2026 is more accessible than ever — but it still requires careful architectural decisions. Get the foundation wrong and you will spend months untangling technical debt. In this guide, I walk you through the complete production-ready stack I use: Next.js 16 App Router, Supabase, Stripe, and Vercel.

This is based on real production experience — I built MediLab Pro, a medical laboratory management SaaS, using exactly this stack.

Why This Stack?

Before diving in, let me explain why these specific tools:

Tool Role Why
Next.js 16 Frontend + API App Router, RSC, edge functions, built-in SEO
Supabase Auth + Database + Storage Postgres, row-level security, real-time
Stripe Payments Industry standard, excellent webhooks
Vercel Deployment Zero-config, edge network, preview deployments

This stack covers 95% of SaaS requirements with minimal glue code.

1. Project Architecture

Good SaaS architecture separates concerns clearly from day one. Here is the folder structure I use:

/app
  /(marketing)       # Public pages — landing, pricing, blog
  /(app)             # Authenticated app routes
    /dashboard
    /settings
    /billing
  /api               # API routes and webhooks
/components
  /ui                # Reusable UI primitives
  /marketing         # Landing page components
  /app               # App-specific components
/lib
  /supabase.ts       # Supabase server client
  /stripe.ts         # Stripe client
  /auth.ts           # Auth helpers
/types               # TypeScript interfaces

This structure makes it immediately clear whether a file belongs to the public site or the authenticated app.

2. Database Design with Supabase

Supabase gives you a full Postgres database with Row Level Security (RLS). RLS is the killer feature — it enforces data isolation at the database level, not in your application code.

-- Organizations table (multi-tenancy)
CREATE TABLE organizations (
  id          uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  name        text NOT NULL,
  slug        text UNIQUE NOT NULL,
  created_at  timestamptz DEFAULT now()
);

-- Users belong to organizations
CREATE TABLE memberships (
  id              uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id         uuid REFERENCES auth.users NOT NULL,
  organization_id uuid REFERENCES organizations NOT NULL,
  role            text NOT NULL DEFAULT 'member', -- admin | member | viewer
  created_at      timestamptz DEFAULT now(),
  UNIQUE(user_id, organization_id)
);

-- RLS: users can only see their organization's data
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Members can view their organization"
  ON organizations FOR SELECT
  USING (id IN (
    SELECT organization_id FROM memberships WHERE user_id = auth.uid()
  ));

Key RLS principle: Write your policies to ask "does the current user have permission to see this row?" — the database enforces this automatically on every query.

3. Authentication with Supabase Auth

Supabase Auth handles the entire authentication flow out of the box — email/password, magic links, Google OAuth, and more.

// lib/supabase.ts — server-side client (Next.js App Router)
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll: (cs) => cs.forEach(({ name, value, options }) =>
          cookieStore.set(name, value, options)
        ),
      },
    }
  )
}

Protect routes using Next.js middleware:

// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse } from 'next/server'

export async function middleware(request) {
  const supabase = createServerClient(/* ... */)
  const { data: { user } } = await supabase.auth.getUser()

  if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  return NextResponse.next()
}

4. Multi-Tenancy and Organizations

Most SaaS apps need organizations (teams/workspaces). The pattern I use:

  1. User signs up and is redirected to an onboarding flow
  2. Onboarding creates an organization and a membership record
  3. All subsequent data is scoped to organization_id
  4. RLS policies enforce tenant isolation at the database level

This approach scales from 1 user to thousands of organizations without architectural changes.

5. Subscription Billing with Stripe

Stripe is the standard for SaaS billing. The integration has three parts:

Checkout — Create a Stripe Checkout session server-side:

// app/api/billing/create-checkout/route.ts
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(req: Request) {
  const { priceId, customerId } = await req.json()
  const session = await stripe.checkout.sessions.create({
    customer: customerId,
    mode: 'subscription',
    payment_method_types: ['card'],
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard?upgraded=true`,
    cancel_url:  `${process.env.NEXT_PUBLIC_URL}/pricing`,
  })
  return Response.json({ url: session.url })
}

Webhooks — Handle subscription events reliably:

// app/api/webhooks/stripe/route.ts
export async function POST(req: Request) {
  const sig  = req.headers.get('stripe-signature')!
  const body = await req.text()
  const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)

  switch (event.type) {
    case 'customer.subscription.created':
    case 'customer.subscription.updated':
      await updateSubscriptionStatus(event.data.object)
      break
    case 'customer.subscription.deleted':
      await cancelSubscription(event.data.object)
      break
  }
  return Response.json({ received: true })
}

Customer Portal — Let users manage their own subscriptions:

const session = await stripe.billingPortal.sessions.create({
  customer: stripeCustomerId,
  return_url: `${process.env.NEXT_PUBLIC_URL}/settings`,
})
redirect(session.url)

6. Feature Flags and Plan Limits

Every SaaS needs to enforce plan limits. Store plan features in your database and check them server-side:

// lib/limits.ts
const PLAN_LIMITS = {
  free:  { users: 1,  storage_gb: 1,  api_calls: 1000  },
  pro:   { users: 10, storage_gb: 20, api_calls: 50000 },
  scale: { users: -1, storage_gb: -1, api_calls: -1    }, // unlimited
}

export async function checkLimit(orgId: string, feature: keyof typeof PLAN_LIMITS.free) {
  const org = await getOrganizationWithPlan(orgId)
  const limit = PLAN_LIMITS[org.plan][feature]
  if (limit === -1) return true // unlimited
  const current = await getCurrentUsage(orgId, feature)
  return current < limit
}

7. Deployment and Environment Management

Deploy to Vercel with these environment variables:

# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_URL=https://yourapp.com

Use Vercel's preview deployments for every pull request — this gives you a unique URL to test changes before merging.

Conclusion

This stack — Next.js, Supabase, Stripe, Vercel — gives you a production-ready SaaS foundation in days rather than months. Supabase's Row Level Security eliminates entire classes of data leakage bugs. Next.js App Router provides the perfect server/client component model for SaaS dashboards. Stripe handles billing complexity so you can focus on your product.

The most important advice: launch early, charge from day one, and let real user feedback guide your architecture decisions. The best SaaS architecture is the one that gets paying customers, not the most technically elegant one.

Was this helpful?

Share this article

MS

Written by

Muhammad Shams Paracha

IT Expert, Front-End Developer & Digital Marketer with 8+ years of experience. Based in Islamabad, Pakistan.

Comments

Add a Comment

Your email will never be published.