๐ŸŒฑ Still tending the garden โ€” a few corners are still growing
All writing

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.