The Postgres feature that decides who sees which rows — enforced inside the database, not in your app code.
Research-based overview. This article synthesizes public documentation, pricing pages, and user reports. How we research.
Most web apps enforce auth in the application layer. You write a query like SELECT * FROM invoices WHERE user_id = $currentUser and trust that the WHERE clause is always present. RLS flips this around: even if a developer forgets the WHERE clause, the database refuses to return rows that do not belong to the calling user. The security check is moved inside the database engine.
Imagine a junior developer (or you, at 1 a.m., or an AI coding agent) writes this:
In a typical Express + Postgres app, this query runs successfully and returns the full table. If that query is exposed through any API endpoint — even an internal admin-only one that turns out to not check auth correctly — you have just leaked your entire customer database.
With RLS enabled and a sensible policy, the same query becomes:
The application can no longer accidentally return another user's data. The database refuses, regardless of which framework, ORM, or AI agent wrote the query.
This matters more in 2026 than in 2020. AI coding assistants generate hundreds of new SQL queries per project, and they do not have the same intuition for "did I include the user_id filter?" that an experienced human reviewer might. RLS is a guardrail that catches the agent's mistakes before they become incidents. Supabase has written extensively on this trade-off and recommends RLS by default for any table accessed from the client.
The mechanism has four parts. They have to all be in place; missing any one of them breaks the security model.
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY; — until you do, no policies apply.postgres superuser bypasses RLS by default. Your application must connect as a non-privileged role for policies to take effect.Here is a working example for a multi-tenant invoices table:
The USING clause is checked for SELECT/UPDATE/DELETE; the WITH CHECK clause is checked for INSERT/UPDATE to validate new or modified rows. Most policy mistakes come from confusing these two. Postgres docs on RLS at postgresql.org/docs/current/ddl-rowsecurity.html are the canonical reference.
| Backend | RLS support | Notes |
|---|---|---|
| Supabase | First-class, opinionated | RLS is the recommended pattern. Auth context (auth.uid(), auth.jwt()) is auto-injected. Dashboard has policy templates. |
| Neon | Native Postgres RLS works as standard | You manage session context yourself. No built-in auth helpers like Supabase's auth.uid(). |
| Firebase Firestore | Different model: security rules | Not RLS. JavaScript-like rules evaluated per request. Conceptually similar but separate from SQL. |
| PlanetScale | No RLS (MySQL/Vitess) | MySQL has no row-level security feature. Enforce in application layer. |
| Self-hosted Postgres | Native RLS available | Same as Neon — works, but you wire up auth context yourself. |
If you are choosing between Postgres providers, our deep dives on Supabase vs Firebase and Supabase vs Neon walk through how each handles auth and RLS in practice. Supabase gets you to "RLS in production" fastest because the auth context is a one-line setup; Neon gives you more control but expects you to wire it up.
postgres with the BYPASSRLS attribute, you have RLS configured but never enforced. Use a dedicated app role.auth.uid() but your app forgot to set the JWT in the connection, the function returns null and the policy fails open or closed unpredictably. Test policies with explicit session-context setup.user_id or tenant_id).RLS is excellent for multi-tenant SaaS where users only access their own data. It is overkill or even harmful in some cases.
RLS is a tool, not a religion. The question is always: "is the threat model 'a developer makes a mistake' or 'an attacker has direct database access'?" RLS protects against the first; encryption and network isolation protect against the second. Your choice of auth library influences how easy this is to wire up — some libraries (Supabase Auth, Clerk with custom JWT templates) make RLS context trivial, others require you to assemble it yourself.
The auth provider's job is to issue a signed token containing a user identifier. The database's job is to read that token and apply policies. The hand-off between them is where most real-world failures happen.
Supabase Auth + Supabase Postgres is the easiest pairing because the same vendor controls both ends. Clerk + Supabase Postgres works but requires a custom JWT template that maps Clerk's user ID into the Postgres auth.uid() claim. Our breakdown of Clerk vs Supabase Auth goes into the practical wiring.
If you are using a different database (Neon, RDS, Railway Postgres) you will need to inject the user context yourself. The pattern is roughly: in your API middleware, call SET LOCAL app.current_user = $userId at the start of each request, and reference current_setting('app.current_user') in your policies.
Row Level Security moves auth checks from your application layer into the database. For multi-tenant SaaS, this is one of the highest-leverage security choices you can make: a single missed WHERE clause stops being a data breach. The cost is a one-time setup (15–60 minutes for a typical schema) and a small ongoing tax of writing policies whenever you add tables. For most solo founders building B2B SaaS, the trade is overwhelmingly worth it — and Supabase makes the setup trivial enough that there is no excuse to skip it.
The stack, prompts, pricing, and mistakes to avoid — for solo founders building with AI.