# Multi-tenant SaaS with RLS: the full isolation pattern
Tenant isolation that lives only in application code is a bug waiting for one missed filter. RLS enforces it at the database, so every query path, including direct API access, respects tenancy.
## The schema
1. An `orgs` table (id, name, ...) and an `org_members` table (org_id, user_id, role). Membership is data, not a JWT claim you hand-edit.
2. Every tenant-scoped table gets an `org_id` column, not null, with a foreign key to orgs. No nullable tenant ids: null breaks the isolation logic silently.
3. A helper function like `is_org_member(org_id)` marked stable and security definer, so policies stay readable and the membership check is consistent.
## The policies
4. SELECT/INSERT/UPDATE/DELETE policies on each tenant table requiring `org_id` in (select org_id from org_members where user_id = auth.uid()). Users see only their orgs' rows.
5. WITH CHECK on insert and update must constrain `org_id` to a membership, or a user can move rows into another org by updating the column.
6. Admin/backoffice access goes through a separate service-role path, never by widening the tenant policies.
## Ordering constraints
7. Create orgs and memberships before the tenant tables that reference them. Seed a test org and two users in different orgs before writing app code.
8. Test the matrix: user A reads org A rows (allowed), user A reads org B rows (empty, not error), user A writes with org B id (rejected). An empty result for cross-tenant reads is correct; an error would leak existence.
## Verification
Run the full matrix as both users plus anon. Then add a new tenant table and confirm you cannot forget the policy: make "every tenant table has four policies" a migration checklist item.