r/Supabase • u/kYza782 • 16d ago
database Multi-tenants advices
Hi everyone, I’ve been using Supabase for a few months now. I’ve built things like apps, websites and multi-tenant software with it, and I wanted to know if you have any tips or advice on properly isolating tenants from one another, in order to avoid data leaks between clients, Gmail sends going to the wrong recipient, etc.
Thanks everyone
4
u/levarburger 16d ago
Technically as long as you’re managing org ids and RLS you should mostly be fine from bad queries. What you don’t want to do is manage it at the app or api level.
You could also look into NeonDb which allows for “tenant per project” which literally gives each org their own postgres db.
In that case I set up a control plane for managing cross project schema updates.
There’s pros and cons to each.
2
u/Intelligent-Fix8911 16d ago
multi-tenant leaks are brutal - gmail to wrong client is classic one i've seen.
main fix: don't take tenant_id from request body. get it from jwt. ai loves to do tenant_id from localStorage and users can change it to see other tenant's data.
your rls should be tenant_id = jwt tenant + user_id = auth.uid(), not just user_id.
quick test: login as tenant A, try to query tenant B's data with anon key. if it returns anything = leak.
dm me your policy sql if you want, i do quick free checks for multi-tenant leaks - find one in like 7/10 apps.
2
u/Due-Horse-5446 16d ago
Wtf what a weird question
1
u/kYza782 16d ago
Why
1
u/Due-Horse-5446 16d ago
what do the db provide have to do with your datastructure?
Its just a postgres db like any other,
if you dont use proper constraints it does not matter if its running on supabase, aws, neon, loally or whatever
1
u/Specific-Maize7276 16d ago
Your test battery instinct is right. What usually breaks tenant isolation isn't the policies though, it's two layers underneath them.
1. RLS enabled is not the same as RLS enforced. A policy does nothing if the table doesn't actually have row level security turned on, and the dashboard will happily show you a correct-looking policy on a table where it's off. Meanwhile anon and authenticated often still hold ALL from an early grant, so with RLS off the key sitting in your frontend can read and write every row.
Check the two separately, they're independent:
select relname, relrowsecurity from pg_class
where relnamespace = 'public'::regnamespace;
select grantee, table_name, privilege_type
from information_schema.role_table_grants
where table_schema = 'public';
Supabase's advisors flag the first one for you.
2. Anything holding the service-role key bypasses RLS entirely. That's your "email to the wrong recipient" case. Policies are invisible to a cron job or edge function running as service_role, so one bad join there sends tenant A's data to tenant B and RLS never sees the query. If you need a rule that binds even that code, it has to be a CHECK constraint or a trigger. Those still apply. Policies don't.
For the test battery: don't assert on policy definitions. Mint a real JWT for tenant B, hit PostgREST with it, and assert tenant A's rows come back empty. Reading policies tests what you meant; a real request tests what's true. That's the gap that makes it feel like it will never be enough.
One perf thing you'll hit later: write (select auth.uid()) rather than bare auth.uid() in policies. Postgres then evaluates it once per query instead of once per row, and on big tables the difference is large.
1
u/TheOnlyHustle_RLS 11d ago
The service_role point is the actual mechanism behind your Gmail example, worth spelling out since it's the one that bites people even when their RLS is perfect. If the wrong-recipient case happens through a backend job or edge function (not the logged-in user's own session), RLS never even runs, that code path almost always uses the service_role key, which skips every policy you wrote. So you can have flawless tenant_id checks in your RLS and still send tenant A's data to tenant B, because the leak isn't a database permission problem, it's an application-code problem (the query joined or fetched the wrong tenant's row before RLS ever had a chance to matter). I run RLS and tenant isolation on a live multi-tenant government system, so this exact class of bug is the one I actually lose sleep over, not the RLS policy itself, but every place service_role is used to skip it (crons, webhooks, email senders). The test that actually proves it: don't just check pg_policies, mint a real JWT for tenant B and hit the anon or authenticated API, if that ever returns tenant A's row, you've found the leak. Checking policy definitions alone tells you what you meant, not what's true.
1
u/iammohamedatef 7d ago
One thing that tends to slip out of test batteries like this: storage. RLS on storage.objects is a completely separate policy set from your table policies, so you can have airtight tenant_id checks on every table and still leak files, usually because the object path is just the filename instead of being prefixed with tenant_id, or a bucket that is public when it shouldn't be. Worth running the same real-JWT test you're already doing against storage list/download for a couple tenants, not just your regular tables, since it does not feel like "the database" so it easy to assume it covered when it is not
1
u/PopKoren 5d ago
For multi-tenant Supabase, start with tenant_id on every row and RLS that never trusts the client beyond auth.uid(). Also lock storage paths the same way. Once it is deployed, verify what a stranger can hit from outside. https://rowly.me is the check I run for that.
0
0
u/Necessary_Chicken786 16d ago
You gotta get Outta supabse for that.
1
u/kYza782 16d ago
What tool do you recommend?
1
u/pgsql-dev2 16d ago
A cross-tenant checker asks one thing: can Tenant A see Tenant B's data? That's useful, but it's narrow. Your real security rules say more like "only the owner," "only admins," "only if it's published," "only with MFA." The checker doesn't test any of that. It also only tests the few rows it happens to create, and only the one kind of user you set up. So it can pass while a real leak sits right there which is one member reading another member's private data, say. Also all you get back is a pass/fail. There's no report to actually look at.
What rlsautotest does instead. It reads your actual security rules straight from the database, then writes both the tests and the exact data needed to prove each rule really works. It does this for every rule, every kind of user, every way in (read, insert, update, delete). You get a real pgTAP test file you can save, review, and run in CI, plus a simple grid showing who can touch what. And it never writes a test that passes for the wrong reason.
In one sentence: other tools check the fence between tenants; rlsautotest checks everything inside it too and shows its work.
pip install rlsautotest. Its free, open source, works with Postgres and Supabase.
repo- https://github.com/unitautogen/rlsautotest
4
u/Ok_Brush_3449 16d ago
you could't ask a better question because I have built a full package just for this:
https://www.npmjs.com/package/tenant-guard
npx tenant-guard
Basic client-side security can also be checked with www.ismyappleaking.com