Supabase Advisor · INFO

RLS Enabled No Policy — why your queries return an empty array

Luca Urti

RLS Enabled No Policy

Why does my Supabase query return an empty array instead of an error?

Row Level Security is enabled on the table and no policy permits the current role to do anything, so PostgreSQL filters every row out. A SELECT that matches nothing is not an error — it is an empty result — so the API returns [] with a 200 status and your client shows an empty list rather than a failure. Writes do raise an error, which is why the insert breaks loudly and the read breaks silently. The state is secure and non-functional, and it is the normal consequence of running enable row level security without adding policies in the same migration.

Telling it apart from a genuinely empty table

Query the table in the SQL editor of the dashboard. That session runs as a superuser and bypasses RLS, so it shows the rows that are really there. If the editor shows rows and your app shows none, the policies are the reason — not caching, not the client library, not a filter.

Write the policy for the role you actually use

A policy with no to clause applies to every role, including anon. If the data belongs to signed-in users, say to authenticated — otherwise a policy written for logged-in users is also evaluated for anonymous ones, where auth.uid() is null and the comparison is merely false rather than an error. It works, until you write one whose expression is accidentally true for a null uid.

Each command needs its own coverage: for select and for insert are separate, and for all needs both using and with check to cover reads and writes together.

The fix

the standard owner-scoped policy

create policy "read own rows"
  on public.documents
  for select
  to authenticated
  using (auth.uid() = user_id);

create policy "write own rows"
  on public.documents
  for insert
  to authenticated
  with check (auth.uid() = user_id);

-- Verify as the app sees it, not as a superuser:
--   select policyname, cmd, roles, qual, with_check
--   from pg_policies where tablename = 'documents';

The fix that works and costs you the database

Because the symptom is “my app shows nothing”, the fastest fix is the broadest one — using (true), or moving the query to a server route that uses the service_role key. Both make the list appear. The first makes the table public; the second removes RLS from the equation for that route, so whatever filtering you thought the database was doing is now entirely your route handler's job, and any missing check there is an IDOR.

Whether the trap is already in your repo is a question you can answer

Sentris reads the SQL and the client code, so it reports the shortcut above where it was actually taken — a service_role key in a browser bundle, a policy that is using (true), a table with RLS switched off. A scan needs no account and no card. How often it is wrong is measured and published.

Scan my app