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:

  1. RLS is on and there's no select policy for the caller's role. With RLS enabled and no policies, nobody except owners and service_role sees anything.
  2. The request runs as anon, not authenticated. 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 written to authenticated doesn't apply to anon.
  3. The policy doesn't match the data. For example user_id holds a different value than auth.uid(), or rows were imported with user_id empty.
  4. The filter matches nothing: wrong case in .eq('status', 'Confirmed'), or comparing a timestamp to a date string in another time zone.
  5. You're talking to a different project than the one you're looking at in Studio.

How to fix it

  1. 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';
    
  2. 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.

  3. 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);
    
  4. 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 sure createServerClient gets the request cookies through getAll/setAll.

  5. update() and delete() can also return an empty array: the rows exist, but the caller has no select policy to read them back, or no update/delete policy 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.

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us