RLS Is Enabled and Your Data Is Still Readable
RedHub AI Editorialupdated October 4, 20267 min read

Jump to a section8
using ( true ) is a complete Postgres policy. Attach it to a table's select operation and every check passes: row level security is on, a policy exists, and every row goes to anyone who asks. That is what RLS enabled but not secure looks like in its plainest form. The harder cases are conditions that look like a check, vary from request to request, and still never compare the row to the person asking for it.
TL;DR: Row level security enforces exactly what a policy says. With the switch on and no policy, Postgres denies the operation, so a forgotten policy fails safe. A table stays readable when a policy admits everyone: using ( true ), a policy that lets any signed-in user through, or one that trusts data the user can edit. Supabase's Security Advisor flags the always-true kind. The rest takes a read of each condition, per operation. The Supabase RLS Read-Off ($69) scores a policy that admits everyone as OPEN TABLE, whatever the badge says.
Why the switch can't catch it
Postgres isn't ignoring anything here, and Supabase isn't failing to enforce. Row level security runs the condition you wrote, row by row, and returns the rows where it comes out true. A condition that is true for every row returns every row. Nothing about the switch can object, because the switch only asks whether a policy applies, not what the policy decides.
The opposite case gets misread too. Switching RLS on and writing no policy for an operation does not leave that operation open. Postgres's documentation describes a default-deny policy for exactly that state, so a delete with no delete policy matches no rows. The exposure to look for is not a missing policy. It is a policy that checks nothing.
Five conditions that admit everyone
Each of these is valid SQL, and each one shows up in the dashboard's policy list looking as deliberate as a correct policy. The right-hand column is whether Supabase's Security Advisor has a documented check for it.
| Condition | Who gets through | Security Advisor check |
|---|---|---|
using ( true ) | Every role the policy names, on every row | Yes: "permissive RLS policy" |
to authenticated using ( true ) | Anyone signed in, including anonymous sign-ins, which Supabase runs as the authenticated role | Yes: "permissive RLS policy" |
using ( (select auth.uid()) is not null ) | Anyone signed in, on every row | None documented |
using ( user_id is not null ) | Everyone, on every row that has an owner | None documented |
A role read from auth.jwt() -> 'user_metadata' | Anyone who edits their own metadata to claim the role | Yes: "RLS references user metadata" |
The last row needs its own warning. Supabase's RLS guide says raw_user_meta_data can be updated by the signed-in user, so it is not a place for authorization data. A policy that grants admin rights when the metadata says "admin" lets any user promote themselves. Supabase points to raw_app_meta_data, which the user can't change, for that job.
The middle two rows are the ones no check catches by pattern, because neither is always true. A signed-out request fails auth.uid() is not null, so the condition varies. It still asks only whether someone is signed in, never whether this row is theirs. That is the route-level mistake in signed in is not authorization, moved into the database.
Two kinds of public, and only one is a choice
Not every open table is a problem. Supabase's own guide uses a policy called "Anyone can read announcements" with using ( true ), granted select only, for a feed nobody writes to from the browser. Published prices, public listings and blog posts belong in the same group. The Advisor will flag them anyway, and Supabase notes that some findings are intentional.
The line is between reading and writing. Nobody means to let any visitor insert, update or delete rows in a table they don't own. And the read policy decides how far an open write can go. Postgres's documentation says an update or delete that filters on a column must also pass the table's select policy. On an owner-only table, a delete policy of true reaches the caller's own rows. On a table everyone can read, it reaches every row, which makes a public-read table the worst place to leave a write policy loose.
true, open the insert, update and delete policies and confirm each one names to authenticated and compares an owner column to (select auth.uid()), or that the role holds no grant for that operation at all.What a real check compares
A policy that does its job compares something on the row with something about the request. In Supabase that is usually the row's owner column against auth.uid(), the id of the signed-in user. Supabase's guide writes it as (select auth.uid()) = user_id, wrapping the call in a select so Postgres evaluates it once per statement instead of once per row.
The comparison also fails closed for strangers. Supabase's docs note that auth.uid() returns null when no one is signed in, and null = user_id is never true. The same comparison in application code is what our post on the record ownership check walks through, and a policy written this way still stops a request when a route forgets that check.
Shared records are where this gets hard, and there is no one-line answer. A document a team shares can't be scoped to one user_id. It needs a membership lookup, and Supabase's guide warns that two tables whose policies read each other fail with an infinite-recursion error. Its fix is a security definer function in an unexposed schema, with search_path pinned. That is a real design decision, and a read-off can tell you only that the comparison is present, not that the membership model behind it is right.
Check these before the policies
Everything above assumes the request is being checked against a policy at all. Three things can skip that. A table in public with RLS off is open to any role with a grant, which on many projects means anyone with your publishable key. A view created by postgres bypasses RLS by default unless it uses security_invoker = true. And Supabase's secret key, or the legacy service_role key, ignores every policy. If that key can be reached from the browser, it outranks everything in this post. The Anon Key Is Public. The Service Role Key Ends the Argument. covers it, and Supabase RLS Policies, One Operation at a Time covers how each clause works.
A policy can exist and still do nothing
The Supabase RLS Read-Off asks, for every table and every operation, whether a policy exists and whether it compares the row to the signed-in user. A policy that admits everyone still reads OPEN TABLE. One .xlsx, filled in from your own dashboard. It connects to nothing and is not a scanner.
Get the Supabase RLS Read-Off — $69Pairs well with
The Browser Line Read-Off ($59) reads the checks your app makes outside the database, routing each one ENFORCED / TRUSTS THE BROWSER / FINE IN THE BROWSER. The Data Model Sanity Bench ($69) reads whether each table has the owner column a policy needs to compare against. A records table with none reads NOTHING TO ENFORCE AGAINST. The Vibe-Coded App Pre-Launch Security Gate ($79) marks six launch controls, including whether access control was tested, and bands the app LAUNCH-READY AS DESCRIBED / HARDEN FIRST / DO NOT SHIP.
More in this guide
Frequently asked questions
How can RLS be enabled and a table still be public?
"Enabled" only means Postgres checks a policy before allowing an operation. If the policy's condition doesn't compare the row to the person asking, such as one that evaluates to true, the switch is satisfied and everyone gets through.
If I enable RLS and don't write any policies, is the table open?
No. Postgres applies a default-deny policy when RLS is on and no policy applies to an operation. The exposure to look for is an existing policy that is too permissive, not a missing one.
Is a policy that allows any signed-in user safe?
Only for data every user is meant to see. Anyone who can create an account is signed in, and Supabase runs anonymous sign-ins as the authenticated role too. For private rows, the policy has to compare an owner column to auth.uid().
Can I use user_metadata in an RLS policy?
Not for authorization. Supabase's documentation says users can update their own raw_user_meta_data, so a role stored there can be claimed by anyone. Supabase points to raw_app_meta_data, which users can't change, and its Security Advisor flags policies that reference user metadata.
Is a table being publicly readable always a problem?
No. Public read access is a common, legitimate choice for prices or published content. It becomes a problem when insert, update or delete is as open, and on a table everyone can read, an open delete reaches every row.


The gate this post refers to, drawn from the tool’s own logic. See the tool.