🏗️ Enterprise Multi-Tenant SaaS Architecture

Hierarchical • Scalable • Secure

👥 Tenant Hierarchy Model

🔑 SUPER ADMIN

Platform Owner (You)

🏢 Tenant A

Client Organization

🏢 Tenant B

Client Organization

🏢 Tenant N...

Scales to 100s+

👤 User
👤 User
👤 User
👤 User
👤 ...

Each tenant's customers/users (isolated data)

🔐 Auth & Identity Layer

Supabase Auth

JWT tokens, OAuth providers

RBAC System

super_admin → admin → manager → user

Tenant Context

tenant_id in JWT claims

⚡ API Gateway Layer

Edge Functions

Supabase/Vercel serverless

Rate Limiting

Per-tenant quotas

Request Validation

Tenant isolation enforcement

🗄️ Data Layer

PostgreSQL + RLS

Row Level Security per tenant

tenant_id FK

Every table references tenants

Soft Deletes

deleted_at for data recovery

📊 Core Database Schema

tenants

id: uuid PK
name: text
slug: text UNIQUE
plan: enum
settings: jsonb
created_at: timestamp
deleted_at: timestamp

users

id: uuid PK
tenant_id: uuid FK
email: text
role: enum
profile: jsonb
last_login: timestamp

[your_resources]

id: uuid PK
tenant_id: uuid FK
created_by: uuid FK
data: jsonb
created_at: timestamp
updated_at: timestamp

audit_logs

id: uuid PK
tenant_id: uuid FK
user_id: uuid
action: text
entity: text
changes: jsonb
created_at: timestamp

🛡️ Row Level Security (RLS) Pattern

-- Enable RLS on all tenant tables
ALTER TABLE your_resources ENABLE ROW LEVEL SECURITY;

-- Tenant isolation policy
CREATE POLICY "tenant_isolation" ON your_resources
    FOR ALL
    USING (tenant_id = auth.jwt() ->> 'tenant_id');

-- Super admin bypass
CREATE POLICY "super_admin_access" ON your_resources
    FOR ALL
    USING (auth.jwt() ->> 'role' = 'super_admin');

-- Helper function for tenant context
CREATE OR REPLACE FUNCTION get_current_tenant_id()
RETURNS uuid AS $$
    SELECT (auth.jwt() ->> 'tenant_id')::uuid;
$$ LANGUAGE sql SECURITY DEFINER;

📈 Scalability Strategies

Horizontal DB Scaling

Read replicas, connection pooling (PgBouncer)

Tenant Sharding (Future)

Large tenants → dedicated schemas/DBs

Caching Layer

Redis for sessions, tenant configs

Background Jobs

Queue system for heavy operations

🏛️ Infrastructure Stack

Frontend

React (Lovable) → Vercel/Cloudflare Pages

Backend

Supabase (Auth + DB + Edge Functions)

Automation

n8n for workflows, webhooks, integrations

Payments

Stripe (per-tenant billing)

🎭 Role-Based Access Control Matrix

Permission Super Admin Tenant Admin Manager User
Manage all tenants
View platform analytics
Manage tenant settings
Invite/manage users
Create/edit resources
View own data

📚 How to Use This Architecture

1. Set Up Your Tenants Table

This is your "clients" table. Every business that signs up gets a row here.

CREATE TABLE tenants (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,  -- e.g., "acme-corp"
  plan TEXT DEFAULT 'free',   -- free, pro, enterprise
  created_at TIMESTAMPTZ DEFAULT now()
);

2. Add tenant_id to ALL Your Tables

Every table that holds customer data needs a tenant_id column. This is how you keep data separate.

CREATE TABLE products (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID REFERENCES tenants(id),  -- 👈 THIS IS KEY
  name TEXT,
  price DECIMAL
);

-- Same pattern for: orders, customers, invoices, etc.

3. Enable Row Level Security (RLS)

This is the magic. RLS automatically filters data so users only see their own tenant's data — no code needed.

-- Turn on RLS
ALTER TABLE products ENABLE ROW LEVEL SECURITY;

-- Create the policy (Supabase version)
CREATE POLICY "Users see own tenant data" ON products
  FOR ALL USING (
    tenant_id = (auth.jwt() ->> 'tenant_id')::uuid
  );

4. Store tenant_id in the User's JWT

When a user logs in, include their tenant_id in their auth token. Supabase does this via custom claims.

-- In Supabase: create a function that runs on login
CREATE FUNCTION set_tenant_claim() RETURNS trigger AS $$
BEGIN
  -- Add tenant_id to the user's JWT
  UPDATE auth.users SET raw_app_meta_data = 
    raw_app_meta_data || jsonb_build_object('tenant_id', NEW.tenant_id)
  WHERE id = NEW.user_id;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

5. That's It — Query Normally!

Now your app code stays simple. Just query the table — RLS handles the filtering automatically.

// In your React app (with Supabase client)
const { data } = await supabase
  .from('products')
  .select('*');

// 🎉 Returns ONLY products for the logged-in user's tenant
// No WHERE clause needed!

📋 Quick Summary

  • Tenants = Your clients (the businesses using your SaaS)
  • tenant_id = Goes on every table to tag who owns the data
  • RLS = Automatic filtering so Client A never sees Client B's data
  • Super Admin (you) = Bypasses RLS to see everything
  • Scale = Works for 1 tenant or 10,000 — same code

Architecture designed for Supabase + React/Lovable stack • Scales from 1 to 1000s of tenants