Supabase select returns empty array
Guide · October 18, 2026
You run supabase.from("posts").select("*"). You get { data: [], error: null }. The rows are in the table. You can see them in the SQL editor. The same query works there. Your app shows nothing.
That is Row Level Security working correctly. RLS enabled with no matching SELECT policy silently filters every row out. Zero rows, no error. The SQL editor bypasses RLS, so it sees everything.
Source: Row Level Security (how policies filter rows, auth.uid() is null with no JWT). Related: Supabase troubleshooting: empty array FAQ.
What it looks like
const { data, error } = await supabase
.from("posts")
.select("*");
console.log(data); // []
console.log(error); // null
Typical companions:
- The same query works in the Supabase SQL editor.
- One user sees rows; another sees nothing.
- It worked until someone "turned on RLS" or regenerated policies in Lovable.
SELECT * FROM posts;in the SQL editor returns the rows you expect.
If you instead see 42501 permission denied for table, stop. That is a missing GRANT, not this bug. Use the Oct 30 grants guide.
Why the SQL editor sees the rows
The Supabase SQL editor runs as the postgres role. That role owns the tables and bypasses Row Level Security. Your app uses the Data API (PostgREST), which runs as anon or authenticated. Those roles must pass every RLS policy to see or touch a row.
RLS is row-level filtering, not row-level errors. If no policy allows a row, it is invisible. If no policy exists at all, the table is invisible. The client gets an empty array and error: null because Postgres returned zero rows, not an error.
Common causes
1. RLS on, but no SELECT policy
Enable RLS without a policy and every query returns zero rows. Supabase documents this: "Once RLS is enabled, no data is accessible through the API when using a publishable key, until you create policies."
-- missing this:
create policy "anyone can read posts"
on public.posts
for select
using (true);
Without a SELECT policy, the Data API cannot return any rows. It does not raise an error. It just returns nothing.
2. Policy uses the wrong role
A policy with to authenticated is invisible to anon. If the user is not signed in, or the session has not loaded yet, the client runs as anon. The policy never fires.
create policy "signed-in users can read posts"
on public.posts
for select
to authenticated -- anon does not match
using (true);
If you want public read access, use to anon, authenticated or omit the to clause (defaults to public, which includes both).
3. auth.uid() is null
Your policy says using (auth.uid() = user_id). That works only when the user is logged in and the session is attached to the request. If auth.uid() is null:
null = user_idis false for every row.- Every row is filtered out.
- The app still works (it has the anon key), so
erroris null.
Common ways auth.uid() ends up null:
- The user is not signed in.
- The session has not loaded yet (common on first render before
onAuthStateChangefires). - Server-side code creates a new Supabase client with only the anon key, without passing the user's access token.
// check the session before querying
const { data: { session } } = await supabase.auth.getSession();
console.log("uid:", session?.user?.id); // null = no session
4. user_id does not match
Rows exist, but they belong to a different user. The policy user_id = auth.uid() correctly hides them. Check whether the logged-in user actually owns the rows:
-- in the SQL editor
select id, user_id from posts;
If user_id is null or belongs to another account, the policy is doing its job. The fix is inserting rows with the right owner, or changing the policy if the data should be shared.
5. Column name mismatch
Lovable sometimes names the owner column id, user_id, or owner_id in different files. A policy that checks auth.uid() = id (the row's primary key) instead of auth.uid() = user_id (the owner column) will never match.
How to confirm
1. Check the policies in the dashboard
Go to Table Editor → the table → RLS. Or run:
select polname, cmd, roles, qual
from pg_policies
where tablename = 'posts';
Look for a policy with cmd = SELECT. If there is none, add one.
2. Test with the user's session
Log the user's id right before the query:
const { data: { user } } = await supabase.auth.getUser();
console.log("querying as:", user?.id);
const { data, error } = await supabase.from("posts").select("*");
console.log("rows:", data?.length);
If user?.id is undefined, you have no session. Fix auth first.
3. Temporarily add a permissive policy
For debugging only, add a wide-open SELECT policy and see if rows appear:
create policy "debug_allow_all"
on public.posts
for select
using (true);
If rows appear, the table has data and the problem is your policies. Remove this policy before going to production.
Step-by-step fix
1. Confirm RLS is on
alter table public.posts enable row level security;
2. Add a SELECT policy
For public data anyone can read:
create policy "anyone can read posts"
on public.posts
for select
using (true);
For private data only the owner can read:
create policy "owners can read their posts"
on public.posts
for select
to authenticated
using ( (select auth.uid()) = user_id );
Wrap auth.uid() in (select auth.uid()) so Postgres caches it per statement.
3. Grant access to the role
If you get 42501 instead of an empty array, the role has no GRANT. Add it:
grant select on table public.posts to anon, authenticated;
4. Wait for the session
If auth.uid() is null, your query runs before the session is ready. Wait for onAuthStateChange or check getSession() before fetching:
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
// not logged in or session not ready
return;
}
const { data } = await supabase.from("posts").select("*");
Quick checklist
- RLS on?
alter table ... enable row level security - SELECT policy exists? Check
pg_policiesforcmd = SELECT. - Policy role matches?
to authenticatedhides fromanon. auth.uid()not null? Log it before the query.- Owner column matches the policy?
user_idvsidvsowner_id. - Rows exist for this user? Check
user_idin the SQL editor. - 42501 instead of empty? That is a GRANT issue, not a policy issue.
Questions
Why does supabase.from().select() return an empty array when rows exist?
Row Level Security is filtering every row out. RLS enabled with no SELECT policy, or a policy that does not match your session, returns zero rows and no error.
Why does the same query work in the Supabase SQL editor?
The SQL editor runs as the postgres role, which owns the tables and bypasses RLS. Your app uses anon or authenticated, which must pass policies.
Can a missing session cause an empty array?
Yes. auth.uid() is null without a session. A policy like user_id = auth.uid() matches nothing because null = user_id is always false.
What is the difference between an empty array and a 42501 error?
42501 means the role has no GRANT on the table, so RLS never runs. Empty array means the role has SELECT privilege but policies filtered every row. Fix 42501 with GRANT; fix empty with SELECT policies.
Should I disable RLS to fix the empty array?
No. Disabling RLS exposes all rows to anyone with the anon key. Add a SELECT policy instead. If you need public reads, use using (true). For private data, use using (auth.uid() = user_id).
Seeing rows update but not actually change? Silent updates guide. New table returning 42501? Oct 30 grants. Not sure if your database is public? Run the 5-minute check.
Still getting an empty array after checking policies? That is usually an app-side auth bug or a policy the AI wrote wrong. We reproduce it in your repo and fix it. You pay only if it works.
Tell us what's broken