Architecture

Permission Design for Multi-Tenant SaaS: Practical Patterns and Common Mistakes

How to design org, role, and resource-based permissions in multi-tenant SaaS, with RBAC vs ABAC vs ReBAC tradeoffs and code.

Who should read this

Summary: In B2B SaaS, permissions are the area most often deferred with “we will handle it later” — and the most painful to refactor. The safest path is to design around three axes from day one (tenant, role, resource), start with RBAC, and extend to ReBAC only when the need is proven.

This article is written for backend developers designing multi-tenant SaaS systems.

Comparing three permission models

RBACABACReBAC
Core concept User -> Role -> PermissionAttribute (context) based policiesRelationship (graph) based
Best fit 10 or fewer rolesComplex attribute combinationsResource ownership is central
Implementation difficulty Low (3 DB tables)Medium (policy engine)High (relationship graph)
Performance O(1) lookupPolicy evaluation costGraph traversal cost
Prominent tools Build your ownOPA, CedarOpenFGA, Oso, SpiceDB
Example Admin, Editor, Viewer"dept=Marketing AND time=business hours""user owns doc via folder"
Most B2B SaaS products start with RBAC and extend to ReBAC when needed.

Starting with RBAC — three DB tables

schema.sql SQL
-- Minimal multi-tenant RBAC schema
CREATE TABLE organizations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  plan TEXT DEFAULT 'free'
);

CREATE TABLE roles (
  id SERIAL PRIMARY KEY,
  org_id UUID REFERENCES organizations(id),
  name TEXT NOT NULL,  -- 'owner', 'admin', 'editor', 'viewer'
  permissions JSONB NOT NULL DEFAULT '[]'
);

CREATE TABLE org_members (
  user_id UUID NOT NULL,
  org_id UUID REFERENCES organizations(id),
  role_id INT REFERENCES roles(id),
  PRIMARY KEY (user_id, org_id)
);

The cardinal rule: every query must include an org_id filter. Fetching data without WHERE org_id = $current_org is a cross-tenant data leak.

Defense in depth — three layers of permission checks

  1. API middleware — Verify role when the request arrives (first line of defense)
  2. DB query — Row Level Security or mandatory WHERE org_id = ? (second line of defense)
  3. UI rendering — Hide buttons and menus the user lacks permission for (UX, not security)

Using Supabase Row Level Security (RLS) makes the second line of defense automatic at the database level.

When to go beyond RBAC

  • Resource ownership matters: “Only the creator of this document can delete it” — move to ReBAC (relationship-based)
  • Complex attribute combinations: “Marketing team + business hours + specific region” — move to ABAC (attribute-based)
  • Role explosion (20+ roles): Consider ABAC or ReBAC to tame the complexity

Most B2B SaaS products get by for 3—5 years with RBAC plus a single “resource owner” check.

Pitfalls to avoid

Further reading