Production SaaS Starter
Multi-Tenant SaaS Platform
Production-oriented B2B SaaS foundation with workspace-based multi-tenancy, Stripe billing, platform admin, RBAC, and i18n. This guide gets you from clone to running locally in under an hour.
Overview
The Production SaaS Starter is a workspace-based multi-tenant platform — not a minimal demo. It includes real patterns: PostgreSQL RLS, SECURITY DEFINER RPCs, Stripe webhook idempotency, dual RBAC layers, and a service-provider abstraction.
- Workspaces with memberships, invites, and slug-scoped URLs
- Stripe subscriptions, usage metering, promo codes, and enterprise deals
- Platform admin console with audit logs and user/workspace moderation
- Email/password + OAuth, 2FA, password reset via Resend
Stack: Next.js 16, React 19, Supabase, Stripe, Resend, next-intl with 4 locales.
Install dependencies
Install npm packages from the project root before configuring environment variables.
Run this once after cloning the repository:
npm installEnvironment setup
Configure .env.local before database migrations or running the app. Create the file first, then add values section by section as you complete setup below.
Create your local env file
Copy the example file — add Stripe and Resend values in the sections below. Supabase variables are configured in the Supabase setup section.
cp .env.example .env.localApp
RequiredYour deployed app URL — used for checkout redirects, email links, and OAuth callbacks.
Steps
- 1For local dev, set NEXT_PUBLIC_APP_URL=http://localhost:3000
- 2For production, set it to your Vercel deployment URL or custom domain
- 3Add the variable in Vercel → Project → Settings → Environment Variables
Environment variables
NEXT_PUBLIC_APP_URLRequiredPublic URL of your SaaS app (no trailing slash).
Where to find
Local: http://localhost:3000 · Production: your Vercel project domain or custom domain
Example: https://app.yourdomain.com
NEXT_PUBLIC_APP_NAMEBrand name shown in transactional emails.
Where to find
Choose any display name — no external service needed
Example: Acme SaaS
Supabase setup
Complete these Supabase steps in order — project creation, environment variables, and database migration.
Complete these Supabase steps in order — project creation, environment variables, and database migration.
7 steps — complete in order
Supabase setup
~5 minCreate your project and copy your Project ID
Create a Supabase project
Sign in at supabase.com, click New project, choose a name and region, set a database password, and wait for provisioning to finish.
Open Supabase DashboardCopy your Project ID
You will use this when linking the CLI and building your API URL.
Supabase environment variables
Add these to .env.local after creating your Supabase project — before running database migrations.
NEXT_PUBLIC_SUPABASE_URLYour project API URL. Replace YOURPROJECTID with your actual Supabase Project ID.
NEXT_PUBLIC_SUPABASE_URL=https://YOURPROJECTID.supabase.coWhere to find it
Supabase Dashboard → Project Settings → API
NEXT_PUBLIC_SUPABASE_ANON_KEYPublic anon key — safe for browser use. Copy from your dashboard; do not use a placeholder value.
Where to find it
Supabase Dashboard → Project Settings → API
SUPABASE_SERVICE_ROLE_KEYServer-only key. Never expose to the client. Copy from your dashboard; do not use a placeholder value.
Where to find it
Supabase Dashboard → Project Settings → API
Database setup and migrations
~15 minCLI setup, link, and schema migration
Install Supabase CLI
Install globally, then verify:
npm install -g supabaseVerify installation:
supabase --versionLog in to Supabase CLI
A browser window opens for authentication. Sign in and return to the terminal.
supabase loginLink your local project
Replace PROJECT_REF with your Project ID from step 2.
supabase link --project-ref <PROJECT_REF>Example:
supabase link --project-ref abcdefghijklmnopApply database migrations
Creates all tables, functions, policies, and seed data. Ensure Supabase credentials are in .env.local before continuing.
supabase db pushVerify tables were created
Confirm your schema is in place before continuing.
Updating database
After pulling repo updates with database changes:
git pull
supabase db pushStripe setup
Stripe CLI
Use the Stripe CLI for local authentication and webhook forwarding during development.
- 1Install Stripe CLI using the instructions from the official Stripe documentation
- 2Complete the platform-specific installation steps
- 3Run Stripe authentication/login
- 4Continue with local Stripe development after authentication
Authenticate the CLI
Run login — a browser window opens to connect your Stripe account:
stripe loginForward webhooks locally
In a separate terminal, forward events to your local webhook handler. Copy the signing secret into STRIPE_WEBHOOK_SECRET:
stripe listen --forward-to localhost:3000/api/stripe/webhookAPI keys
Copy your keys from the Stripe dashboard into .env.local — never hardcode keys in the repository or documentation.
Where to find them
Stripe Dashboard → Developers → API Keys
- Publishable Key (pk_test_...)
- Secret Key (sk_test_...)
Environment variables
STRIPE_SECRET_KEYRequiredServer-side Stripe API key — copy from your dashboard, never hardcode.
Where to find
Stripe Dashboard → Developers → API Keys — Secret key (sk_test_...)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYRequiredClient-side key for Stripe.js checkout — copy from your dashboard, never hardcode.
Where to find
Stripe Dashboard → Developers → API Keys — Publishable key (pk_test_...)
Webhooks
Steps
- 1For local development, run stripe listen --forward-to localhost:3000/api/stripe/webhook and copy the webhook signing secret
- 2For deployed environments, add webhook endpoint: https://YOUR_APP_URL/api/stripe/webhook
- 3Select events: checkout.session.completed, customer.subscription.*, invoice.*, payment_method.*
- 4Copy the webhook signing secret to STRIPE_WEBHOOK_SECRET in .env.local
Environment variables
STRIPE_WEBHOOK_SECRETRequiredVerifies webhook signatures from Stripe.
Where to find
Stripe Dashboard → Developers → Webhooks → Signing secret · Local dev: output of stripe listen
Resend setup
Resend
RequiredTransactional email — invites, 2FA OTP, password reset. Supabase built-in email is not used.
Steps
- 1Sign up at resend.com
- 2Add and verify your sending domain (DNS records)
- 3Create an API key
- 4Set RESEND_FROM_EMAIL to a verified address on your domain
Environment variables
RESEND_API_KEYRequiredAPI key for sending emails.
Where to find
Resend Dashboard → API Keys → Create API Key
RESEND_FROM_EMAILRequiredVerified sender address (e.g. noreply@yourdomain.com).
Where to find
Resend Dashboard → Domains → Add domain → verify DNS → use an address on that domain
Quick start
After completing environment, Supabase, Stripe, and Resend setup above, run these commands in order:
Install project dependencies
npm installSync seeded plans to Stripe — mainly for testing and keeping Stripe plans in sync with the database
npm run sync:plansStart dev server at http://localhost:3000
npm run devDevelopment vs production
Use different commands for day-to-day development and for previewing a production build locally.
Development
Start the development server with hot reload:
npm run devProduction preview
Preview the production environment locally — build the app, then start the production server:
npm run buildnpm run startScripts reference
| Command | Description |
|---|---|
| supabase db push | Apply database schema (supabase db push) |
| npm run sync:plans | Sync seeded plans to Stripe products/prices |
| npm run dev | Start development server |
| npm run build | Production build |
| npm run start | Start production server (after build) |
| npm run test | Run Jest unit tests (services only) |
Architecture
Four layers with strict call order — agents and developers follow the same path:
UI → Server Actions / API → Services → Repositories → PostgreSQL (RLS/RPC)Key locations:
Layer 1
Presentation — src/app/, src/components/
Layer 2
Application — server actions, createRequestContext(), RBAC guards
Layer 3
Domain — src/modules/*/ *.service.ts (business rules)
Layer 4
Data — repositories + PostgreSQL RLS/RPCs via src/services/
Multi-tenancy
A workspace is the tenant — there is no separate organization table.
- Tenant key: workspaces.id (UUID); URLs use unique slug
- Memberships link users to workspaces with owner, admin, or member roles
- RLS policies enforce workspace_id IN (user's memberships)
Platform admins access /admin via platform_admins table with super_admin, platform_admin, or platform_viewer roles.
Authentication & authorization
Auth flow: login → optional 2FA → onboarding (no workspace) or workspace dashboard. Platform admin invites route to platform-onboarding.
- Email/password and OAuth via Supabase Auth
- 2FA via email OTP (Resend) — not Supabase built-in email
- Password reset with token-based flow and rate limiting
Workspace roles and permissions:
ownerbilling, delete workspace, full member managementadmininvite/remove members, update workspace settingsmemberread and participate — no admin actions
Billing & subscriptions
One active subscription per workspace, tied to Stripe customer and subscription IDs.
- Checkout for new subscriptions; portal for self-serve management
- Upgrades with proration; downgrades scheduled at period end
- Seeded plans: Starter ($29/mo), Pro ($39/mo), Enterprise (custom)
- Webhook at /api/stripe/webhook syncs state with idempotent stripe_events table
Deployment
Recommended stack: Vercel (Next.js) + Supabase Cloud + Stripe webhooks.
- 1Apply schema to production Supabase (supabase db push)
- 2Set all env vars in Vercel — use the Environment setup section above
- 3Deploy the Next.js app
- 4Register Stripe webhook → https://YOUR_APP_URL/api/stripe/webhook
- 5Bootstrap super admin and run sync:plans in production

