Build a SaaS from Zero: Next.js + Supabase in 2025.
Complete guide to building a production-ready SaaS using Next.js App Router, Supabase, Prisma, and Stripe. From idea to deployment in record time.
Build a SaaS from Zero: Next.js + Supabase in 2025
Building a SaaS in 2025 is faster than ever. With the right stack, you can go from idea to paying customers in weeks, not months.
This is the exact blueprint I use to build production-ready SaaS products for clients at uara — and it's proven to work.
The Stack That Just Works
- Next.js App Router → Full-stack React with server components
- Supabase → PostgreSQL database + real-time + auth
- Prisma → Type-safe database ORM
- Tailwind + Shadcn → Beautiful UI components
- Stripe → Payment processing that scales
- Vercel → Deploy in seconds
This isn't just trendy tech — it's battle-tested in production.
Project Structure That Scales
src/
app/
(auth)/
sign-in/page.tsx
sign-up/page.tsx
(dashboard)/
dashboard/
page.tsx
settings/page.tsx
billing/page.tsx
api/
stripe/
webhooks/route.ts
auth/
callback/route.ts
components/
ui/ # Shadcn components
auth/ # Auth forms
dashboard/ # Dashboard UI
lib/
supabase.ts # Supabase client
stripe.ts # Stripe config
prisma.ts # Database client
Clean. Organized. Maintainable.
Database Schema (Prisma)
Start with this foundation:
model User {
id String @id @default(cuid())
email String @unique
name String?
createdAt DateTime @default(now())
// Subscription
stripeCustomerId String?
stripeSubscriptionId String?
stripePriceId String?
stripeCurrentPeriodEnd DateTime?
}
model Post {
id String @id @default(cuid())
title String
content String?
userId String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
}
Pro tip: Always start simple. Add complexity as you grow.
Authentication in 3 Steps
1. Supabase Setup
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient(supabaseUrl, supabaseKey);
2. Auth Component
export function SignInForm() {
const [email, setEmail] = useState("");
const handleSignIn = async () => {
await supabase.auth.signInWithOtp({ email });
};
return <Button onClick={handleSignIn}>Sign in with Email</Button>;
}
3. Protected Routes
export default async function DashboardPage() {
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
redirect("/sign-in");
}
return <Dashboard user={user} />;
}
Magic link auth = zero password headaches.
Payments That Convert
Stripe Integration
// lib/stripe.ts
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2023-10-16",
});
// Create checkout session
export async function createCheckoutSession(userId: string) {
return await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
line_items: [
{
price: process.env.STRIPE_PRICE_ID,
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
client_reference_id: userId,
});
}
Webhook Handler
// app/api/stripe/webhooks/route.ts
export async function POST(req: Request) {
const sig = headers().get("stripe-signature")!;
const body = await req.text();
const event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
if (event.type === "checkout.session.completed") {
// Update user subscription in database
await updateUserSubscription(event.data.object);
}
return new Response("OK");
}
Always use webhooks — they're your source of truth.
UI Components That Ship Fast
Use Shadcn for instant professional UI:
npx shadcn-ui@latest add button card form input
Then build like this:
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
export function PricingCard() {
return (
<Card>
<CardHeader>
<CardTitle>Pro Plan</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">$29/mo</p>
<Button className="w-full mt-4">Start Free Trial</Button>
</CardContent>
</Card>
);
}
Result: Beautiful components in minutes, not hours.
Deployment on Vercel
- Connect GitHub → Auto-deploy on push
- Add environment variables → Database, Stripe, etc.
- Deploy → Live in 60 seconds
# Environment variables you need:
DATABASE_URL=
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
Performance Optimization
Server Components by Default
// This runs on the server
export default async function Dashboard() {
const posts = await prisma.post.findMany();
return (
<div>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
Client Components When Needed
"use client";
export function InteractiveButton() {
const [count, setCount] = useState(0);
return (
<Button onClick={() => setCount(count + 1)}>Clicked {count} times</Button>
);
}
Rule: Server by default, client when interactive.
Real-World Metrics
SaaS products I've built with this stack:
- Time to MVP: 2-4 weeks
- Performance: 95+ Lighthouse scores
- Scalability: Handles 10k+ users
- Cost: ~$50/month to start
Common Pitfalls to Avoid
- Over-engineering → Start simple, add features later
- Ignoring TypeScript → Use strict mode from day one
- Skipping tests → At least test your payment flow
- No error boundaries → Handle errors gracefully
- Forgetting SEO → Use Next.js metadata API
What's Next?
This stack gets you 80% of the way to a production SaaS. The remaining 20% is:
- Advanced analytics
- Email marketing integration
- Multi-tenant architecture
- Advanced user roles
Want me to build your SaaS with this exact stack? €900/month gets you unlimited development requests.
P.S. Building in public works. Content is still king in 2025.