Skip to content
Glossary

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:

  • USING decides which rows a query can read (and which it may update or delete).
  • WITH CHECK decides 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 whenRLS 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 accountRules depend on logic Postgres cannot see (external service calls, complex app state)
You want one enforcement point you cannot forgetPer-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 CHECK on 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_id is indexed. Without the index, every query becomes a full table scan — a silent performance killer at scale.
  • Trusting the service_role key 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) and WITH 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.

Continue reading

Want this built?