Skip to content

mailkite/saas-startup

Repository files navigation

SaaS Starter

Next.js SaaS Starter

The Next.js SaaS Starter is a production-ready template to launch your SaaS. It wires together authentication (self-contained, JWT sessions), payments (Stripe subscriptions and customer portal), team management, and a polished dark-first dashboard — so you can skip the boilerplate and ship your product.

Auth runs entirely in your app. There is no external auth service to sign up for: the OAuth code exchange, password hashing and session signing all happen in your own Next.js server, against your own database. The only outbound dependency is MailKite for sending email — and that's optional; without an API key the app simply skips the welcome mail.

  • Auth — Email/password + Google and GitHub OAuth, self-contained. PBKDF2 hashing via WebCrypto (no native module), JWT sessions via jose
  • Payments — Stripe Checkout, Customer Portal, webhooks, and subscription lifecycle
  • Team — Invite members, manage permissions, shared workspace
  • UI — Dark-first, animated backgrounds, gradient cards, light/dark toggle, color theme picker
  • DB — PostgreSQL with Drizzle ORM (type-safe schema, migrations, seed scripts)

Deploy with Vercel

Live demo →

Stack

Layer Technology
Framework Next.js 15 (App Router)
Auth Self-contained — JWT sessions (jose), OAuth, PBKDF2 passwords
Email MailKite — transactional send (optional)
Payments Stripe — checkout, portal, webhooks
Database PostgreSQL + Drizzle ORM
UI Tailwind CSS v4, shadcn/ui
Platform Vercel (deploys anywhere)

Quick Start

git clone https://github.com/mailkite/saas-startup
cd saas-startup
pnpm install
cp .env.example .env    # then fill in AUTH_SECRET, POSTGRES_URL, STRIPE_SECRET_KEY
cd apps/web
npx drizzle-kit migrate
pnpm run dev            # http://localhost:3000

Deploy

Deploy with Vercel

Env vars

Variable Required Purpose
AUTH_SECRET Yes JWT signing key — openssl rand -base64 32. Rotating it invalidates every session
POSTGRES_URL Yes PostgreSQL connection (Neon, Supabase, or any Postgres)
BASE_URL Prod Public origin, e.g. https://example.com. OAuth redirect_uri is built from it; on Vercel it falls back to VERCEL_PROJECT_PRODUCTION_URL
STRIPE_SECRET_KEY Yes Stripe secret key (sk_test_...)
STRIPE_WEBHOOK_SECRET Yes Stripe webhook secret (whsec_...)
MAILKITE_API_KEY No Sends the welcome email. Omit and email is skipped, not failed
NEXT_PUBLIC_GOOGLE_CLIENT_ID No Google OAuth — enables the button (build-time inlined)
GOOGLE_CLIENT_SECRET No Google OAuth — required for the code exchange
NEXT_PUBLIC_GITHUB_CLIENT_ID No GitHub OAuth — enables the button (build-time inlined)
GITHUB_CLIENT_SECRET No GitHub OAuth — required for the code exchange

Register these callback URLs with each provider:

Provider Callback URL
Google <BASE_URL>/auth/google/callback
GitHub <BASE_URL>/auth/github/callback

Project Structure

apps/web/
├── app/
│   ├── (dashboard)/      # Landing, pricing, dashboard pages
│   ├── (login)/          # Sign in, sign up
│   ├── auth/             # OAuth callbacks — code exchange happens here
│   └── api/              # API routes (stripe, user, team)
├── components/           # React components
├── lib/
│   ├── auth/
│   │   ├── oauth/        # GitHub + Google exchange, identity, account upsert
│   │   ├── password.ts   # PBKDF2 hash/verify (WebCrypto)
│   │   └── local.ts      # Email/password sign-in + sign-up
│   ├── mailkite-auth/    # Sessions, config, middleware, MailKite email
│   └── db/               # Drizzle schema + migrations
├── middleware.ts         # Route protection
└── vercel.json           # Vercel deploy config

Brand assets

Every icon is generated from one path definition in apps/web/scripts/generate-icons.mjs. Change the glyph or the accent colours there and re-run:

cd apps/web
pnpm run brand:icons
File Purpose
app/icon.svg Primary favicon — modern browsers prefer this
app/favicon.ico Legacy favicon (16/32/48)
app/apple-icon.png iOS home screen (180px, full bleed — iOS applies its own mask)
app/opengraph-image.png Social preview (1200×630)
app/twitter-image.png Twitter/X summary card
public/brand/icon.svg The mark, standalone
public/brand/icon-mono.svg Single-colour mark; inherits currentColor
public/brand/logo.svg Mark + wordmark; adapts to light/dark automatically
public/brand/logo-mono.svg Single-colour lockup
public/brand/icon-{192,512}.png PWA manifest icons
public/brand/icon-maskable-512.png Android maskable (glyph inside the 80% safe zone)
public/brand/mask-icon.svg Safari pinned tab

The in-app <Logo /> component draws its tile with a CSS gradient rather than a baked SVG one, so the mark follows whichever accent the theme picker is set to. The static files above bake the default Ocean palette.

License

MIT — use for anything. Attribution appreciated.

Designed by MailKite.