Home / RLS / RLS vs app-layer authorization
Supabase RLS · Decision
RLS vs app-layer authorization: which and why
Make RLS the security floor — it's enforced by the database and can't be bypassed by a missing WHERE. Use app-layer checks on top for UX and business rules, never as the only thing between a user and someone else's data.
01 The difference that matters
App-layer authorization lives in your code: an if (row.userId !== me) or a .eq('user_id', me) on every query. It works — until the one query that forgot the filter. That single miss is a cross-tenant breach, and it's invisible in code review because the code looks fine.
RLS lives in the database. Once a policy is on the table, every query — from any client, any endpoint, any forgotten code path — is filtered. There's no query that can accidentally skip it. That's why RLS is a boundary and app-layer checks are convenience.
02 Use RLS as the floor
- Tenant isolation — who can see whose rows. This is security; it belongs in RLS, always.
- Ownership and membership — owner-scoped or workspace-scoped access. RLS.
- The service_role exception — trusted server jobs bypass RLS on purpose; keep that key server-only.
03 Where app-layer still belongs
- UX — hiding a button the user isn't allowed to use, before they click it.
- Business rules — "you can't archive a project with open invoices." Richer than a row filter; fine in code.
- Rate limits, workflow state, validation — logic that isn't about row ownership.
The rule of thumb: if getting it wrong leaks data, it must be in RLS. If getting it wrong is a worse experience or a broken rule, app-layer is fine.
04 Belt and suspenders
The strongest setup runs both: RLS guarantees isolation no matter what, and app-layer checks give fast, friendly feedback. If your app-layer filter and your RLS ever disagree, RLS wins — and that's the point. Prove the floor holds with an isolation test.
Catch this before it ships
airlock-rls is a CI gate that fails your build when a table ships exposed or a policy is permissive — the same class of bug, caught on the pull request instead of in prod.
Or start from nextjs-supabase-starter — auth + a table with RLS + an isolation test, so a fresh table is safe by default.
Grab the Supabase RLS cheat sheet
The golden rules, the footguns that leak in prod, correct policy snippets, and the isolation test — on one page.
FAQ
Is RLS enough on its own?
For tenant isolation, yes — that's exactly what it's built for. You'll still want app-layer logic for UX and business rules, but you don't need app-layer checks to make data access safe once RLS is correct and tested.
Does RLS replace my API's auth?
No — you still authenticate users (Supabase Auth) and may gate whole endpoints in code. RLS handles row-level access: given an authenticated user, which rows they may touch. The two layers complement each other.