Docs

Documentation

B2B SaaS OS is a source-code boilerplate for building multi-tenant B2B SaaS with Next.js, Supabase and Stripe. This page covers the concepts behind the codebase and how to set it up. The full reference documentation ships inside the source package.

Everything on this page describes the codebase as it ships. Nothing here requires a hosted service beyond your own Supabase and Stripe accounts.

Getting Started

The package is a standard Next.js App Router project with strict TypeScript, Tailwind CSS v4 and a component library modeled on shadcn/ui. Marketing, auth and dashboard routes are separated into route groups, and the dashboard ships with a complete example application backed by clearly-labeled mock data.

Installation

Requirements: Node.js 20+ and pnpm (or npm/yarn of your choice). No global tools, no CLI login.

terminalbash
# 1. Unzip the package
unzip b2b-saas-os.zip && cd b2b-saas-os

# 2. Install dependencies
pnpm install

# 3. Configure environment (next section)
cp .env.example .env.local

# 4. Start the dev server
pnpm dev

Environment Variables

Copy .env.example to .env.local and fill in the values from your Supabase and Stripe dashboards.

VariableUsed forWhen
NEXT_PUBLIC_SITE_URLCanonical URL, metadata, sitemapNow
NEXT_PUBLIC_SUPABASE_URLSupabase project URLBackend stage
NEXT_PUBLIC_SUPABASE_ANON_KEYBrowser clientBackend stage
SUPABASE_SERVICE_ROLE_KEYServer-only admin tasksBackend stage
STRIPE_SECRET_KEYBilling APIBilling stage
STRIPE_WEBHOOK_SECRETWebhook signature verificationBilling stage

Supabase

You connect your own Supabase project. The package ships its schema as readable SQL migrations (organizations, members, invitations, projects, API keys, audit log, usage) plus one policy file per table, so you can run supabase db push and audit exactly what gets created.

Authentication

Supabase Auth handles credential storage and sessions. The package supplies the flow around it: sign-in, sign-up, forgot-password and reset-password pages, an auth callback route that exchanges codes for sessions, and OAuth wiring for Google and GitHub.

Organizations

An organization is the tenant root: members, roles, projects, keys, billing state and audit entries all belong to one. The membership table is the join point — a user can belong to many organizations, which is what makes the organization switcher work.

Multi-tenancy

Every tenant-owned row carries an organization_id. There is one database for all customers — isolation is achieved with Row Level Security, not with schema-per-tenant gymnastics. This keeps migrations simple and queries fast.

RLS

Policies are deny-by-default: a table is unreadable unless a policy explicitly allows it, and the policies check membership through the authenticated user id. Example shape:

supabase/policies/members.sqlsql
create policy "members_are_visible_to_their_org"
on organization_members for select
using (
  organization_id in (
    select organization_id from organization_members
    where user_id = auth.uid()
  )
);

RBAC

Four roles — Owner, Admin, Billing, Member — declared once in config/permissions.ts as a permission map. The same map drives Postgres policies and an in-app hasPermission() helper, so UI and database can never disagree about who may do what.

Stripe

The billing layer covers checkout sessions, the customer portal, seat-aware subscriptions, and webhook handlers that reconcile Stripe events (subscription updates, invoices, cancellations) into your database. You provide the API keys and the webhook endpoint URL.

Team Invitations

Invitations are rows with a secure random token, an expiry, and a pre-selected role. Accepting happens at /invite/[token], which validates the token and attaches the new member to the organization in one transaction.

API Keys

Keys use pk_live_ and pk_test_ prefixes, are stored hashed (the full value is shown exactly once at creation), support read-only and read-write scopes, and can be rotated or revoked per organization.

Audit Logs

An append-only table records who did what to which resource, with metadata — invite acceptance, role changes, key creation, billing events. Entries are written server-side, never from the browser.

Usage Limits

A metered counter tracks API requests per organization per window. The billing layer compares the counter against the plan's included quota and exposes an overage hook, so you can enforce limits or trigger upsells.

Customization

  • Branding lives in config/site.ts — name, tagline, price, URLs.
  • The color system is CSS variables in app/globals.css; restyle the whole app by editing one palette.
  • Navigation is data, not markup: config/navigation.ts.
  • Roles and permissions: config/permissions.ts.

Deployment

Deploy the Next.js app to any Node-compatible host (Vercel, Fly, your own box). Push migrations with the Supabase CLI, then register your deployed /api/stripe/webhook endpoint in the Stripe dashboard. Set all environment variables in the host's dashboard — nothing else is required.

Testing

The repo is structured for testability: pure helpers live in lib/, route handlers are thin, and the permission map is pure data. Quality gates included:

terminalbash
pnpm typecheck   # strict TypeScript, zero errors expected
pnpm lint        # eslint with next/core-web-vitals + next/typescript
pnpm build       # production build across all routes

Troubleshooting

  • Build fails on missing env — you skipped .env.local. Copy .env.example first.
  • Rows return empty unexpectedly — an RLS policy is filtering them; check that the user has an organization_members row for that tenant.
  • Stripe webhook returns 400 — the endpoint secret doesn't match; re-copy STRIPE_WEBHOOK_SECRET from the Stripe dashboard.