Supabase Returns Empty Data With No Error? It's Usually Row Level Security
Quick answer: Supabase query returns [] with no error, or insert fails with "new row violates row-level security policy"? Why RLS does that, and the SQL policies to fix it.
The error
Your table clearly has rows in the Supabase dashboard, but your app gets nothing back:
{ data: [], error: null }
No red errors in the console, just a blank list or dashboard. The write-side version of the same problem does throw an error:
new row violates row-level security policy for table "projects"
(Postgres error code 42501.)
Why it happens
When Row Level Security (RLS) is enabled on a table, Postgres denies everything that no policy explicitly allows. It handles reads and writes differently:
- SELECT with no matching policy doesn't error. Rows you aren't allowed to see are simply filtered out, so you get
[]. - UPDATE and DELETE with no matching policy also don't error. They just affect zero rows.
- INSERT that no policy allows fails with
new row violates row-level security policy.
The Supabase Table Editor shows all rows because it doesn't run as your app's user, which is why the data "is there" but your app can't see it.
Common triggers in AI-built apps:
- RLS was turned on (often by the AI tool or a security scan) but only an INSERT policy was written, with no SELECT policy.
- The policy is
to authenticatedbut the user isn't signed in yet when the query runs, soauth.uid()is null. - The row's
user_idis empty or doesn't match the signed-in user. insert(...).select()needs a SELECT policy too, because returning the new row is a read.
Fixes
1. Confirm it's RLS
- In the Supabase dashboard, open the table's policies page to see whether RLS is enabled and which policies exist.
- Open the Security Advisor. It flags tables that have RLS enabled but no policies.
- In the SQL editor, run the query as a different role using the role selector, instead of the default admin role. If it returns rows as admin and none as
anonorauthenticated, it's a policy problem.
2. Add a SELECT policy that matches who should see the rows
Private rows owned by each user:
create policy "Users read own projects" on public.projects
for select to authenticated
using ((select auth.uid()) = user_id);
Public data anyone can read (for example, a product list):
create policy "Anyone can read products" on public.products
for select to anon, authenticated
using (true);
3. Fix "new row violates row-level security policy" on insert
Add an INSERT policy, and make sure user_id is set to the signed-in user:
create policy "Users insert own projects" on public.projects
for insert to authenticated
with check ((select auth.uid()) = user_id);
alter table public.projects alter column user_id set default auth.uid();
For the full set of select, insert, update and delete policies, see our Supabase RLS errors guide.
4. Make sure the user is signed in before you query
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
// show the login screen instead of querying
}
const { data, error } = await supabase.from('projects').select('*');
If the page loads data before the session is restored, the first query runs as anon and returns []. Wait for the session (or listen with onAuthStateChange) before fetching.
5. Rule out the non-RLS causes
- Your app points at a different Supabase project than the one you're looking at (check the URL in your env vars).
- A filter like
.eq('user_id', ...)uses the wrong value. - The table is in a schema other than
publicand you didn't specify it.
6. Don't "fix" it by disabling RLS or using the service role key in the browser
Turning RLS off, or putting the service_role / sb_secret_... key in frontend code, makes the data readable (and often writable) by anyone who has your public anon key, which is visible to every visitor. Keep RLS on and write the policy you actually need.
Prompt to paste into Lovable, Bolt or Cursor
My Supabase query on the "projects" table returns an empty array with no error, but the table has rows. RLS is enabled. Show me the existing policies, then write the missing SELECT policy so authenticated users can read rows where user_id = auth.uid(). Make sure inserts set user_id to auth.uid(). Don't disable RLS and don't use the service_role key in client code.
FAQ
Why doesn't Supabase just show a permission error on select? That's how Postgres RLS works: it filters rows rather than rejecting the query. An empty result is the policy saying "you can't see any of these."
My update "succeeds" but nothing changes. Same problem? Yes. With no matching UPDATE policy (or a USING clause that doesn't match), the update affects zero rows without an error. Add .select() after the update to see what actually changed.
Is the anon key safe if RLS is on? The anon or publishable key is meant to be public. It's only safe because RLS policies limit what it can do, so every table the browser touches needs RLS and the right policies.
Related guides
- Supabase RLS errors in Lovable and Bolt apps
- Works in preview, blank page in production
- Stripe checkout succeeds but the app never records the payment
Still stuck? Paste your exact error into the free Error Fixer to get a plain-English cause, the fix, and a prompt to paste back into your AI tool.