Research-based overview. This article synthesizes public documentation, pricing pages, and user reports. How we research.

One-sentence definition
Row Level Security (RLS) is a Postgres feature that lets you attach security policies to a table so that, for any query, the database itself filters which rows the calling user can read or write — making access control a property of the data, not the application.

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.

Why RLS matters more than app-layer auth

Imagine a junior developer (or you, at 1 a.m., or an AI coding agent) writes this:

-- Forgot the WHERE clause. Returns everyone's invoices. SELECT id, amount, customer_email FROM invoices;

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:

-- Postgres rewrites this to: SELECT id, amount, customer_email FROM invoices WHERE user_id = current_setting('request.jwt.claims', true)::json->>'sub';

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.

How RLS works in Postgres

The mechanism has four parts. They have to all be in place; missing any one of them breaks the security model.

  1. Enable RLS on the table. By default, Postgres tables have RLS off. You explicitly turn it on with ALTER TABLE invoices ENABLE ROW LEVEL SECURITY; — until you do, no policies apply.
  2. Set a session variable that identifies the current user. Postgres has no native concept of "the logged-in user from your web app." You inject this context per-request, typically via a JWT claim or a Postgres role.
  3. Write policies that reference the session variable. A policy is a SQL expression that returns true or false for each row. Postgres evaluates it for every row touched by every query.
  4. Use a database role that the policies actually apply to. The 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:

ALTER TABLE invoices ENABLE ROW LEVEL SECURITY; CREATE POLICY "users can read their own invoices" ON invoices FOR SELECT USING (user_id = auth.uid()); CREATE POLICY "users can insert their own invoices" ON invoices FOR INSERT WITH CHECK (user_id = auth.uid());

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.

RLS in Supabase vs other backends

BackendRLS supportNotes
SupabaseFirst-class, opinionatedRLS is the recommended pattern. Auth context (auth.uid(), auth.jwt()) is auto-injected. Dashboard has policy templates.
NeonNative Postgres RLS works as standardYou manage session context yourself. No built-in auth helpers like Supabase's auth.uid().
Firebase FirestoreDifferent model: security rulesNot RLS. JavaScript-like rules evaluated per request. Conceptually similar but separate from SQL.
PlanetScaleNo RLS (MySQL/Vitess)MySQL has no row-level security feature. Enforce in application layer.
Self-hosted PostgresNative RLS availableSame 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.

Common RLS mistakes

When you don't need RLS

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.

How RLS pairs with your auth provider

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.

The takeaway

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.

Get one SaaS build breakdown every week

The stack, prompts, pricing, and mistakes to avoid — for solo founders building with AI.