Building a SaaS App with Next.js, Supabase, and Stripe
โก Quick Answer
Learn how to build a SaaS app with Next.js, Supabase, and Stripe โ authentication, database, subscription billing, and deployment on Vercel in one complete guide.
Get more content like this on Telegram!
Daily AI tips, notes & resources โ free
Advertisement
Building a SaaS App with Next.js, Supabase, and Stripe
Two years ago, building a basic SaaS with authentication, a database, and subscription billing required wiring together dozens of services and writing hundreds of lines of boilerplate. Today, the combination of Next.js, Supabase, and Stripe reduces that to a weekend of work.
I built a SaaS using this exact stack that reached 150 paying customers in its first three months. The entire infrastructure cost was under $100/month. No DevOps team, no infrastructure engineers โ just a lean stack that handles authentication, data, payments, and deployment without babysitting.
In this guide, you'll learn the architecture and key implementation details for each piece โ from Supabase auth through Stripe subscriptions to webhook handling.
Architecture Overview
User's Browser (Next.js SSR/CSR)
โ HTTPS
Vercel Edge Network
โ
Next.js App (App Router)
โโโ /app/(auth) โ Login, signup pages
โโโ /app/dashboard โ Protected routes (auth required)
โโโ /app/api/stripe โ Stripe checkout and webhook handlers
โโโ Server Actions โ Database mutations
โ โ
Supabase (PostgreSQL + Auth) Stripe (Subscriptions)
- User data - Products & prices
- Subscription records - Payment processing
- App data - Webhook eventsStep 1: Supabase Setup
Create Project and Schema
- Create project at supabase.com
- Go to SQL Editor, run initial schema:
-- profiles table (extends Supabase auth.users)
CREATE TABLE profiles (
id UUID REFERENCES auth.users(id) PRIMARY KEY,
email TEXT NOT NULL,
full_name TEXT,
avatar_url TEXT,
stripe_customer_id TEXT UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- subscriptions table
CREATE TABLE subscriptions (
id TEXT PRIMARY KEY, -- Stripe subscription ID
user_id UUID REFERENCES profiles(id) NOT NULL,
status TEXT NOT NULL, -- active, past_due, canceled, trialing
price_id TEXT NOT NULL, -- Stripe price ID
current_period_end TIMESTAMPTZ,
cancel_at_period_end BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Row Level Security
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view their own profile"
ON profiles FOR SELECT USING (auth.uid() = id);
CREATE POLICY "Users can update their own profile"
ON profiles FOR UPDATE USING (auth.uid() = id);
ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view their own subscriptions"
ON subscriptions FOR SELECT USING (auth.uid() = user_id);Supabase in Next.js
npm install @supabase/supabase-js @supabase/ssr// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr';
export const createClient = () =>
createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);// lib/supabase/server.ts (for Server Components and Route Handlers)
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export const createServerSupabase = () => {
const cookieStore = cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => cookieStore.getAll(),
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
},
},
}
);
};Step 2: Authentication with Supabase Auth
Supabase Auth provides email/password, magic links, and OAuth (GitHub, Google) out of the box:
// app/(auth)/login/page.tsx
'use client';
import { useState } from 'react';
import { createClient } from '@/lib/supabase/client';
import { useRouter } from 'next/navigation';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const supabase = createClient();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) {
setError(error.message);
} else {
router.push('/dashboard');
router.refresh();
}
};
return (
<form onSubmit={handleLogin}>
<input value={email} onChange={e => setEmail(e.target.value)} type="email" placeholder="Email" required />
<input value={password} onChange={e => setPassword(e.target.value)} type="password" placeholder="Password" required />
{error && <p style={{color: 'red'}}>{error}</p>}
<button type="submit">Sign In</button>
</form>
);
}Protected Route Middleware:
// middleware.ts
import { createServerClient } from '@supabase/ssr';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { getAll: () => request.cookies.getAll(), setAll: (cookiesToSet) => { cookiesToSet.forEach(({ name, value, options }) => response.cookies.set(name, value, options)); } } }
);
const { data: { user } } = await supabase.auth.getUser();
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return response;
}
export const config = { matcher: ['/dashboard/:path*'] };Step 3: Stripe Subscription Billing
Create Stripe Products
In your Stripe dashboard, create:
- Product: "SaaS Pro Plan"
- Price: $29/month (recurring)
- Note the Price ID:
price_xxxxx
npm install stripe// lib/stripe.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-04-10',
});Checkout Session Creation
// app/api/stripe/create-checkout/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { createServerSupabase } from '@/lib/supabase/server';
export async function POST(req: NextRequest) {
const supabase = createServerSupabase();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { data: profile } = await supabase
.from('profiles')
.select('stripe_customer_id')
.eq('id', user.id)
.single();
const session = await stripe.checkout.sessions.create({
customer: profile?.stripe_customer_id || undefined,
customer_email: !profile?.stripe_customer_id ? user.email : undefined,
mode: 'subscription',
line_items: [{ price: process.env.STRIPE_PRICE_ID!, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
metadata: { userId: user.id },
});
return NextResponse.json({ url: session.url });
}Webhook Handler โ The Critical Piece
// app/api/stripe/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { createClient } from '@supabase/supabase-js';
// Use service role key for admin database access
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = req.headers.get('stripe-signature')!;
let 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;
const userId = session.metadata?.userId;
if (!userId || !session.subscription) break;
// Store customer ID and subscription
await supabaseAdmin
.from('profiles')
.update({ stripe_customer_id: session.customer as string })
.eq('id', userId);
const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
await supabaseAdmin.from('subscriptions').upsert({
id: subscription.id,
user_id: userId,
status: subscription.status,
price_id: subscription.items.data[0].price.id,
current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
});
break;
}
case 'invoice.payment_failed':
case 'customer.subscription.deleted': {
const subscription = event.data.object;
await supabaseAdmin
.from('subscriptions')
.update({ status: subscription.status })
.eq('id', subscription.id);
break;
}
}
return NextResponse.json({ received: true });
}Step 4: Checking Subscription Status
// lib/subscription.ts
import { createServerSupabase } from './supabase/server';
export async function getUserSubscription() {
const supabase = createServerSupabase();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return null;
const { data } = await supabase
.from('subscriptions')
.select('*')
.eq('user_id', user.id)
.in('status', ['active', 'trialing'])
.single();
return data;
}
export async function isSubscribed(): Promise<boolean> {
const sub = await getUserSubscription();
return !!sub;
}For deployment architecture and containerization, see our Docker tutorial. For the database fundamentals behind Supabase's PostgreSQL, see our SQL guide.
Further Reading
- How I Built a Full-Stack App in 48 Hours Using AI Tools
- The 2025 Full Stack Developer Roadmap: From Zero to Job-Ready
- PostgreSQL vs MongoDB: Which Database for Your Next Project?
- MERN Stack Tutorial: Build a Full-Stack App from Scratch
- Microservices vs Monolith in 2025: Which Architecture to Choose
- Web Development Roadmap 2026: The Complete Step-by-Step Guide
- Next.js 14 App Router: The Complete Guide from Zero to Deploy
- React Native vs Flutter: Building Mobile Apps in 2025
Advertisement
๐ฌ DiscussionPowered by GitHub Discussions
Frequently Asked Questions
Problem Solver and Cloud Expert
Solves complex infrastructure challenges and architects reliable, scalable cloud deployments for every project. Shamshur Rahman keeps AiTechWorldsโ hosting and cloud infrastructure fast, resilient, and cost-efficient.
Not sure yet? Ask AI about this article
Get an instant, unbiased AI summary of โBuilding a SaaS App with Next.js, Supabase, and Stripeโ.
Advertisement
Related Articles
How I Built a Full-Stack App in 48 Hours Using AI Tools
Learn how to use AI tools to build a full-stack app fast โ GitHub Copilot, Claude, and ChatGPT for planning, coding, debugging, and deploying a real web application in 48 hours.
The 2026 Full Stack Developer Roadmap: From Zero to Job-Ready
The complete full stack developer roadmap for 2026 โ learn frontend, backend, databases, DevOps, and the exact learning path from beginner to job-ready in 12โ18 months.
The Full Stack Developer Salary Guide for 2026 by Country
Full stack developer salary guide 2026 โ average salaries by country, experience level, tech stack, and remote work, plus tips to negotiate a higher salary.
How to Get Your First Full-Stack Job Without a CS Degree
Full stack job no degree guide โ how self-taught developers and bootcamp grads land their first software job with a portfolio, networking, and interview prep.