Supabase returns an empty array? How to fix RLS and query problems
Your table clearly has rows in the Supabase dashboard, but your app's select() comes back as [] with error: null. That is almost never a network bug. Postgres is quietly filtering the rows out, usually because of row-level security (RLS). Here is how to find which rule is hiding your data.
- Understand why there's no error
- Check whether RLS is on and which policies exist
- Add the right SELECT policy
- Make sure the user is actually logged in when the query runs
- Check the user_id column really matches
- Rule out filters, schema and project mix-ups
- Test the policy as the user
- Never use the service_role key in the browser
1. Understand why there's no error
When RLS is enabled on a table, every query is silently filtered by its policies. Rows the current user isn't allowed to see are simply left out. So an empty array with no error means "no rows passed the policy", not "the query failed". The dashboard Table Editor shows all rows because it runs as an admin role that bypasses RLS, which is why it looks fine there.
2. Check whether RLS is on and which policies exist
In the Supabase dashboard open Authentication → Policies (or the table's RLS settings). If RLS is enabled and there is no SELECT policy, every read returns []. Or run in the SQL editor:
select tablename, policyname, cmd, roles, qual
from pg_policies where schemaname = 'public';
Look for a policy whose cmd is SELECT or ALL on your table.
3. Add the right SELECT policy
Pick the rule that matches your app. Rows owned by the logged-in user:
create policy "read own rows" on public.todos
for select to authenticated
using ((select auth.uid()) = user_id);
Public data anyone may read (product lists, blog posts):
create policy "public read" on public.products
for select to anon, authenticated
using (true);
Do not fix this by turning RLS off. With RLS off, anyone holding your public anon key can read and change the whole table.
4. Make sure the user is actually logged in when the query runs
A policy using auth.uid() returns nothing if the request is sent as anon. Common causes in AI-generated code:
- The query runs on page load before the session is restored. Wait for
supabase.auth.getSession()or theonAuthStateChangeevent before fetching. - In Next.js server components or API routes, a plain
createClient()has no cookies, so it's anonymous. Use the@supabase/ssrserver client that reads the auth cookies. - Check the Network tab: the request's
Authorizationheader should carry the user's JWT, not just the anon key.
5. Check the user_id column really matches
If rows were inserted without user_id, or with a different id (e.g. from a seed script or an old account), the ownership policy filters them out. Run select id, user_id from todos limit 10; in the SQL editor and compare with the logged-in user's id. Setting the column default to auth.uid() prevents this for future inserts.
6. Rule out filters, schema and project mix-ups
- Wrong project: the URL and key in your
.envor hosting dashboard point at a different Supabase project (often a dev copy) than the one you're looking at. - Filters:
.eq('status', 'Active')is case-sensitive; an undefined variable in.eq('id', id)matches nothing. - Schema: tables outside
publicneedsupabase.schema('name')and must be exposed in the API settings. - Joins: an embedded table like
select('*, profiles(*)')also needs its own SELECT policy, or the related part comes back null.
7. Test the policy as the user
In the SQL editor you can impersonate a user to see exactly what RLS lets through:
begin;
set local role authenticated;
set local request.jwt.claims = '{"sub":"USER-UUID-HERE","role":"authenticated"}';
select * from public.todos;
rollback;
Run it all as one query. If this returns rows but the app doesn't, the problem is the session in your app (step 4), not the policy.
8. Never use the service_role key in the browser
The service_role key bypasses RLS, so it "fixes" empty arrays instantly, and AI tools sometimes suggest it. In frontend code it gives every visitor full database access. Keep it only in server code or Edge Function secrets, and rotate it right away if it was ever shipped to the browser.
Still stuck? We'll fix it at a flat price.
VibeFix fixes broken Lovable, Bolt, Cursor, Replit and v0 apps. Request a free check first, and we'll tell you what's wrong and which plan fits before you pay. If we can't fix it, you get a full refund.
Already know you need help? Quick Fix, $49 (one bug, 24h) · Standard Rescue, $129 · Deploy-Ready, from $199