A production-ready SaaS boilerplate built with Next.js 16, React 19, TypeScript, Prisma, Stripe, and NextAuth.js. Launch your SaaS product in days, not months.
- Features
- Screenshots
- Tech Stack
- Project Structure
- Quick Start (5 Minutes)
- Environment Variables
- Database Setup (Neon / PostgreSQL)
- Stripe Integration (Payments)
- OAuth Setup (Google & GitHub)
- Email Setup (Resend)
- Page-by-Page Guide
- API Routes Reference
- Database Schema
- Webhook Setup
- Authentication & Route Protection
- Customization Guide
- Deployment
- Testing Checklist
- Troubleshooting
- Contributing
- License
| Category | What's Included |
|---|---|
| Auth | Email/password login, Google OAuth, GitHub OAuth, forgot password, session management |
| Payments | Stripe subscriptions, plan upgrades/downgrades, proration, invoices, promo codes |
| Dashboard | Stats overview, revenue chart, activity feed, sidebar navigation |
| Billing | Plan management, usage meter, payment method, billing email, invoice history with PDF |
| Settings | Profile update, password change, account deletion |
| API Keys | Generate, copy, delete API keys (max 10 per user) |
| Marketing | Landing page, pricing, blog, changelog, FAQ, testimonials |
| UI | Dark/light theme, responsive design, Radix UI + Tailwind CSS |
| Welcome emails, password reset via Resend | |
| DevOps | CI/CD with GitHub Actions, CodeQL security scanning |
| Type Safety | End-to-end TypeScript, Zod validation, Prisma types |
| Login | Register |
|---|---|
![]() |
![]() |
| Light | Dark |
|---|---|
![]() |
![]() |
| Category | Technology | Version |
|---|---|---|
| Framework | Next.js (App Router) | 16.2 |
| Language | TypeScript | 5.x |
| UI Components | Radix UI + shadcn/ui | Latest |
| Styling | Tailwind CSS | v4 |
| Database | PostgreSQL + Prisma ORM | 6.19 |
| Auth | NextAuth.js (Auth.js) | v5 beta |
| Payments | Stripe | 17.x |
| Resend | 6.x | |
| Forms | React Hook Form + Zod | Latest |
| Icons | Lucide React | Latest |
| Toasts | Sonner | Latest |
| Theme | next-themes | Latest |
next-saas-starter/
├── .github/ # CI/CD & community templates
│ ├── workflows/
│ │ ├── ci.yml # Lint, type-check, build
│ │ └── codeql.yml # Security scanning
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── FUNDING.yml
├── docs/ # Per-page deep documentation
│ ├── auth-guide.md # Login, Register, OAuth, sessions
│ ├── billing-guide.md # Stripe, plans, invoices, usage
│ ├── dashboard-guide.md # Stats, charts, activity, navigation
│ ├── landing-guide.md # Hero, features, pricing, blog, footer
│ └── settings-api-keys-guide.md # Profile, password, API keys
├── prisma/
│ ├── schema.prisma # Database models (User, Subscription, ApiKey, etc.)
│ └── seed.ts # Demo users seeder (admin + user)
├── screenshots/ # README screenshots (add your own)
├── public/ # Static assets (favicon, og-image, SVGs)
├── src/
│ ├── app/
│ │ ├── (auth)/ # Auth pages (no navbar/sidebar)
│ │ │ ├── login/page.tsx # Sign in + demo autofill buttons
│ │ │ ├── register/page.tsx # Create account
│ │ │ └── forgot-password/page.tsx
│ │ ├── (dashboard)/ # Dashboard pages (sidebar + topbar)
│ │ │ ├── layout.tsx # Shared dashboard layout
│ │ │ ├── dashboard/page.tsx # Overview: stats, chart, activity
│ │ │ ├── billing/page.tsx # Plans, invoices, usage, payment, coupons
│ │ │ ├── settings/page.tsx # Profile, password, connected accounts, delete
│ │ │ └── api-keys/page.tsx # Create, copy, revoke API keys
│ │ ├── (marketing)/ # Public pages (navbar + footer)
│ │ │ ├── layout.tsx # Shared marketing layout
│ │ │ ├── page.tsx # Landing (hero, features, pricing, CTA)
│ │ │ ├── pricing/page.tsx # Pricing page
│ │ │ ├── blog/page.tsx # Blog listing
│ │ │ ├── blog/[slug]/page.tsx # Blog post detail
│ │ │ └── changelog/page.tsx # Version changelog
│ │ ├── api/
│ │ │ ├── auth/
│ │ │ │ ├── [...nextauth]/route.ts # NextAuth handler
│ │ │ │ └── register/route.ts # User registration
│ │ │ ├── stripe/
│ │ │ │ └── webhook/route.ts # Stripe webhook handler
│ │ │ └── v1/ # REST API endpoints
│ │ │ ├── api-keys/route.ts # GET list, POST create
│ │ │ ├── api-keys/[id]/route.ts # DELETE revoke
│ │ │ ├── billing-email/route.ts # GET/PUT billing email
│ │ │ ├── checkout/route.ts # POST create/switch plan
│ │ │ ├── coupon/route.ts # POST validate promo code
│ │ │ ├── invoices/route.ts # GET invoice history
│ │ │ ├── payment-method/route.ts # GET card / POST update
│ │ │ ├── portal/route.ts # POST Stripe portal session
│ │ │ ├── subscription/route.ts # GET current subscription
│ │ │ ├── subscription/cancel/route.ts # POST cancel at period end
│ │ │ ├── usage/route.ts # GET API usage stats
│ │ │ └── user/
│ │ │ ├── route.ts # DELETE account
│ │ │ ├── profile/route.ts # PATCH update name/email
│ │ │ └── password/route.ts # PATCH change password
│ │ ├── layout.tsx # Root layout (providers, metadata, fonts)
│ │ ├── globals.css # Tailwind + CSS variables (themes)
│ │ ├── error.tsx # Global error page
│ │ ├── not-found.tsx # 404 page
│ │ ├── robots.ts # SEO robots.txt
│ │ └── sitemap.ts # SEO sitemap
│ ├── components/
│ │ ├── auth/
│ │ │ ├── login-form.tsx # Login form + demo autofill
│ │ │ ├── register-form.tsx # Register form with validation
│ │ │ └── social-buttons.tsx # Google + GitHub OAuth buttons
│ │ ├── dashboard/
│ │ │ ├── sidebar.tsx # Desktop sidebar navigation
│ │ │ ├── topbar.tsx # Header + avatar dropdown + mobile nav
│ │ │ ├── stats-card.tsx # Metric card with trend indicator
│ │ │ └── recent-activity.tsx # Activity feed with color-coded icons
│ │ ├── marketing/
│ │ │ ├── navbar.tsx # Auth-aware marketing navbar
│ │ │ ├── hero.tsx # Hero section with mockup
│ │ │ ├── features.tsx # 8-feature grid
│ │ │ ├── pricing-cards.tsx # Plan cards with monthly/yearly toggle
│ │ │ ├── testimonials.tsx # 6 testimonial cards
│ │ │ ├── faq.tsx # Accordion FAQ section
│ │ │ ├── cta.tsx # Call-to-action section
│ │ │ └── footer.tsx # Footer with links + credit
│ │ ├── shared/
│ │ │ ├── theme-toggle.tsx # Dark/light mode switch
│ │ │ ├── loading.tsx # Loading spinner
│ │ │ └── error-boundary.tsx # Error boundary wrapper
│ │ ├── ui/ # shadcn/ui primitives
│ │ │ ├── accordion.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── skeleton.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── table.tsx
│ │ │ └── tabs.tsx
│ │ └── providers.tsx # Session + Theme providers
│ ├── config/
│ │ ├── site.ts # App name, description, URLs, creator
│ │ ├── plans.ts # Pricing plans (Free, Pro, Enterprise)
│ │ └── nav.ts # Marketing + dashboard nav items
│ ├── hooks/
│ │ ├── use-current-user.ts # Auth state hook
│ │ └── use-subscription.ts # Subscription state hook
│ ├── lib/
│ │ ├── auth.ts # NextAuth v5 config + providers
│ │ ├── db.ts # Prisma client singleton
│ │ ├── stripe.ts # Stripe helpers (checkout, portal)
│ │ ├── email.ts # Resend email templates
│ │ ├── blog.ts # Blog posts content
│ │ ├── utils.ts # Utility functions (cn, formatDate)
│ │ └── validations.ts # Zod schemas (login, register, etc.)
│ ├── types/
│ │ ├── index.ts # Shared TypeScript types
│ │ └── next-auth.d.ts # NextAuth type augmentation
│ └── proxy.ts # Route protection (auth redirect)
├── .env.example # Environment variable template
├── CONTRIBUTING.md # Contribution guidelines
├── CODE_OF_CONDUCT.md # Community code of conduct
├── SECURITY.md # Security policy
├── CHANGELOG.md # Version history
└── LICENSE # MIT License
# 1. Clone
git clone https://github.com/ali-raza-arain/next-saas-starter.git
cd next-saas-starter
# 2. Install
npm install
# 3. Environment
cp .env.example .env.local
# Fill in your keys (see Environment Variables section below)
# 4. Database
npm run db:push # Create tables
npm run db:seed # Create demo users
# 5. Run
npm run dev # http://localhost:3000Demo accounts (after seeding):
| Password | Role | |
|---|---|---|
demo@example.com |
password123 |
User |
admin@example.com |
password123 |
Admin |
Create .env.local in the project root:
# ============================================
# REQUIRED
# ============================================
# Database (PostgreSQL)
DATABASE_URL="postgresql://user:password@host:5432/dbname?sslmode=require"
# Auth (generate: openssl rand -base64 32)
AUTH_SECRET="your-random-secret-here"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
# ============================================
# STRIPE (Required for payments)
# ============================================
# Get from: https://dashboard.stripe.com/apikeys
STRIPE_SECRET_KEY="sk_test_..."
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."
# Get from: stripe listen (CLI) or Webhook settings
STRIPE_WEBHOOK_SECRET="whsec_..."
# Get from: Stripe > Products > [Plan] > Pricing > Price ID
# IMPORTANT: Must start with "price_" (NOT "prod_")
NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID="price_..."
NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID="price_..."
NEXT_PUBLIC_STRIPE_ENTERPRISE_MONTHLY_PRICE_ID="price_..."
NEXT_PUBLIC_STRIPE_ENTERPRISE_YEARLY_PRICE_ID="price_..."
# ============================================
# OAUTH (Optional)
# ============================================
# Google: https://console.cloud.google.com > APIs & Services > Credentials
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
# GitHub: https://github.com/settings/developers > OAuth Apps
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
# ============================================
# EMAIL (Optional)
# ============================================
# Resend: https://resend.com > API Keys
RESEND_API_KEY="re_..."
EMAIL_FROM="onboarding@yourdomain.com"- Go to https://neon.tech and sign up (GitHub login works)
- Click "New Project"
- Set project name (e.g.
saas-starter), choose nearest region - Click Create Project
- Copy the connection string:
postgresql://neondb_owner:abc123@ep-xxx.region.aws.neon.tech/neondb?sslmode=require - Paste into
.env.localasDATABASE_URL
sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresql
sudo -u postgres psql -c "CREATE USER myuser WITH PASSWORD 'mypass';"
sudo -u postgres psql -c "CREATE DATABASE saas_starter OWNER myuser;"Then set: DATABASE_URL=postgresql://myuser:mypass@localhost:5432/saas_starter
npm run db:push # Create all tables from schema
npm run db:seed # Insert demo users
npm run db:studio # Visual database browser (localhost:5555)| Command | Purpose |
|---|---|
npm run db:push |
Sync schema to database |
npm run db:generate |
Regenerate Prisma client (after schema changes) |
npm run db:seed |
Seed demo data |
npm run db:studio |
Open Prisma Studio GUI |
- Go to https://dashboard.stripe.com/register
- Sign up with email
- Toggle "Test Mode" ON (top-right)
- Go to Developers > API Keys
- Copy Publishable key (
pk_test_...) and Secret key (sk_test_...)
- Go to Products > Add Product
- Pro Plan:
- Name:
Pro Plan - Add price:
$12.00> Recurring > Monthly - (Optional) Add price:
$120.00> Recurring > Yearly - Save > Copy the Price ID (starts with
price_)
- Name:
- Enterprise Plan:
- Name:
Enterprise Plan - Add price:
$49.00> Recurring > Monthly - (Optional) Add price:
$490.00> Recurring > Yearly - Save > Copy the Price ID
- Name:
Common Mistake: Copy the Price ID (
price_xxx), NOT the Product ID (prod_xxx). The Price ID is found under the product's Pricing section.
# Install Stripe CLI
curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public | gpg --dearmor | sudo tee /usr/share/keyrings/stripe.gpg
echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main" | sudo tee -a /etc/apt/sources.list.d/stripe.list
sudo apt update && sudo apt install stripe
# Login & forward webhooks
stripe login
stripe listen --forward-to localhost:3000/api/stripe/webhookCopy the whsec_... output to STRIPE_WEBHOOK_SECRET in .env.local.
- Products > Coupons > Create Coupon
- Set: 20% off, duration: 3 months
- Click coupon > Promotion Codes > Create
- Set code:
LAUNCH20 - Users can enter this on the billing page
| Card Number | Result |
|---|---|
4242 4242 4242 4242 |
Success |
4000 0000 0000 0002 |
Declined |
4000 0000 0000 9995 |
Insufficient funds |
4000 0025 0000 3155 |
3D Secure |
Use any future expiry (e.g. 12/30), any CVC (123), any ZIP (12345).
- Go to https://console.cloud.google.com
- Create a new project
- APIs & Services > Credentials > Create OAuth Client ID
- Application type: Web application
- Authorized redirect URI:
http://localhost:3000/api/auth/callback/google - Copy Client ID and Client Secret
- Go to https://github.com/settings/developers
- Click New OAuth App
- Homepage URL:
http://localhost:3000 - Callback URL:
http://localhost:3000/api/auth/callback/github - Copy Client ID and Client Secret
- Go to https://resend.com and sign up
- API Keys > Create API Key > copy the key
- Add to
.env.local:RESEND_API_KEY=re_xxxxx EMAIL_FROM=onboarding@yourdomain.com
- (Optional) Add & verify your domain in Resend for production
Email templates included:
- Welcome email (on registration)
- Password reset email (on forgot password)
URL: /
Sections (top to bottom):
| Section | Description | File |
|---|---|---|
| Navbar | Logo, nav links, auth buttons (Sign In/Get Started or Dashboard if logged in) | src/components/marketing/navbar.tsx |
| Hero | Headline, description, 2 CTA buttons, animated dashboard mockup | src/components/marketing/hero.tsx |
| Features | 8 feature cards in grid | src/components/marketing/features.tsx |
| Pricing | 3-tier plan comparison with monthly/yearly toggle | src/components/marketing/pricing-cards.tsx |
| Testimonials | Customer quotes | src/components/marketing/testimonials.tsx |
| FAQ | 8 common questions in accordion | src/components/marketing/faq.tsx |
| CTA | Final call-to-action section | src/components/marketing/cta.tsx |
| Footer | Links, social icons, copyright | src/components/marketing/footer.tsx |
Buttons:
| Button | Logged Out | Logged In |
|---|---|---|
| Navbar "Sign In" | Visible > goes to /login |
Hidden |
| Navbar "Get Started" | Visible > goes to /register |
Hidden |
| Navbar "Dashboard" | Hidden | Visible > goes to /dashboard |
| Hero "Get Started" | Goes to /register |
Goes to /register |
| Hero "Star on GitHub" | Opens GitHub repo | Opens GitHub repo |
| Pricing "Upgrade to Pro" | Goes to /register |
Goes to /register |
Customization:
- Edit site name/description:
src/config/site.ts - Edit features:
src/components/marketing/features.tsx - Edit FAQ:
src/components/marketing/faq.tsx - Edit testimonials:
src/components/marketing/testimonials.tsx
URL: /pricing
Elements:
| Element | Description |
|---|---|
| Monthly/Yearly toggle | Switch between billing intervals, "Save 17%" badge |
| Free card | $0, 4 included features, "Get Started" button |
| Pro card (highlighted) | $12/mo or $120/yr, 6 features, "Upgrade to Pro" button, "Most Popular" badge |
| Enterprise card | $49/mo or $490/yr, 8 features, "Contact Sales" button |
| FAQ section | Same accordion as landing page |
Customization: Edit src/config/plans.ts to change plan names, prices, features.
Blog URL: /blog
Blog Post URL: /blog/[slug]
Changelog URL: /changelog
Blog posts included:
- "Introducing Next SaaS Starter"
- "Setting Up Stripe Subscriptions"
- "Migrating to NextAuth.js v5"
Customization: Edit src/lib/blog.ts to add/modify blog posts.
URL: /login
| Element | Description |
|---|---|
| Google button | OAuth login via Google |
| GitHub button | OAuth login via GitHub |
| Email input | Email address field |
| Password input | Password field |
| "Forgot password?" link | Goes to /forgot-password |
| "Sign In" button | Submits credentials |
| "Create an account" link | Goes to /register |
Error handling:
- Wrong credentials: "Invalid email or password"
- Empty fields: Zod validation messages
URL: /register
| Element | Description |
|---|---|
| Name input | Min 2, max 50 characters |
| Email input | Must be valid email |
| Password input | Min 8 chars, 1 uppercase, 1 lowercase, 1 number |
| Confirm password | Must match password |
| "Create Account" button | Submits, redirects to login |
URL: /forgot-password
| Element | Description |
|---|---|
| Email input | Enter registered email |
| "Send Reset Link" button | Sends reset email via Resend |
| Success state | Shows confirmation message |
URL: /dashboard (Protected — requires login)
Layout: Sidebar (left) + Topbar (top) + Content area
| Element | Description |
|---|---|
| Sidebar | Logo, 4 nav links (Dashboard, Billing, API Keys, Settings), Sign Out button |
| Topbar | Page title, theme toggle, user avatar dropdown |
| Stats Cards (4) | Monthly Revenue, Total Users, Active Subscriptions, Churn Rate — with trend indicators |
| Revenue Chart | 12-month bar chart |
| Recent Activity | Activity feed with icons and relative timestamps |
Sidebar Navigation:
| Item | Icon | URL |
|---|---|---|
| Dashboard | LayoutDashboard | /dashboard |
| Billing | CreditCard | /billing |
| API Keys | Key | /api-keys |
| Settings | Settings | /settings |
Topbar User Dropdown:
| Item | Action |
|---|---|
| Dashboard | Go to /dashboard |
| Billing | Go to /billing |
| Settings | Go to /settings |
| Sign Out | Confirmation dialog > signs out > redirects to / |
Sign Out: Both sidebar and topbar show a confirmation dialog: "Are you sure you want to sign out?"
URL: /billing (Protected)
This is the most feature-rich page. For deep documentation, see docs/billing-guide.md.
Sections (top to bottom):
| # | Section | Description |
|---|---|---|
| 1 | Current Plan | Shows plan name, badge, cancel status, "Manage Billing" button |
| 2 | API Usage Meter | Progress bar showing API requests used/limit, color-coded |
| 3 | Payment Method | Card on file (brand, last 4, expiry), "Update Card" button |
| 4 | Billing Email | Input to change invoice email (separate from account email) |
| 5 | Promo Code | Input to apply coupon, shows discount details |
| 6 | Monthly/Yearly Toggle | Switch billing interval, "Save 17%" badge |
| 7 | Plan Cards (3) | Free, Pro, Enterprise with upgrade/downgrade/switch buttons |
| 8 | Invoice History | Table with invoice #, date, description, period, amount, payment method, status, view/download |
Plan Switching Logic:
| From | To | What Happens |
|---|---|---|
| Free | Pro/Enterprise | Stripe Checkout opens (new payment) |
| Pro | Enterprise | Instant switch, Stripe prorates charge |
| Enterprise | Pro | Instant switch, Stripe gives credit |
| Any paid | Free | Cancel at period end (keeps access until billing period ends) |
Buttons on each plan card:
| User's Current Plan | Free Card | Pro Card | Enterprise Card |
|---|---|---|---|
| Free | "Current Plan" (disabled) | "Upgrade to Pro" | "Upgrade to Enterprise" |
| Pro | "Downgrade to Free" | "Current Plan" (disabled) | "Switch to Enterprise" |
| Enterprise | "Downgrade to Free" | "Switch to Pro" | "Current Plan" (disabled) |
| Canceling | "Switching to Free on [date]" | "Switch to Pro" | "Switch to Enterprise" |
URL: /settings (Protected)
Sections:
| Section | Fields | Description |
|---|---|---|
| Profile | Name, Email | Update profile info |
| Password | Current password, New password, Confirm | Change password (requires current) |
| Danger Zone | Delete Account button | Permanently delete account (with confirmation dialog) |
Password Requirements:
- Minimum 8 characters
- At least 1 uppercase letter
- At least 1 lowercase letter
- At least 1 number
URL: /api-keys (Protected)
| Element | Description |
|---|---|
| "Create API Key" button | Opens dialog with name input |
| Key table | Name, Key prefix (sk_...), Last used, Created date |
| Copy button | Copies full key to clipboard (only shown once on creation) |
| Delete button | Removes key with confirmation |
Limits: Maximum 10 API keys per user.
Key Format: sk_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
| Route | Method | Purpose |
|---|---|---|
/api/auth/[...nextauth] |
GET/POST | NextAuth handler (login, session, providers) |
/api/auth/register |
POST | Create new account (name, email, password, confirmPassword) |
| Route | Method | Request Body | Response |
|---|---|---|---|
/api/v1/subscription |
GET | — | { planId, status, currentPeriodEnd, cancelAtPeriodEnd } |
/api/v1/checkout |
POST | { priceId, promotionCodeId? } |
{ url } or { switched: true, planId } |
/api/v1/subscription/cancel |
POST | — | { message, periodEnd } |
/api/v1/portal |
POST | — | { url } (Stripe customer portal) |
/api/v1/invoices |
GET | — | Invoice array with amounts, dates, PDF URLs |
/api/v1/usage |
GET | — | { used, limit, unlimited, percentage } |
/api/v1/payment-method |
GET | — | { paymentMethod: { brand, last4, expMonth, expYear } } |
/api/v1/payment-method |
POST | — | { url } (Stripe setup session) |
/api/v1/billing-email |
GET | — | { email } |
/api/v1/billing-email |
PUT | { email } |
{ message, email } |
/api/v1/coupon |
POST | { code } |
{ valid, promotionCodeId, discount, duration } |
| Route | Method | Request Body | Response |
|---|---|---|---|
/api/v1/user |
DELETE | — | Deletes account |
/api/v1/user/profile |
PATCH | { name, email } |
Updated user |
/api/v1/user/password |
PATCH | { currentPassword, newPassword, confirmPassword } |
Success message |
| Route | Method | Request Body | Response |
|---|---|---|---|
/api/v1/api-keys |
GET | — | Array of keys (prefix only) |
/api/v1/api-keys |
POST | { name } |
{ key } (full key, shown once) |
| Route | Method | Purpose |
|---|---|---|
/api/stripe/webhook |
POST | Handles Stripe events (signature verified) |
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ User │────>│ Subscription │ │ ApiKey │
│──────────────│ │──────────────│ │──────────────────│
│ id │ │ id │ │ id │
│ name │ │ userId (FK) │ │ userId (FK) │
│ email │ │ stripeCustom │ │ name │
│ password │ │ stripeSub │ │ keyHash │
│ role │ │ stripePriceId│ │ keyPrefix │
│ image │ │ planId │ │ lastUsedAt │
│ emailVerified│ │ status │ │ createdAt │
│ createdAt │ │ cancelAt... │ └──────────────────┘
│ updatedAt │ │ apiRequests* │
└──────┬───────┘ │ createdAt │
│ │ updatedAt │
│ └──────────────┘
│
┌────┴─────┐ ┌──────────────────┐
│ Account │ │ VerificationToken│
│──────────│ │──────────────────│
│ provider │ │ identifier │
│ provAccId│ │ token │
│ tokens...│ │ expires │
└──────────┘ └──────────────────┘
Models: User, Account (OAuth), Session, VerificationToken, Subscription, ApiKey
Subscription statuses: inactive, active, past_due, canceled
Plan IDs: free, pro, enterprise
The app listens for these Stripe webhook events:
| Event | Handler |
|---|---|
checkout.session.completed |
Creates/updates subscription in DB |
customer.subscription.updated |
Syncs plan changes, renewals |
customer.subscription.deleted |
Sets plan to "free" |
invoice.payment_succeeded |
Updates period end, status |
invoice.payment_failed |
Sets status to "past_due" |
Local: Run stripe listen --forward-to localhost:3000/api/stripe/webhook
Production: Add webhook endpoint in Stripe Dashboard with URL https://yourdomain.com/api/stripe/webhook
Proxy (src/proxy.ts) protects routes:
| Route | Auth Required | Behavior |
|---|---|---|
/dashboard |
Yes | Redirects to /login if not authenticated |
/billing |
Yes | Redirects to /login if not authenticated |
/settings |
Yes | Redirects to /login if not authenticated |
/api-keys |
Yes | Redirects to /login if not authenticated |
/login |
No (redirect if authed) | Redirects to /dashboard if already logged in |
/register |
No (redirect if authed) | Redirects to /dashboard if already logged in |
/ |
No | Public (navbar changes based on auth state) |
Providers configured:
| Provider | Type | Config |
|---|---|---|
| Credentials | Email + Password | bcrypt hashed, Zod validated |
| OAuth | Requires GOOGLE_CLIENT_ID + SECRET | |
| GitHub | OAuth | Requires GITHUB_CLIENT_ID + SECRET |
// src/config/site.ts
export const siteConfig = {
name: "Your App Name",
description: "Your app description",
creator: "Your Name",
links: {
github: "https://github.com/you/your-repo",
twitter: "https://twitter.com/you",
},
};// src/config/plans.ts
{
id: "pro",
name: "Pro",
price: { monthly: 19, yearly: 190 },
stripePriceId: {
monthly: process.env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID ?? "",
yearly: process.env.NEXT_PUBLIC_STRIPE_PRO_YEARLY_PRICE_ID ?? "",
},
features: [
{ text: "Your feature here", included: true },
],
}// src/config/nav.ts
export const dashboardNav = [
{ title: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
{ title: "Your Page", href: "/your-page", icon: YourIcon },
];- Create
src/app/(dashboard)/your-page/page.tsx - Add nav item in
src/config/nav.ts - Page automatically gets sidebar + topbar + auth protection
- Edit
prisma/schema.prisma - Run
npm run db:generatethennpm run db:push - Use
import { db } from "@/lib/db"to query
- Create
src/app/api/v1/your-route/route.ts - Use
import { auth } from "@/lib/auth"for protection
- Push code to GitHub
- Go to vercel.com > Import repo
- Add all environment variables from
.env.local - Change
NEXT_PUBLIC_APP_URLto your domain - Deploy
- All env vars set in Vercel dashboard
-
NEXT_PUBLIC_APP_URL= your production domain - Stripe switched to Live Mode with live keys
- Stripe webhook URL:
https://yourdomain.com/api/stripe/webhook - OAuth callbacks updated:
https://yourdomain.com/api/auth/callback/googleetc. -
public/robots.txtsitemap URL updated - Resend domain verified
- Remove demo/seed users from production database
- Test full flow: register > subscribe > dashboard
npm run lint # 0 errors
npm run type-check # 0 errors
npm run build # Compiles successfully| # | Test | URL | Expected |
|---|---|---|---|
| 1 | Landing page loads | / |
Hero, features, pricing visible |
| 2 | Register new account | /register |
Account created, redirect to login |
| 3 | Login | /login |
Redirect to dashboard |
| 4 | Dashboard loads | /dashboard |
Stats, chart, activity visible |
| 5 | Update profile | /settings |
Name/email saved |
| 6 | Change password | /settings |
Password updated |
| 7 | Create API key | /api-keys |
Key generated, shown once |
| 8 | Upgrade to Pro | /billing |
Stripe checkout > payment > plan updated |
| 9 | Switch to Enterprise | /billing |
Instant switch, no new payment |
| 10 | Apply promo code | /billing |
Discount shown |
| 11 | Update card | /billing |
Stripe setup page > card saved |
| 12 | Change billing email | /billing |
Email updated |
| 13 | Downgrade to Free | /billing |
Cancel at period end, access retained |
| 14 | View invoices | /billing |
Real invoices with PDF download |
| 15 | Toggle dark/light mode | Any page | Theme switches |
| 16 | Sign out | Dashboard | Confirmation > redirected to home |
| 17 | Visit dashboard logged out | /dashboard |
Redirected to login |
| 18 | Mobile responsive | All pages | Layout adapts, hamburger menu works |
| Problem | Solution |
|---|---|
| "Can't reach database server" | Check DATABASE_URL. For Neon, ensure ?sslmode=require is included |
| "Failed to create checkout session" | Check STRIPE_SECRET_KEY and Price IDs (must start with price_, not prod_) |
| Plan shows "Free" after payment | Webhook not running. Start stripe listen --forward-to localhost:3000/api/stripe/webhook |
| "Coming Soon" on plan button | That plan's Price ID is empty in .env. Create price in Stripe and add it |
| OAuth not working | Check Client ID/Secret. Verify callback URL: http://localhost:3000/api/auth/callback/google |
| Coupon "Invalid or expired" | Must be a Promotion Code (not just a Coupon). Create in Stripe: Coupon > Promotion Codes |
| Hydration errors in console | Expected with next-themes. suppressHydrationWarning is set — safe to ignore |
| Email not sending | Check RESEND_API_KEY. Verify domain in Resend dashboard for production |
| Command | Purpose |
|---|---|
npm run dev |
Start dev server |
npm run build |
Production build |
npm run start |
Start production server |
npm run lint |
Run ESLint |
npm run lint:fix |
Auto-fix lint issues |
npm run type-check |
TypeScript check |
npm run format |
Prettier format |
npm run db:generate |
Regenerate Prisma client |
npm run db:push |
Push schema to database |
npm run db:seed |
Seed demo data |
npm run db:studio |
Visual database browser |
See CONTRIBUTING.md for development setup and PR guidelines.
Built with Next SaaS Starter — https://github.com/ali-raza-arain/next-saas-starter
























