In a multi-tenant app, the worst bug you can ship is one customer seeing another customer's data. I build with Supabase a lot, and the feature that lets me sleep about this is row-level security. It moves the "can this user see this row" decision out of my application code and into the database itself.
Here's how I use it to keep tenants isolated, drawn from building Fasco, where shoppers must only ever see their own orders while an admin can see everything.
Why filtering in the frontend isn't enough
The naive version of access control is a query like "give me the orders where user_id equals the current user." It works until it doesn't. One forgotten filter on one endpoint, one clever request with a different ID, and you've leaked data. The rule lives in dozens of places in your code, and it only takes one of them to be wrong.
Row-level security flips this. You write the rule once, on the table, and the database enforces it on every single query no matter where it comes from. Even if my API code asks for every order in the table, Postgres hands back only the rows the current user is allowed to see.
Turning it on
By default a table with RLS enabled returns nothing. You opt in, then add policies that grant access. That "deny by default" posture is exactly what you want.
alter table orders enable row level security;At this point every query against `orders` returns zero rows until a policy says otherwise. That feels broken the first time, but it's the whole point: access is something you explicitly grant, not something you forget to remove.
A policy for "only your own rows"
Supabase exposes the logged-in user's ID through `auth.uid()`. A policy that lets people read only their own orders looks like this:
create policy "Users read their own orders"
on orders
for select
using (auth.uid() = user_id);The `using` clause is a condition Postgres checks against every row. If it's false for a row, that row simply doesn't exist as far as the query is concerned. No application code involved, no way to forget it.
You add separate policies for insert, update, and delete, each with its own condition, so a user can create an order for themselves but never modify someone else's.
Letting admins see everything
Tenant isolation can't mean nobody has the full picture. The store owner needs every order. I handle that with a second policy keyed off a role stored on the user, so admins pass the check that regular users fail.
create policy "Admins read all orders"
on orders
for select
using (
exists (
select 1 from profiles
where profiles.id = auth.uid()
and profiles.role = 'admin'
)
);Policies are additive. A row is visible if any policy allows it, so a normal user sees their own orders through the first policy, and an admin sees all of them through the second.
Test the isolation directly
Don't just click around as one user and assume it's fine. Sign in as user A, sign in as user B in another session, and confirm B genuinely cannot read A's rows even when you ask for them by ID. The point of RLS is that the database says no for you, so verify the database actually says no.
Once those policies are in place, multi-tenancy stops being a thing I have to remember in every query. The boundary lives in one place, the database enforces it, and a missing filter in my frontend can't turn into a data leak.


