Supabase table still open after adding an owner policy? A leftover using (true) policy wins
Short answer: Postgres combines permissive row level security policies with OR. If an old using (true) policy is still on the table, it lets every row through, and the owner policy you added next to it changes nothing. Drop the open policy (or turn your guard into a restrictive policy), then test signed out and as a second user. To see what your own live site gives away, run a free SniffPup scan (SniffPup is a free passive security scanner for vibe-coded apps). Only check sites you own.
Home › Guides › Leftover using (true) policy · Last updated · By Ember, SniffPup
The fast way: a free SniffPup scan runs 15 checks from the outside: leaked keys in your public JavaScript, exposed endpoints and security headers. No signup. Ember reads the live site only, so she can't see your database policies. Use the query below for that.
A 100 means no issues were found in our checks. It is never proof of security.
1. Why the owner policy didn't close the table
Policies are permissive by default. The Postgres CREATE POLICY docs say permissive policies that apply to a query are combined with OR, and restrictive ones with AND. So this pair leaves every row readable and writable by anyone with your public key:
-- added "for now" while building create policy "Allow all for now" on public.invoices for all using (true) with check (true); -- added later, looks right, does nothing while the one above exists create policy "Owners only" on public.invoices for all to authenticated using (auth.uid() = user_id) with check (auth.uid() = user_id);
A policy with no TO clause applies to PUBLIC, which includes anon. That is why a policy named for the service role but missing to service_role is open to everyone. The service role bypasses RLS anyway, so that policy was never needed. Supabase's RLS guide recommends always naming the role.
2. Find leftover open policies
Run this in the Supabase SQL editor. It reads the pg_policies view and lists permissive policies whose condition is literally true:
select schemaname, tablename, policyname, roles, cmd, qual, with_check from pg_policies where schemaname = 'public' and permissive = 'PERMISSIVE' and (qual = 'true' or with_check = 'true') order by tablename, cmd;
Read every row. A select policy with true on a table you mean to be public (a product list, say) is fine. Anything on user data, or any ALL, UPDATE or DELETE policy with true, is the leak. Also look at roles: {public} means anon is included.
3. Fix it
Usually the fix is to drop the open policy and keep the owner one:
drop policy "Allow all for now" on public.invoices;
If you want a hard guard that holds no matter what other policies get added later, add a restrictive policy. Restrictive policies are ANDed with the rest, so every row must also pass this check. You still need at least one permissive policy, or nothing is returned:
create policy "Must be owner" on public.invoices as restrictive for all to authenticated, anon using (auth.uid() = user_id) with check (auth.uid() = user_id);
Put the change in a migration so it isn't undone the next time migrations replay. If you build with Lovable or Bolt, ask it to drop the named policy rather than add another one: adding another permissive policy can never take access away from an open one.
4. Test it
Per the Postgres row security docs, superusers and roles with BYPASSRLS always bypass row security, and table owners normally do too. The Supabase SQL editor runs as postgres, which has bypassrls, so it shows every row and proves nothing. Test as the roles your app uses:
# signed out, with the public anon or publishable key: should return [] (or a permission error if anon has no grant) curl "https://YOUR-PROJECT.supabase.co/rest/v1/invoices?select=*" \ -H "apikey: YOUR_PUBLIC_KEY" -- in SQL, as anon inside a transaction you roll back begin; set local role anon; select count(*) from public.invoices; -- expect 0, or a 42501 error if anon has no grant rollback;
Then sign in as a second test user and try to read, update and delete the first user's rows through the API, not just the UI. Re-run the pg_policies query after every schema change. More patterns: Supabase RLS policy examples.
FAQ
Why can strangers still read my table after I added an owner policy?
Because permissive policies are combined with OR. If any one permissive policy for that command lets a row through, the row is visible. An old using (true) policy lets every row through, so the new owner policy changes nothing until the open one is dropped.
Does a policy without a TO clause apply to anon?
Yes. With no TO clause a policy applies to PUBLIC, which includes anon and authenticated. A policy named "Service can manage all rows" with using (true) and no TO service_role is open to everyone. Supabase's RLS docs recommend always naming the role.
Should I use restrictive policies instead?
Restrictive policies (AS RESTRICTIVE) are combined with AND, so every one must pass. They are useful as a guard, for example requiring auth.uid() = user_id on top of whatever else exists. You still need at least one permissive policy, or no rows are returned. The simplest fix is usually to drop the open policy.
Can a SniffPup scan find using (true) policies?
No. SniffPup reads your live site from the outside (leaked keys, exposed endpoints, headers). It can't see your database policies. Run the pg_policies query in this guide to find them.
Sources: PostgreSQL CREATE POLICY, Row Security Policies and pg_policies docs, and Supabase Row Level Security. Checked November 1, 2026.
A score of 100 means no issues were found in our checks. It is not proof that your app is secure.