What Is Row-Level Security (RLS)?
Row-Level Security is a Postgres feature that restricts which database rows a user can read or write — enforced at the database level, not in application code.
Row-Level Security (RLS) is a Postgres feature that controls which rows of a table each user can read or write, enforced inside the database itself rather than in application code. Once a policy is attached to a table, every query is automatically filtered against it — so a bug in your API or a forgotten WHERE clause cannot expose data the user was never allowed to see.
How RLS works
RLS attaches access-control policies directly to a table. After you turn it on, the table is locked down by default — no rows are visible until a policy grants access. Each policy is a SQL boolean expression evaluated per row, per query:
USINGdecides which rows a query can read (and which it may update or delete).WITH CHECKdecides which rows a query is allowed to write (insert or update).
The key idea: the rule lives next to the data. Whether a row is requested by your web app, a background job, or an auto-generated REST endpoint, the same policy runs — there is no second place to forget the check.
ALTER TABLE bookings ENABLE ROW LEVEL SECURITY;
CREATE POLICY "tenant_isolation" ON bookings
FOR ALL
USING (tenant_id = current_setting('app.tenant_id')::uuid);
In Supabase the same idea usually keys off the authenticated user, e.g. USING (user_id = auth.uid()), since Supabase exposes the logged-in user's id to Postgres on every request.
Why database-level enforcement matters
Application-level guards are fragile. A single missing where user_id = $1 in one query path exposes every row in that table to every user — and that one query is easy to miss in a code review. RLS removes the question entirely: the database applies the rule no matter what the application sends. That makes it the key defense wherever untrusted clients talk to Postgres directly, such as Supabase apps where the browser calls the database through PostgREST.
The multi-tenant SaaS pattern
The most common production use of RLS is tenant isolation in a multi-tenant SaaS. Every table carries a tenant_id (or user_id) column, and a policy guarantees a query only returns rows belonging to the authenticated tenant. One running application, one database, but each customer's data is walled off from the others — the pattern behind any product where accounts must stay private, such as the React + Supabase build behind Pizzeria Bestek.
When to use RLS — and when not to
| Use RLS when | RLS is the wrong tool when |
|---|---|
| Clients query Postgres directly (Supabase / PostgREST) | All access goes through a trusted backend that already filters every query |
| Multi-tenant data must be isolated per account | Rules depend on logic Postgres cannot see (external service calls, complex app state) |
| You want one enforcement point you cannot forget | Per-row policies on hot tables would add filtering you can't index |
RLS is not a replacement for authentication — it assumes you already know who the user is. It answers exactly one question: which rows is this identity allowed to touch.
Common mistakes
- Enabling RLS but writing no policy. The table goes empty for everyone (default-deny). Add policies, or you'll think your app broke.
- Forgetting
WITH CHECKon writes. A read policy alone can still let a user insert or update rows attributed to another user. Cover both directions. - Not indexing the filter column. RLS adds negligible overhead when
tenant_id/user_idis indexed. Without the index, every query becomes a full table scan — a silent performance killer at scale. - Trusting the
service_rolekey in the browser. Service-role connections bypass RLS by design. That key belongs only on a trusted server, never shipped to a client.
Key takeaways
- RLS enforces per-row access in Postgres, so application bugs can't leak data.
- A policy has two halves:
USING(reads) andWITH CHECK(writes) — set both. - It is the standard isolation layer for multi-tenant SaaS and any Supabase app where clients query the database directly.
- Always index the column your policies filter on, and keep the service-role key server-side.
For help auditing or implementing row-level isolation on a Postgres or Supabase project, contact for a quote.