May 30, 20261 views
I Stopped Double-Guarding My Supabase Queries
Letting Row-Level Security do the work it's meant to do โ and how the codebase relaxed when I did.
In this post
For a while, every query I wrote was doing the same work twice. The loader would check userId === resource.ownerId, and then the database would check it again because RLS was on. The app would refuse to fetch what the database would have refused to return anyway.
It worked, but it felt wrong โ like locking the front door, walking to the kitchen, and locking the pantry just in case.
The double-guard habit
The instinct is reasonable: trust no one, including yourself. So you guard at the route, at the resolver, at the query. The problem is that those guards drift out of sync. One gets updated, another doesn't, and now you've got three nearly-identical predicates with different opinions about who can see what.
The bug isn't usually that authorization fails โ it's that one layer relaxes silently, and you don't notice for weeks.
What RLS actually buys you
Supabase RLS, done well, gives you one place where access rules live: in SQL, alongside the table they govern. When the database is the source of truth for who-can-see-what, the application code stops needing to know.
That has two real benefits:
- Queries become declarative. I ask for the resource. If the user can't see it, I get nothing back. The shape of the code matches the shape of the intent.
- Reviews get easier. Access rules show up in migrations, with diffs, in the same review I'm already doing.
There's a third benefit that's subtler: tests against the real database verify the actual rule, not a mock of a mock.
Where I still check at the app layer
RLS isn't a replacement for everything. I still do app-layer checks for things the database can't see:
- Plan limits ("you can create five of these")
- Multi-step workflows where the database state is mid-update
- UX gating (showing the upgrade prompt before they hit the limit)
The rule I follow: authorization belongs in the database; rules-of-the-app belong in the app. Confusing those is what makes both layers messy.
Final thoughts
The version of the codebase where I trusted RLS ended up smaller, easier to read, and harder to break. The pantry has a lock for a reason. The kitchen doesn't need one too.
Comments
Be the first to say something.