SaaS
Building a SaaS with Next.js, Stripe, and PostgreSQL
How to structure a Next.js 15 SaaS with Prisma, PostgreSQL, email-password sessions, and Stripe subscription state.
A SaaS on Next.js is a small set of boundaries that have to agree: who the user is, which plan they are on, and what Stripe last told you. The Complete SaaS Starter Kit uses Next.js 15, React 19, Prisma 6, PostgreSQL, and the Stripe Node SDK 17. This guide is the shape of that project, not a second product.
What belongs in the first version
Ship accounts, a plan column, and a webhook before you invent features. The kit’s Prisma schema is intentionally small:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
passwordHash String
name String
plan String @default("free")
stripeCustomerId String? @unique
stripeSubscriptionId String?
createdAt DateTime @default(now())
}
plan is the field your pages read. Stripe remains the source of truth for money; the database is the copy your app can query without calling Stripe on every request. How that copy stays current is covered in synchronizing Stripe subscription state.
Request path
- A server action or route handler reads the session and loads
User. - Public pages (home, pricing, login) do not require a session.
/dashboardredirects to login whencurrentUser()is empty.- Paid screens check
user.plan === "pro". They do not call Stripe. - Checkout and the webhook are the only places that write
stripeCustomerId,stripeSubscriptionId, andplan.
The Complete kit stores passwords with bcryptjs and sessions with jose. Both are already in its package.json. Do not put the Stripe secret or AUTH_SECRET in a NEXT_PUBLIC_ variable.
PostgreSQL locally
The kit’s README expects Docker Compose for Postgres and npx prisma db push against DATABASE_URL. Prisma 6 reads the URL from the schema env("DATABASE_URL"). Use a normal Postgres URL, not the SQLite file the smaller kits use. The Next.js SaaS Boilerplate defaults to SQLite so you can boot without Docker; this kit does not.
Billing is a separate module
Keep Checkout, the Customer Portal, and the webhook in src/app/api/stripe/. The rest of the app only sees plan. That split is why the same billing code also exists as the Stripe Subscription Starter, which uses SQLite instead of Postgres. Read Stripe subscriptions with Next.js before you copy a Checkout session into a page component.
What this does not decide
The schema has no teams table, no seat count, and no invoice history. A plan string of free or pro is enough for the kit’s route guard. Add organizations when a customer can belong to more than one account; until then a user row is the tenant.
Want to skip the setup? The Complete SaaS Starter Kit already includes the Postgres schema, session auth, Checkout, Customer Portal, webhooks, and a dashboard.