Multi-tenant architecture for Next.js B2B SaaS
Most Next.js SaaS templates fake multi-tenancy with a `team_id` column bolted onto a single-tenant demo. That works for a launch screenshot and falls over the first time security review asks where isolation is enforced. Real multi-tenancy is a database-level concern — Postgres Row Level Security policies attached to every tenant-scoped table — paired with auth flows that put workspace membership in the request context.
What 'workspaces as the isolation boundary' means
A workspace is the unit of customer ownership: one paying account, one set of billing, one set of members, one set of data. Every domain table (projects, documents, comments, settings) carries a `workspace_id` foreign key. The app never queries a domain table without scoping to a workspace; the database refuses to return rows the current user's workspace membership doesn't grant.
This pattern is different from 'pool tenancy with a tenant_id column' on two axes: where the check lives (in Postgres, not in application code) and what happens on a missing scope (the query returns zero rows, not a leaked row from another tenant). The first axis matters for code review confidence; the second matters for audit and incident response.
Postgres RLS as the enforcement layer
Supabase Auth sets a JWT claim that names the authenticated user. A small Postgres function reads that JWT and joins the user against the `workspace_members` table to derive the active workspace. RLS policies on every domain table reference that function. The policy is pinned to the database, not the ORM — Drizzle, Prisma, raw SQL all see the same isolation.
The trade-off is that schema changes must keep RLS policies up to date. Adding a new tenant-scoped table means writing the corresponding policy; SaaSForge Core ships migrations that pair table and policy in the same SQL file so this stays mechanical.
create policy "members read own workspace projects"
on projects for select
using (
workspace_id in (
select workspace_id
from workspace_members
where user_id = (auth.jwt() ->> 'sub')::uuid
)
);Four-role RBAC and what each role can do
Most B2B SaaS converges on the same role surface: an Owner who controls billing and ownership transfer, an Admin who manages members and settings, a Member who works on the product, and a Viewer who reads but does not write. SaaSForge Core ships these four roles with explicit permission checks attached to mutations.
The permission system is granular enough that you can define new actions (e.g., `documents.delete`, `billing.read`) without rewriting the role table. Roles map to permission sets; the UI hides what the user cannot do, and the API rejects what the UI hides. Both checks exist because hiding-only UIs leak through API testing tools.
What enterprise-shaped buyers ask about
Procurement reviews tend to ask the same questions: Is two-factor available? Is SSO supported? Are API keys rotated and masked? Are audit logs immutable? Is impersonation gated?
SaaSForge Core ships TOTP 2FA via Supabase Auth, SAML SSO hooks for Okta/Azure AD/Google Workspace, API keys with secret masking after creation, audit log rows on every mutation (actor, resource, metadata, timestamp), support impersonation with explicit allowlists, and optional IP allowlisting for workspace access. None of this is bolted on after the demo — they are first-class patterns in the codebase.