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.
# 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 devEnvironment Variables
Copy .env.example to .env.local and fill in the values from your Supabase and Stripe dashboards.
| Variable | Used for | When |
|---|---|---|
| NEXT_PUBLIC_SITE_URL | Canonical URL, metadata, sitemap | Now |
| NEXT_PUBLIC_SUPABASE_URL | Supabase project URL | Backend stage |
| NEXT_PUBLIC_SUPABASE_ANON_KEY | Browser client | Backend stage |
| SUPABASE_SERVICE_ROLE_KEY | Server-only admin tasks | Backend stage |
| STRIPE_SECRET_KEY | Billing API | Billing stage |
| STRIPE_WEBHOOK_SECRET | Webhook signature verification | Billing 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:
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:
pnpm typecheck # strict TypeScript, zero errors expected
pnpm lint # eslint with next/core-web-vitals + next/typescript
pnpm build # production build across all routesTroubleshooting
- Build fails on missing env — you skipped
.env.local. Copy.env.examplefirst. - Rows return empty unexpectedly — an RLS policy is filtering them; check that the user has an
organization_membersrow for that tenant. - Stripe webhook returns 400 — the endpoint secret doesn't match; re-copy
STRIPE_WEBHOOK_SECRETfrom the Stripe dashboard.