Guide · October 2026
auth.uid() is null: the RLS policy that lets signed-out visitors read everything
This policy looks like it protects each user's rows. It actually hands every row to anyone who isn't signed in.
create policy "todos access" on public.todos for all using (auth.uid() is null or user_id = auth.uid());
Read it the way Postgres does. For a signed-in user, auth.uid() is their ID, so the first half is false and the second half limits them to their own rows. So far, so good.
For a signed-out request, auth.uid() is null. The first half is true, and true or anything is true, for every row. Because this is a for all policy, a signed-out visitor can also update and delete every row.
What the attack looks like
No hacking skills are needed. Your project URL and anon key are inside your app's JavaScript, and anyone can read them in the browser's developer tools. Then:
curl 'https://YOUR-PROJECT.supabase.co/rest/v1/todos?select=*' \ -H "apikey: YOUR_ANON_KEY"
That request carries no user, so auth.uid() is null and the response is the whole table. We reproduced this on a test Postgres database set up like Supabase: the signed-out request returned another user's private todo.
How it ends up in your app
Nobody writes this on purpose to leak data. It usually appears when something didn't work:
- The app showed a "permission denied" or an empty list before login was built, and the quickest fix that made the error go away was letting null users through.
- A landing page needed to show some data to visitors, and the policy was widened instead of adding a separate public rule.
- An AI coding tool was asked to "fix the RLS error" and loosened the policy rather than fixing the request.
The same hole has a few spellings. Watch for all of them:
auth.uid() is null or ... (select auth.uid()) is null or ... -- the "fast" style Supabase recommends auth.role() = 'anon' or ...
Why security scans don't catch it
Supabase's Security Advisor (the database linter, which Lovable's security scan builds on) checks for patterns such as RLS being turned off, or a policy that is literally true. This policy is neither. It has a real owner check in it, so it passes.
The Supabase team knows about the gap. A proposal to detect it was opened in June 2026, with a pull request adding the lint. As of late October 2026 both are still open, so the Advisor in your dashboard does not flag it yet.
Check your project in one query
Run this in the Supabase SQL Editor. It reads only your policy definitions:
select schemaname, tablename, policyname, cmd
from pg_policies
where concat_ws(' ', qual, with_check) ~* 'auth\.uid\(\)( as uid\))? is null'
order by 1, 2;
Any row it returns is a policy that lets signed-out visitors in. To check this together with the other common mistakes (users who can make themselves admin, private storage buckets anyone can list, "any signed-in user" policies), use RLS Check. It's free and runs in your browser.
How to fix it
Remove the is null branch so only the owner gets in:
drop policy "todos access" on public.todos; create policy "owner can manage own todos" on public.todos for all to authenticated using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id);
If signed-out visitors really do need to see some rows, say so explicitly with a separate read-only policy, for example on a column like is_public:
create policy "anyone can read public todos" on public.todos for select to anon using (is_public);
Then run the curl request above again. Without a signed-in user it should return [] (or only the public rows), and the owner should still see their own data in the app.
Using Lovable, Bolt or Cursor? Paste this:
My Supabase policy "todos access" on public.todos uses "auth.uid() is null or user_id = auth.uid()", which lets signed-out visitors read, change and delete every row. Write a migration that replaces it with a policy for the authenticated role using (select auth.uid()) = user_id for both using and with check. Show me the SQL before applying it, then confirm a signed-out request returns no rows and the owner still sees theirs.