Official Mailtrap Next.js SaaS starter
0
stars
54
commits
TypeScript
primary language
Sep 10, 2026
updated
Blank-slate Next.js SaaS scaffolding with Mailtrap as the email layer, Supabase auth, and Stripe billing.
For learners, indie hackers, and juniors — not production-grade multi-tenant SaaS.
git clone https://github.com/mailtrap/nextjs-saas-starter.git
cd nextjs-saas-starter
pnpm install
pnpm db:setup # Docker: Supabase Auth + Postgres + schema → writes .env.local
Copy remaining values from .env.example into .env.local:
ADMIN_EMAILS (for /admin/emails)Optional Stripe test products/prices:
stripe fixtures stripe-fixtures.json --api-key "$STRIPE_SECRET_KEY"
Start the app (port comes from NEXT_PUBLIC_APP_URL):
pnpm dev
Open the URL in .env.local (default http://127.0.0.1:3000).
NEXT_PUBLIC_APP_URL is the single source of truth for:
pnpm dev)site_url and auth redirectsIf port 3000 is taken, set e.g. NEXT_PUBLIC_APP_URL=http://127.0.0.1:3001, then:
pnpm sync:env
pnpm db:stop && pnpm db:start
pnpm dev
pnpm db:setup doessupabase/migrations/ (source of truth for schema).env.localNEXT_PUBLIC_APP_URLStop Docker: pnpm db:stop · Reset database: pnpm db:reset · Restart: pnpm db:start
Create a project at supabase.com, run SQL from supabase/migrations/, copy API keys into .env.local, and configure:
NEXT_PUBLIC_APP_URLhttps://YOUR_APP/api/auth/send-emailIn a second terminal (use the port from NEXT_PUBLIC_APP_URL):
stripe listen --forward-to localhost:3001/api/webhooks/stripe --api-key "$STRIPE_SECRET_KEY"
Copy the webhook signing secret into STRIPE_WEBHOOK_SECRET in .env.local.
Set MAILTRAP_WEBHOOK_SECRET in .env.local, then simulate an event:
pnpm webhook:test:mailtrap
Refresh /admin/emails to see the test event. In production, point Mailtrap webhooks at https://YOUR_APP/api/webhooks/mailtrap.
| Group | What you need |
|---|---|
| App | NEXT_PUBLIC_APP_URL — dev port, redirects, Supabase hook (pnpm sync:env after changes) |
| Supabase | 4 vars — auto-filled by pnpm db:setup locally |
| Stripe | STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_PRO, STRIPE_PRICE_TEAM |
| Mailtrap | API token, sandbox flag + inbox ID, from address, 8 template UUIDs, webhook secret |
| Auth hook | SEND_EMAIL_HOOK_SECRET — auto-filled locally |
| Admin | ADMIN_EMAILS — comma-separated emails for /admin/emails |
Full list: .env.example
MAILTRAP_SANDBOX=true
MAILTRAP_ACCOUNT_ID=12345 # optional, silences SDK warning
MAILTRAP_TEST_INBOX_ID=123456 # Email Testing → inbox settings
MAILTRAP_WEBHOOK_SECRET=your-secret # local: any string; match in Mailtrap dashboard for prod
Emails appear in your Mailtrap Email Testing inbox, not the recipient's real mailbox. Set MAILTRAP_SANDBOX=false before production deploy.
src/app/ → routes (marketing, auth, dashboard, API webhooks)
src/actions/ → Server Actions (auth, billing, org, settings)
src/lib/mailtrap → sendEmail(), webhook signature verify
src/config/emails → typed template UUID map
src/db/ → Drizzle schema (profiles, orgs, subscriptions, email events)
Email flows: Supabase Send Email Hook → Mailtrap (magic link, password reset, email change). App events → sendEmail() (welcome, invites, billing).
Billing: Stripe Checkout for upgrades; downgrades cancel or update the existing subscription via changePlan().
src/features/your-feature/ — see src/features/README.mdsrc/app/dashboard/your-feature/page.tsx (or another app route)sendEmail() from @/lib/mailtrap for new transactional emailspnpm db:setup # first-time local setup (Docker + migrations + .env.local)
pnpm db:start # start Supabase + reset + sync env
pnpm db:stop # stop Docker Supabase
pnpm db:reset # re-apply supabase/migrations
pnpm sync:env # sync Supabase auth URLs from NEXT_PUBLIC_APP_URL
pnpm dev # Next.js dev server (port from NEXT_PUBLIC_APP_URL)
pnpm build && pnpm start # production build
pnpm lint
pnpm typecheck
pnpm test # unit tests (vitest)
pnpm test:e2e # Playwright
pnpm webhook:test:mailtrap # local Mailtrap webhook test (no ngrok)
pnpm lighthouse # Lighthouse CI
Schema changes: edit supabase/migrations/, then pnpm db:reset. The drizzle/ folder mirrors schema for Drizzle ORM queries; db:generate / db:push are optional and not used by default setup.
supabase/migrations/ SQL on production Postgres (or link hosted Supabase).env.examplehttps://YOUR_APP/api/webhooks/stripehttps://YOUR_APP/api/webhooks/mailtraphttps://YOUR_APP/api/auth/send-emailADMIN_EMAILS for admin accessMIT
Hacker News (1)
TypeScript
90.1%
JavaScript
8.3%
Shell
1.3%
Official Mailtrap Next.js SaaS starter
0
stars
54
commits
TypeScript
primary language
Sep 10, 2026
updated
Blank-slate Next.js SaaS scaffolding with Mailtrap as the email layer, Supabase auth, and Stripe billing.
For learners, indie hackers, and juniors — not production-grade multi-tenant SaaS.
git clone https://github.com/mailtrap/nextjs-saas-starter.git
cd nextjs-saas-starter
pnpm install
pnpm db:setup # Docker: Supabase Auth + Postgres + schema → writes .env.local
Copy remaining values from .env.example into .env.local:
ADMIN_EMAILS (for /admin/emails)Optional Stripe test products/prices:
stripe fixtures stripe-fixtures.json --api-key "$STRIPE_SECRET_KEY"
Start the app (port comes from NEXT_PUBLIC_APP_URL):
pnpm dev
Open the URL in .env.local (default http://127.0.0.1:3000).
NEXT_PUBLIC_APP_URL is the single source of truth for:
pnpm dev)site_url and auth redirectsIf port 3000 is taken, set e.g. NEXT_PUBLIC_APP_URL=http://127.0.0.1:3001, then:
pnpm sync:env
pnpm db:stop && pnpm db:start
pnpm dev
pnpm db:setup doessupabase/migrations/ (source of truth for schema).env.localNEXT_PUBLIC_APP_URLStop Docker: pnpm db:stop · Reset database: pnpm db:reset · Restart: pnpm db:start
Create a project at supabase.com, run SQL from supabase/migrations/, copy API keys into .env.local, and configure:
NEXT_PUBLIC_APP_URLhttps://YOUR_APP/api/auth/send-emailIn a second terminal (use the port from NEXT_PUBLIC_APP_URL):
stripe listen --forward-to localhost:3001/api/webhooks/stripe --api-key "$STRIPE_SECRET_KEY"
Copy the webhook signing secret into STRIPE_WEBHOOK_SECRET in .env.local.
Set MAILTRAP_WEBHOOK_SECRET in .env.local, then simulate an event:
pnpm webhook:test:mailtrap
Refresh /admin/emails to see the test event. In production, point Mailtrap webhooks at https://YOUR_APP/api/webhooks/mailtrap.
| Group | What you need |
|---|---|
| App | NEXT_PUBLIC_APP_URL — dev port, redirects, Supabase hook (pnpm sync:env after changes) |
| Supabase | 4 vars — auto-filled by pnpm db:setup locally |
| Stripe | STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_PRO, STRIPE_PRICE_TEAM |
| Mailtrap | API token, sandbox flag + inbox ID, from address, 8 template UUIDs, webhook secret |
| Auth hook | SEND_EMAIL_HOOK_SECRET — auto-filled locally |
| Admin | ADMIN_EMAILS — comma-separated emails for /admin/emails |
Full list: .env.example
MAILTRAP_SANDBOX=true
MAILTRAP_ACCOUNT_ID=12345 # optional, silences SDK warning
MAILTRAP_TEST_INBOX_ID=123456 # Email Testing → inbox settings
MAILTRAP_WEBHOOK_SECRET=your-secret # local: any string; match in Mailtrap dashboard for prod
Emails appear in your Mailtrap Email Testing inbox, not the recipient's real mailbox. Set MAILTRAP_SANDBOX=false before production deploy.
src/app/ → routes (marketing, auth, dashboard, API webhooks)
src/actions/ → Server Actions (auth, billing, org, settings)
src/lib/mailtrap → sendEmail(), webhook signature verify
src/config/emails → typed template UUID map
src/db/ → Drizzle schema (profiles, orgs, subscriptions, email events)
Email flows: Supabase Send Email Hook → Mailtrap (magic link, password reset, email change). App events → sendEmail() (welcome, invites, billing).
Billing: Stripe Checkout for upgrades; downgrades cancel or update the existing subscription via changePlan().
src/features/your-feature/ — see src/features/README.mdsrc/app/dashboard/your-feature/page.tsx (or another app route)sendEmail() from @/lib/mailtrap for new transactional emailspnpm db:setup # first-time local setup (Docker + migrations + .env.local)
pnpm db:start # start Supabase + reset + sync env
pnpm db:stop # stop Docker Supabase
pnpm db:reset # re-apply supabase/migrations
pnpm sync:env # sync Supabase auth URLs from NEXT_PUBLIC_APP_URL
pnpm dev # Next.js dev server (port from NEXT_PUBLIC_APP_URL)
pnpm build && pnpm start # production build
pnpm lint
pnpm typecheck
pnpm test # unit tests (vitest)
pnpm test:e2e # Playwright
pnpm webhook:test:mailtrap # local Mailtrap webhook test (no ngrok)
pnpm lighthouse # Lighthouse CI
Schema changes: edit supabase/migrations/, then pnpm db:reset. The drizzle/ folder mirrors schema for Drizzle ORM queries; db:generate / db:push are optional and not used by default setup.
supabase/migrations/ SQL on production Postgres (or link hosted Supabase).env.examplehttps://YOUR_APP/api/webhooks/stripehttps://YOUR_APP/api/webhooks/mailtraphttps://YOUR_APP/api/auth/send-emailADMIN_EMAILS for admin accessMIT
Hacker News (1)
TypeScript
90.1%
JavaScript
8.3%
Shell
1.3%