Skip to main content
Security9 min readPublished September 19, 2026Updated September 22, 2026

Row Level Security explained

Row Level Security (RLS) moves permission decisions into the database itself. Instead of trusting your app to filter results, every query is checked row by row against policies. If no policy allows a row, the row does not come back — no error, just absence.

What is Row Level Security?

A rule attached to a table that says which rows a given user may select, insert, update or delete. The database evaluates it on every query, including ones made directly from a browser. That is what makes it safe to let a browser talk to a database at all.

A policy that lets people read only their own rows
alter table public.notes enable row level security;

create policy "Read your own notes"
  on public.notes for select
  to authenticated
  using (auth.uid() = user_id);

Why is my table suddenly empty?

Because enabling RLS with no policies denies everything. That is deliberate: the safe default is nothing rather than everything. A table with RLS enabled and zero policies returns an empty result to every query, forever, with no error text to explain it.

  • RLS enabled, no policy → nothing is readable.
  • Policy exists but for the wrong role → nothing is readable by the role your app uses.
  • Policy compares the wrong column → readable only in the case that happens to match.
  • Grants missing → a permission error rather than an empty list.

What is the difference between USING and WITH CHECK?

ClauseApplies toQuestion it answers
USINGselect, update, deleteMay this user see or touch this existing row?
WITH CHECKinsert, updateIs this user allowed to produce this new row?

An insert policy with a missing or permissive WITH CHECK is how people accidentally allow a user to create rows owned by someone else — the classic version being adding yourself to a team you were never invited to.

Why do policies pass for me and fail for everyone else?

Because you are usually the row owner, and often the account that created the data. Every condition based on ownership is trivially true for you. Test as a second account — a real second signup — before believing a policy works.

  1. 1Sign up a second test account.
  2. 2Create a record as account A.
  3. 3Sign in as account B and try to read, edit and delete A's record.
  4. 4Confirm B sees nothing and cannot write. If B can, the policy is wrong.
  5. 5Repeat signed out entirely, which is how a stranger will arrive.

What about roles that bypass RLS?

Service-role or admin credentials ignore RLS completely, by design, so background jobs can work. They belong on a server and nowhere else. Generated code sometimes reaches for admin access to make a stubborn query work — that silences the symptom and removes the protection at the same time.

If a fix involves switching from the normal client to the admin client, treat it as a red flag rather than a solution.

Frequently asked questions

Is RLS enough on its own?
For data access, it is the strongest layer you have, because it cannot be bypassed by editing front-end code. You still need validation and rate limiting elsewhere.
Does RLS slow queries down?
Slightly, and much more if a policy calls an expensive function per row. Index the columns your policies compare.
Can I test policies without a second account?
You can run a query as a specific role in your database tooling, but a real second signup catches mistakes that role-switching hides.

Practice this in MessyDev

Reading it once helps. Doing it once sticks. These are the hands-on parts of MessyDev that cover the same ground.

Keep going