Multi-Tenant Data Isolation with Supabase RLS, Tested Through Real Logins in a Small Invoice Portal A developer built a demo invoice portal on Supabase that enforces multi-tenant data isolation entirely through Row Level Security (RLS), with no tenant filter in the application page itself. Testing with the publishable API key showed that before RLS was enabled, all four invoices from both seeded companies were readable without any login; after enabling RLS and adding membership-based policies on the tenants, memberships, and invoices tables, access was restricted to each user's own tenant. The demo, published on GitHub, walks through the SQL policies and verifies them through real logins. Two customer companies, one Supabase project, one invoices table. How do you make sure each company sees only its own rows? This post walks through Row Level Security RLS from "no rules" to "tested through real logins": SQL, the checks that prove it, and a small invoice portal where the page has no tenant filter at all, because the database does the filtering. AI disclosure: I used Claude to help draft this post. I ran every step in my own project, checked the results, and edited the text. The mistakes are mine. Repo: https://github.com/tomozilla/rls-multitenancy-demo-en https://github.com/tomozilla/rls-multitenancy-demo-en Plain-language companion no code, no product names : https://medium.com/p/dd72e0e13bed https://medium.com/p/dd72e0e13bed tenants , memberships which user belongs to which company and invoices . service role or secret key in a page or a repo. In Authentication, Users, Add user, Create new user tick Auto Confirm User , create alice@example.com and bob@example.com . Choose any passwords; the SQL checks below do not need them. For the portal you will also create demo-acme@example.com and demo-globex@example.com later. create table public.tenants id uuid primary key default gen random uuid , name text not null ; create table public.memberships tenant id uuid not null references public.tenants id , user id uuid not null references auth.users id , primary key tenant id, user id ; create table public.invoices id bigint primary key generated always as identity, tenant id uuid not null references public.tenants id , title text not null, amount usd int not null ; The SQL Editor warns that you are creating tables without RLS. For this demo choose Run without RLS ; the point is to see what happens without it. Seed two companies, link Alice to Acme and Bob to Globex, and add 4 invoices the full file is sql/02 seed.sql : In the SQL Editor, pretend to be someone who is not logged in anon : begin; set local role anon; select t.name as tenant, i.title, i.amount usd from public.invoices i join public.tenants t on t.id = i.tenant id order by i.id; -- afterwards: rollback; All 4 rows. That is the SQL Editor pretending, so I also asked the real API with only the publishable key, the one every app ships with: curl -s "https://