Every time I started a new SaaS project, I found myself spending the first week doing the exact same thing — setting up authentication, connecting a database, wiring up payments, building a dashboard layout. The actual product hadn't even started yet and I'd already burned 7 days.
So I decided to build it once, do it properly, and package it as a reusable starter kit. Two weekends later, it was done. Here's exactly what I built, the decisions I made, and the problems I ran into.
The Stack
I wanted a stack that was modern, well-documented, and something most developers would actually want to use.
Next.js 14 (App Router)
The App Router is the future of Next.js. Server components, nested layouts, and file-based routing make it the obvious choice for any new SaaS project in 2024.
Clerk (Authentication)
I chose Clerk over NextAuth for one simple reason — it just works. Pre-built sign in, sign up, user management UI, webhook support, and it looks great out of the box. NextAuth requires a lot more configuration to get the same result.
Prisma + Supabase (Database)
Prisma gives you type-safe database queries in TypeScript without writing raw SQL. Supabase gives you a free hosted PostgreSQL database with a great dashboard. Together they're the fastest way to get a production database running.
Tailwind CSS + Shadcn/ui (Styling)
Tailwind for utility-first styling, Shadcn for pre-built accessible components. No custom CSS, no fighting with component libraries. Just clean, consistent UI.
Stripe (Payments)
Stripe is the industry standard for SaaS billing. Subscription management, customer portal, webhooks — everything you need is already there.
Vercel (Deployment)
One-click deploy from GitHub. Automatic SSL, edge network, and preview deployments on every push. Built by the same team as Next.js so compatibility is perfect.
What's Included
Authentication Flow
Clerk handles the entire auth flow. Sign up, sign in, forgot password, user profile — all pre-built. Protected routes are handled via middleware so the dashboard is completely locked down.
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isProtectedRoute = createRouteMatcher(["/dashboard(.*)"]);
export default clerkMiddleware((auth, req) => {
if (isProtectedRoute(req)) auth().protect();
});
After sign in, users are automatically redirected to the dashboard. After sign out, they go back to the landing page. Zero configuration needed.
Database Schema
Two models cover everything you need to get started:
model User {
id String @id @default(cuid())
clerkId String @unique
email String @unique
name String?
createdAt DateTime @default(now())
subscription Subscription?
}
model Subscription {
id String @id @default(cuid())
userId String @unique
stripeCustomerId String @unique
stripeSubscriptionId String?
stripePriceId String?
status String @default("inactive")
currentPeriodEnd DateTime?
user User @relation(fields: [userId], references: [id])
}
When a user signs up via Clerk, a webhook fires and saves them to the database. When they upgrade via Stripe, the subscription record is created and linked to their user.
Dashboard Layout
The dashboard has a responsive sidebar with navigation, a top navbar with user avatar and dropdown, and mobile hamburger menu support. Built with Shadcn components so everything is accessible and keyboard-navigable.
app/(dashboard)/
layout.tsx ← Sidebar + navbar shell
page.tsx ← Overview with stat cards
billing/page.tsx ← Current plan + upgrade
settings/page.tsx ← Profile + preferences
Stripe Integration
Three API routes handle the entire billing flow:
-
/api/stripe/checkout— creates a Stripe checkout session -
/api/stripe/portal— opens the customer billing portal -
/api/webhooks/stripe— handles subscription events
The webhook handler listens for checkout.session.completed, customer.subscription.updated, and customer.subscription.deleted — updating the database accordingly so your app always knows the user's current plan status.
Landing Page
A complete marketing page with:
- Navbar with sign in / get started CTAs
- Hero with headline and subheadline
- Features section (6 cards)
- Pricing table (Free / Pro / Business)
- FAQ accordion
- Footer with links
Fully responsive and dark mode ready via next-themes.
Folder Structure
app/
(marketing)/ ← Landing page
(auth)/ ← Sign in / sign up
(dashboard)/ ← Protected dashboard
components/
ui/ ← Shadcn components
marketing/ ← Landing page sections
dashboard/ ← Sidebar, stat cards, etc.
lib/
stripe.ts ← Stripe client + helpers
db.ts ← Prisma client
subscription.ts ← getUserSubscriptionPlan()
prisma/
schema.prisma
The Hardest Parts
Prisma on Vercel
This one caught me off guard. Everything worked perfectly locally but the Vercel deployment kept failing with:
Module '"@prisma/client"' has no exported member 'PrismaClient'
The fix was simple but not obvious — Vercel doesn't run prisma generate automatically. You have to add it to your build script:
"scripts": {
"build": "prisma generate && next build",
"postinstall": "prisma generate"
}
The postinstall script runs after npm install on Vercel, generating the Prisma client before the build starts. One line fix, but it took a while to figure out.
Syncing Clerk Users to the Database
Clerk handles authentication but your database knows nothing about new users until you tell it. The solution is a Clerk webhook that fires on user.created and writes to your database:
// app/api/webhooks/clerk/route.ts
if (eventType === "user.created") {
await db.user.create({
data: {
clerkId: event.data.id,
email: event.data.email_addresses[0].email_address,
name: `${event.data.first_name} ${event.data.last_name}`
}
})
}
Without this, Stripe has no user to attach a subscription to when someone upgrades.
Stripe Webhook Reliability
Stripe webhooks need to be verified to prevent fake requests. Every webhook handler needs this check before processing anything:
const body = await req.text()
const signature = headers().get("stripe-signature")!
const event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
Skipping this verification is a security hole. The starter kit handles it correctly out of the box.
What I Would Do Differently
Start with the webhook setup earlier. I built the entire dashboard before setting up the Clerk → database sync, which meant none of my test users were in the database. Set up webhooks on day one.
Use Lemon Squeezy instead of Stripe if you're outside the US. Stripe has restrictions in some countries. Lemon Squeezy works globally, handles VAT automatically, and has a very similar API.
Add dark mode from the start. I added it at the end and had to touch almost every component. Building with dark: classes from day one saves a lot of time.
What's Next
I'm planning to add:
- Email notifications via Resend
- Team/organization support
- Usage-based billing support
- More dashboard page templates
Try It Yourself
If you want to skip 2 weeks of boilerplate setup and start building your actual product on day one, I've packaged everything into a ready-to-use starter kit.
Live demo: https://saas-template-fzx94d96j-raghuneha2803-5957s-projects.vercel.app/
Get the kit: https://raghuneha.gumroad.com/l/kvxtj
Includes full source code, README with step-by-step setup instructions, and deploys to Vercel in one click.
Happy to answer any questions in the comments. What stack are you using for your SaaS projects?
Top comments (0)