Production‑grade, multi‑tenant Supabase backend with strict RBAC, hierarchical roles, secure storage, and policy‑driven access control.
SupaMax implements:
- Multi‑tenancy (tenant‑isolated data)
- Hierarchical RBAC (DAG‑based role tree)
- JWT‑embedded permissions (computed at login)
- Strict RLS everywhere (Postgres‑first security)
- Controlled file storage (Supabase Storage + DB‑backed authorization)
Designed for SaaS systems where roles, permissions, and ownership matter.
Core Layers
- PostgreSQL (schema, RLS, triggers, functions)
- Supabase Auth (email‑invite onboarding)
- Supabase Storage (DB‑verified access)
- JWT Custom Claims (permissions, hierarchy)
Security Model
- No trust in frontend
- All access enforced at DB level
- Service Role only for provisioning
Logical organization boundary. All data is tenant‑scoped.
- One root role per tenant
- Roles form a DAG (not a tree)
- Closure table used for fast hierarchy queries
- Flat permission codes (
dl:c,rl:u, etc.) - Assigned to roles
- Injected into JWT at login
PRIVATE → Owner only
CONTROLLED → Owner + subordinate roles
PUBLIC → Permission‑based
tenantsrolespermissionsrole_permissionsrole_hierarchyrole_closureprofilesinvitationsdeals
- O(1) permission checks
- Efficient subordinate resolution
- Prevents recursive runtime queries
-
User is invited (
invitationstable) -
User signs up via Supabase Auth
-
handle_new_usertrigger:- Validates invitation
- Creates profile
- Assigns tenant + role
No invitation → no access.
Injected via custom_access_token_hook:
{
"tenant_id": "uuid",
"role_id": "uuid",
"subordinate_role_ids": ["uuid"],
"permissions": ["dl:c", "rl:u"]
}Used by:
- RLS policies
- Access control functions
Central function:
check_resource_access(
tenant_id,
visibility,
owner_id,
owner_role_id,
prefix,
operation
)Controls:
- SELECT / INSERT / UPDATE / DELETE
- Ownership
- Role hierarchy
- Explicit permissions
No duplicated logic in policies.
Enabled on all tables.
Examples:
- Tenants → self only
- Profiles → self or tenant
- Roles → tenant‑scoped + permission‑gated
- Deals → delegated to access engine
Service Role bypasses all checks.
Bucket: deals
Rules:
-
Upload allowed only to
deals -
Download/update/delete only if:
- Matching DB record exists
- User has access to that deal
Storage access is DB‑verified, not public.
rpc('provision_tenant', {
p_name: 'Acme Corp',
p_slug: 'acme',
p_admin_email: 'admin@acme.com',
p_role_name: 'Admin',
p_permissions: ['dl:c','dl:r','dl:u','dl:d']
})Creates:
- Tenant
- Root role
- Permissions
- Admin invitation
Atomic and secure.
Enforced via triggers:
- No permission escalation
- No role escalation
- Single root per tenant
- Root role immutable
- DAG cycle prevention
Impossible to break hierarchy accidentally.
supabase start
supabase stop
supabase db reset
supabase statusService Logs:
| Service | Container |
|---|---|
| API | supabase_kong_SupaMax |
| DB | supabase_db_SupaMax |
| Auth | supabase_auth_SupaMax |
| Storage | supabase_storage_SupaMax |
- SMTP via SendGrid
- Auth email‑only
- Anonymous auth disabled
- Storage S3‑compatible
All secrets via environment variables.
Ideal for:
- B2B SaaS
- Internal enterprise tools
- Role‑heavy platforms
- Compliance‑sensitive systems
- Database is the authority
- JWTs are derived, not trusted
- UI is disposable
- Security is structural, not conditional
-- 1. ELEVATE PRIVILEGES
SET LOCAL request.jwt.claim.role = 'service_role';
DO $$ DECLARE
v_tenant_name TEXT := 'Acme Corp';
v_tenant_slug TEXT := 'acme';
v_email TEXT := 'div@gmail.com';
v_password TEXT := 'password';
v_provision_result JSON;
v_user_id UUID;
BEGIN
-- A. CLEANUP (Reset for fresh seed)
-- 1. Delete Auth User
DELETE FROM auth.users WHERE email = v_email;
-- 2. Delete Tenant (Cascades to Roles, Invitations, Profiles, Deals)
DELETE FROM public.tenants WHERE slug = v_tenant_slug;
-- B. PROVISION TENANT STRUCTURE
-- This call assumes you applied the fix to 'provision_tenant' (Explicit UUIDs)
-- It creates: Tenant, Root Role, Permissions, Invitation
SELECT public.provision_tenant(
p_name := v_tenant_name,
p_slug := v_tenant_slug,
p_admin_email := v_email,
p_role_name := 'Owner'
) INTO v_provision_result;
RAISE NOTICE 'Tenant Structure Provisioned: %', v_provision_result;
-- C. CREATE AUTH USER
-- Inserting into auth.users fires the 'handle_new_user' trigger.
-- That trigger finds the invitation created in Step B and creates the profile.
v_user_id := gen_random_uuid();
INSERT INTO auth.users (
instance_id,
id,
aud,
role,
email,
encrypted_password,
email_confirmed_at,
raw_app_meta_data,
raw_user_meta_data,
created_at,
updated_at,
-- Supabase requires these to be strings, not NULL
confirmation_token,
recovery_token,
email_change_token_new,
email_change
) VALUES (
'00000000-0000-0000-0000-000000000000', -- Standard Supabase Instance ID
v_user_id,
'authenticated',
'authenticated',
v_email,
crypt(v_password, gen_salt('bf')),
now(), -- Auto-confirm email
'{"provider": "email", "providers": ["email"]}',
'{"full_name": "Super Admin"}',
now(),
now(),
-- Empty Strings (Supabase crashes on NULL here)
'', '', '', ''
);
RAISE NOTICE 'SUCCESS: User created. Login with % / %', v_email, v_password;
END $$;