Production SaaS Starter

Production SaaS Starter

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

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.

Setup~3 min

Install dependencies

Install npm packages from the project root before configuring environment variables.

Run this once after cloning the repository:

Terminal
npm install
Prerequisite~5 min

Environment setup

Configure .env.local before running the app. Create the file first, then add values section by section as you complete setup below.

Create your local env file

Copy .env.example → .env.local at the project root before continuing. Add Stripe and Resend values in the sections below. Supabase variables are configured in the Supabase setup section.

Terminal
cp .env.example .env.local

Windows: copy .env.example .env.local

App

Required

Your deployed app URL — used for checkout redirects, email links, and OAuth callbacks.

Steps

  1. 1For local dev, set NEXT_PUBLIC_APP_URL=http://localhost:3000
  2. 2For production, set it to your Vercel deployment URL or custom domain
  3. 3Add the variable in Vercel → Project → Settings → Environment Variables

Environment variables

NEXT_PUBLIC_APP_URLRequired

Public 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_NAME

Brand name shown in transactional emails.

Where to find

Choose any display name — no external service needed

Example: Acme SaaS

Allowed origins

Configure cross-origin access for CSRF validation and server actions.

ALLOWED_ORIGINS

Comma-separated list of frontend domains allowed to access APIs.

.env.local
ALLOWED_ORIGINS=
Single origin
ALLOWED_ORIGINS=http://localhost:3000
Multiple origins
ALLOWED_ORIGINS=http://localhost:3000,https://example.com
Setup~5 min

Supabase setup

Complete these Supabase steps in order — create your project, then add environment variables.

Complete these Supabase steps in order — create your project, then add environment variables.

2 steps — complete in order

Supabase setup

~5 min

Create your project and copy your Project ID

1

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 Dashboard
2

Copy your Project ID

You will use this when building your API URL and configuring the app.

Where to find it: Supabase Dashboard → Project → Project Settings → Project ID

Supabase environment variables

Add these to .env.local after creating your Supabase project.

Both keys are located on the same page.

NEXT_PUBLIC_SUPABASE_URL

Your project API URL.

If you have not created a project yet:

.env.local
NEXT_PUBLIC_SUPABASE_URL=https://YOURPROJECTID.supabase.co

Where to find it

Supabase Dashboard → Open your project → Settings → API → Project URL

NEXT_PUBLIC_SUPABASE_ANON_KEY

Public anon key for client-side Supabase Auth and queries.

Where to find it

Supabase Dashboard → Project Settings → API Keys → Legacy API Keys → anon public

SUPABASE_SERVICE_ROLE_KEY

Server-only service role key — never expose to the client.

Where to find it

Supabase Dashboard → Project Settings → API Keys → Legacy API Keys → service_role

Super admin email

Add after Supabase credentials are configured.

SUPER_ADMIN_EMAIL

Email address that should receive super admin permissions.

.env.local
SUPER_ADMIN_EMAIL=
Example
SUPER_ADMIN_EMAIL=admin@example.com
Configuration~15 min

Stripe setup

Stripe CLI

Use the Stripe CLI for local authentication and webhook forwarding during development.

  1. 1Install Stripe CLI using the instructions from the official Stripe documentation
  2. 2Complete the platform-specific installation steps
  3. 3Run Stripe authentication/login
  4. 4Continue with local Stripe development after authentication
Stripe CLI installation guide

Authenticate the CLI

Run login — a browser window opens to connect your Stripe account:

Terminal
stripe login

Forward webhooks locally

First run:

Terminal
stripe listen --forward-to localhost:3000/api/stripe/webhook

Then copy the generated signing secret (whsec_...) from the terminal output:

Terminal
whsec_xxxxx

Paste it into .env.local:

.env.local
STRIPE_WEBHOOK_SECRET=whsec_xxxxx

API keys

Open the Stripe Dashboard and copy your keys into .env.local — never hardcode keys in the repository or documentation.

Open Stripe Dashboard
  • NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY — copy the Publishable key
  • STRIPE_SECRET_KEY — copy the Secret key
Configuration~10 min

Resend setup

Resend

Required

Transactional email — invites, 2FA OTP, password reset. Supabase built-in email is not used.

Steps

  1. 1Sign up at resend.com
  2. 2Add and verify your sending domain (DNS records)
  3. 3Create an API key
  4. 4Set RESEND_FROM_DOMAIN to your verified sending domain (see Resend setup below)

Environment variables

RESEND_API_KEYRequired

API key for sending emails.

Where to find

Resend Dashboard → API Keys → Create API Key

RESEND_FROM_DOMAIN

Must contain your verified Resend domain. Use the root/origin domain only — not a subdomain.

.env.local
RESEND_FROM_DOMAIN=
Example
RESEND_FROM_DOMAIN=shipy.live
Setup~2 min

Quick start

After completing environment, Supabase, Stripe, and Resend setup above, run:

Sync seeded plans to Stripe — mainly for testing and keeping Stripe plans in sync with the database

Terminal
npm run sync:plans

🎉 Congratulations!

Everything is now configured. Start the application:

Terminal
npm run dev

You're ready to go!

Reference~3 min

Development 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:

Terminal
npm run dev

Production preview

Preview the production environment locally — build the app, then start the production server:

Terminal
npm run build
Terminal
npm run start
Reference

Scripts reference

CommandDescription
npm run sync:plansSync seeded plans to Stripe products/prices
npm run devStart development server
npm run buildProduction build
npm run startStart production server (after build)
npm run testRun Jest unit tests (services only)
Reference

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:

1

Layer 1

Presentation — src/app/, src/components/

2

Layer 2

Application — server actions, createRequestContext(), RBAC guards

3

Layer 3

Domain — src/modules/*/ *.service.ts (business rules)

4

Layer 4

Data — repositories + PostgreSQL RLS/RPCs via src/services/

Reference

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.

Reference

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 management
  • admininvite/remove members, update workspace settings
  • memberread and participate — no admin actions
Reference

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~15 min

Deployment

Recommended stack: Vercel (Next.js) + Supabase Cloud + Stripe webhooks.

  1. 1Set all env vars in Vercel — use the Environment setup section above
  2. 2Deploy the Next.js app
  3. 3Register Stripe webhook → https://YOUR_APP_URL/api/stripe/webhook
  4. 4Bootstrap super admin and run sync:plans in production