All case studies
Portfolio demoMay 4, 2026

Multi-tenant SaaS starter with Stripe billing

A production-shaped SaaS foundation — auth, workspaces, role-based access, and Stripe-billed subscriptions on a serverless stack.
Next.js 16
MUI v9
Auth.js v5
Neon Postgres
Stripe
Vercel

Why this exists

Most "SaaS boilerplates" stop at a logged-in dashboard. The hard parts of a real product — multi-tenant isolation, Stripe edge cases, last-admin guards, edge-safe middleware — only show up after the demo screenshots are done. This starter goes there on purpose.

What's built

  • Email/password auth with bcrypt and JWT sessions, edge-compatible middleware that protects /dashboard/*.
  • Multi-tenant by design. Every server query is scoped by an active-organization context derived from a signed cookie and re-validated as a real membership on every request. Org IDs are never accepted from the client.
  • RBAC with admin / member roles and a can(role, permission) helper.
  • Workspace switcher in the topbar; users belong to many organizations.
  • Member invites, role changes, removal — with a "last admin" guard so a workspace can never become admin-less.
  • Stripe-billed subscriptions per workspace — Checkout, Customer Portal, signature-verified webhooks.
  • "Sync from Stripe" recovery — pulls truth from Stripe when a webhook was missed.
  • Settings — display name, password change, workspace rename, danger-zone deletion that cancels the Stripe sub on the way out.

Technical choices worth calling out

Stripe is the source of truth, not the local DB

The duplicate-subscription guard, the "Sync from Stripe" button, and the webhook handler all derive state by asking Stripe — never the local DB. Webhooks can be dropped (especially in dev when stripe listen isn't running); the recovery paths assume that and reconcile from Stripe rather than papering over inconsistency.

Edge-safe middleware split

Auth.js v5 middleware runs in the Edge runtime where pg and bcrypt aren't available. lib/auth.config.ts is the edge-safe shared config used by proxy.ts; lib/auth.ts extends it with the credentials provider and Postgres lookup. Same callbacks, same JWT, two execution environments.

Last-admin guard, transactional

changeMemberRole and removeMember open a transaction, count current admins, and refuse the operation if it would leave the workspace admin-less. Means an admin can't lock themselves out by demoting themselves when they're the only one.

Outcome

The same building blocks ship into client SaaS products without rework — auth, billing, and tenant isolation that have been tested against the Stripe sandbox and held up under real failure modes (dropped webhooks, double-clicked Checkout buttons, mid-cancel role changes).

Live demo →