select returns an empty array but the table has data
Why supabase.from().select() returns [] with no error when rows exist, usually Row Level Security or a missing session, and how to test the query as the right role.
An empty array with no error almost always means Row Level Security filtered every row out. RLS doesn't raise an error on reads. It quietly returns only the rows the caller may see, and that can be none.
What you see
const { data, error } = await supabase.from('bookings').select('*')
console.log(data, error) // [] null
The Table Editor in Studio shows plenty of rows. Studio connects as the postgres role, which owns the tables you created, and owners aren't subject to RLS, so it sees everything. Your app doesn't.
Why it happens
Common causes, roughly in order of frequency:
- RLS is on and there's no
selectpolicy for the caller's role. With RLS enabled and no policies, nobody except owners andservice_rolesees anything. - The request runs as
anon, notauthenticated. The user isn't signed in, the session hasn't loaded yet, or a server-side client was created without the user's cookies. A policy writtento authenticateddoesn't apply toanon. - The policy doesn't match the data. For example
user_idholds a different value thanauth.uid(), or rows were imported withuser_idempty. - The filter matches nothing: wrong case in
.eq('status', 'Confirmed'), or comparing a timestamp to a date string in another time zone. - You're talking to a different project than the one you're looking at in Studio.
How to fix it
Check RLS and the policies on the table:
select relname, relrowsecurity from pg_class where relname = 'bookings'; select policyname, cmd, roles, qual from pg_policies where tablename = 'bookings';Run the query as the role your app uses. Replace the UUID with a real user id from
auth.users:begin; set local role authenticated; select set_config('request.jwt.claims', '{"sub":"8f1c2c4e-0000-4000-8000-000000000001","role":"authenticated"}', true); select * from public.bookings limit 5; rollback;If this returns nothing, the problem is the policy, not your client code.
Add or fix the policy:
create policy "Guests read their own bookings" on public.bookings for select to authenticated using ((select auth.uid()) = guest_id);Confirm the client has a session before querying:
const { data: { user } } = await supabase.auth.getUser() if (!user) console.warn('Not signed in: this query runs as anon')On the server with
@supabase/ssr, make surecreateServerClientgets the request cookies throughgetAll/setAll.update()anddelete()can also return an empty array: the rows exist, but the caller has noselectpolicy to read them back, or noupdate/deletepolicy that matches them.
On SupaHost
- Check the URL. Every project has its own host, such as
https://app-one.acme.supahost.space, and its own keys. Branches are separate projects with their own URL and keys. A branch created without "Copy data, users and files" starts with empty tables and no users. - A request is limited to 1000 rows. If you get exactly 1000 rows and expected more, that's the cap, not RLS. Use
.range()to page.