Vaultix-IDBlog
← Back to homeGet started free
Architecture

Multi-Tenant SaaS Auth: Organizations, Roles, and Tenant Isolation

How to architect multi-tenant authentication for B2B SaaS — data model, JWT org context, row-level security, and RBAC patterns.

S

Smritix AI LLP

May 5, 2026 · 10 min read

Multi-tenant authentication is one of the most complex parts of building a B2B SaaS product. You need users to belong to organizations, each organization to have its own members and roles, and your application to enforce permissions consistently across every API endpoint and UI surface. Get this wrong and you have data leakage between tenants — one of the most damaging security failures a SaaS company can have.

This guide covers the architecture patterns, data model, and implementation approach for multi-tenant auth in a Next.js + Supabase application using Vaultix-ID.

The Core Data Model

Every multi-tenant auth system needs these entities:

  • Users — individual human accounts with a unique identity (email + user ID)
  • Organizations — a tenant (a company, team, or workspace). Users belong to one or more orgs.
  • Memberships — the join table connecting users to orgs, with a role field
  • Roles — typically owner, admin, member. Roles control what actions a user can take within an org.
-- Vaultix-ID schema (simplified)
CREATE TABLE vx_organizations (
  id          TEXT PRIMARY KEY,
  name        TEXT NOT NULL,
  slug        TEXT UNIQUE,
  created_at  TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE vx_org_memberships (
  id          TEXT PRIMARY KEY,
  org_id      TEXT REFERENCES vx_organizations(id) ON DELETE CASCADE,
  user_id     TEXT REFERENCES vx_users(id) ON DELETE CASCADE,
  role        TEXT NOT NULL DEFAULT 'member',  -- owner | admin | member
  created_at  TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE (org_id, user_id)
);

Embedding Org Context in the JWT

When a user has an active organization session, the JWT should carry the org context so downstream services can authorize requests without a DB lookup:

// JWT payload with org context
{
  "sub": "user_01HXYZ",
  "org_id": "org_acme",
  "org_role": "admin",
  "org_slug": "acme-corp",
  "exp": 1717747200
}

Vaultix-ID automatically includes org_id, org_role, and org_slug in the JWT when the user has an active org session. Your middleware can read these claims without touching the database.

Enforcing Tenant Isolation

At the API layer

// Always scope DB queries to the authenticated org
// app/api/projects/route.ts
import { auth } from "@vaultix.ai/nextjs/server";
import { supabase } from "@/lib/supabase";

export async function GET() {
  const { userId, orgId, orgRole } = await auth();
  if (!userId || !orgId) return Response.json({ error: "Unauthorized" }, { status: 401 });

  // ALWAYS filter by org_id — never trust client-supplied org IDs
  const { data } = await supabase
    .from("projects")
    .select("*")
    .eq("org_id", orgId);  // orgId comes from the verified JWT

  return Response.json(data);
}

At the database layer (Row Level Security)

-- Supabase RLS policy — belt-and-suspenders defense
CREATE POLICY "org isolation" ON projects
  FOR ALL USING (
    org_id = current_setting('app.current_org_id', true)
  );

-- Set the org context at query time from your verified JWT
-- This means even a buggy API endpoint can't leak cross-tenant data
⚠️

Never trust an org_id parameter from the request body or URL. Always derive it from the verified JWT claims. A user switching org context should require re-authentication or an explicit org-switch API call.

Role-Based Authorization

// Helper to check org role in server components
import { auth } from "@vaultix.ai/nextjs/server";

async function requireOrgRole(minRole: "member" | "admin" | "owner") {
  const { orgRole } = await auth();
  const hierarchy = { member: 0, admin: 1, owner: 2 };
  if ((hierarchy[orgRole as keyof typeof hierarchy] ?? -1) < hierarchy[minRole]) {
    throw new Error("Insufficient permissions");
  }
}

// Usage in an admin-only server action
export async function deleteOrganization() {
  await requireOrgRole("owner");
  // ... proceed with deletion
}

Inviting Members to an Organization

Vaultix-ID handles the full invitation flow: create an invitation, send an email, accept via a hosted page or your own UI.

// POST /v1/orgs/:orgId/invitations
const res = await fetch(`${apiBase}/v1/orgs/${orgId}/invitations`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${secretKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    email: "newmember@example.com",
    role: "member",
    redirectUrl: "https://yourapp.com/accept-invitation",
  }),
});
// Vaultix-ID sends the invitation email via Resend automatically

Handling Org Switching

Users who belong to multiple organizations need a way to switch context. The pattern is to issue a new session scoped to the target org:

// POST /v1/orgs/:orgId/activate — switches the active org session
// The response sets a new vaultix-session cookie with the new org context embedded
// Your UI reads the active org from useAuth():

const { orgId, orgRole, orgSlug } = useAuth();

Conclusion

Multi-tenant auth is non-trivial but follows a clear pattern: derive org context from verified JWTs, always scope queries by org_id, enforce RLS at the database layer as a backstop, and never trust client-supplied tenant identifiers. Vaultix-ID implements this pattern out of the box — start building your multi-tenant SaaS today.

multi-tenantsaasrbacorganizationsb2b auth

Ready to build?

Add authentication to your app in 5 minutes.

Self-hosted, Clerk-compatible, no per-MAU fees.

Get started free →

More articles

Clerk Alternative: Self-Hosted Auth That Saves You $800/Month

8 min read

JWT vs Session Cookies: The Complete 2026 Guide

7 min read

How to Add Magic Link (Passwordless) Auth to Your Next.js App

6 min read