Supabase question
Which of my Supabase tables can anyone read with the anon key?
A table can be read by anyone with the anon or publishable key when the anon role has the select grant and either row level security is off or a select policy for anon or public is always true, such as using (true). One query in the SQL editor lists every public table with those three facts, and in the repo the same facts sit in supabase/migrations as grant, enable row level security and create policy statements.
Next step in your repo: run npx -y viberaven@1.6.12 check. It reads supabase/migrations and reports a public table with no enable row level security as rls_disabled, a table granted to anon or authenticated with row level security off as data_api_grant_without_rls, and a select policy with using (true) as rls_policy_allows_all_read.
Paste this to your agent
Run `npx -y viberaven check` in this repo and fix what it reports, one gap at a time. Also check that no table in my migrations can be read by anyone with the anon key. Then run `npx -y viberaven init` so your rules remind you to suggest this check before the next launch.The check runs locally, with no sign-in and no API key. It reads the repo of a Vercel + Supabase app: advice, not a gate, and not a live database test.
Repository check
Check this in your repo
Find open tables in your migrations
For AI-built Vercel + Supabase apps, run the command below from your app folder with Node.js 20 or newer. It reads the SQL in supabase/migrations and reports a public table that no migration enables row level security on as rls_disabled, a table that a migration grants to anon or authenticated while row level security is off as data_api_grant_without_rls, and a policy whose condition is a bare true for reading as rls_policy_allows_all_read.
It is a repository check, not a live database test: it cannot see changes made in the dashboard or policies that never reached a migration, so run the SQL query below on the live project too. It is advice, not a gate, and it exits with code 1 when it finds a blocker.
npx -y viberaven@1.6.12 checkWhat it printed on a fixture repo
On 2026-10-28 we ran it on a small fixture repo made for this page, not a customer repo, with one migration: public.profiles with no row level security and a grant to anon and authenticated, public.posts with a using (true) read policy for anon, and public.notes with an owner-only policy. The same SQL, run on Postgres 17, gave the query result shown further down. The output is below, with the folder shortened to ~/app. Two findings are blockers, so the exit code was 1.
$ npx -y viberaven@1.6.12 check
viberaven check · ~/app
🔴 No RLS policy proof in migrations (rls_disabled)
1 public table without row level security: public.profiles (supabase/migrations/20261001000000_init.sql:1). The Data API does not enforce row restrictions for roles and operations allowed by the table's grants. Verify those grants to determine who can read or change rows.
🔴 Table granted to the Data API with RLS off (data_api_grant_without_rls)
public.profiles is granted to anon and authenticated (supabase/migrations/20261001000000_init.sql:2) but row level security is off. A grant without RLS is the dangerous combination: the Data API serves every row to that role. Enable RLS and add policies before granting. Fix: alter table public.profiles enable row level security;
🟡 A policy lets anyone read every row (rls_policy_allows_all_read)
Policy "anyone can read posts" on public.posts (supabase/migrations/20261001000000_init.sql:6) lets anyone, signed in or not, read every row: using (true). Fine for truly public content; wrong for anything per-user.
Verdict: ❌ 2 blockers, 1 warning · score 50
Fix: viberaven fix · Details: ~/app/.viberaven/agent-tasklist.mdThe query for the live database
Run it in the Supabase SQL editor. It lists every table in the public schema with whether row level security is on, whether anon has the select grant, and the read policies that apply to anon or public. We ran it on Postgres 17 on 2026-10-28 against the fixture schema above.
select c.relname as table_name,
c.relrowsecurity as rls_enabled,
has_table_privilege('anon', c.oid, 'select') as anon_can_select,
coalesce(string_agg(p.policyname || ': using (' || coalesce(p.qual, '') || ')', '; '), '') as anon_select_policies
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
left join pg_policies p on p.schemaname = n.nspname and p.tablename = c.relname
and p.cmd in ('SELECT', 'ALL') and p.roles && array['anon', 'public']::name[]
where n.nspname = 'public' and c.relkind in ('r', 'p')
group by c.relname, c.relrowsecurity, c.oid
order by anon_can_select desc, rls_enabled, table_name;How to read the result
anon_can_select true and rls_enabled false: anyone can read every row. anon_can_select true and a policy with using (true): anyone can read every row, which is right only for public content. anon_can_select true, rls_enabled true and no policy listed: the anon key gets no rows. anon_can_select false: the Data API refuses anon on that table. On the fixture, profiles and posts were open and notes returned nothing to anon.
table_name | rls_enabled | anon_can_select | anon_select_policies
---------------+-------------+-----------------+-------------------------------------
profiles | f | t |
notes | t | t |
posts | t | t | anyone can read posts: using (true)
private_stuff | f | f |The same facts in your migrations
Search the migrations for the three statements. A create table with no enable row level security after it, or a policy with using (true) on a table that holds per-user data, is the line to fix.
grep -rniF -e "enable row level security" -e "create policy" -e "using (true)" -e "to anon" supabase/migrationsSigned in is not the same as trusted
If sign-ups are open, a read policy for authenticated with using (true) lets anyone who creates an account read every row. Whether a new table gets the anon grant on its own depends on the project's Data API setting; see the October 30 Data API change.
Checklist
Close the open tables
- 01Run the query above on the production project and list every table anon can read.
- 02For each one, decide whether the rows are meant to be public.
- 03For a table that is not public, turn on row level security and add a policy that matches each row to its owner, in a new migration:
alter table public.profiles enable row level security; create policy "read own profile" on public.profiles for select to authenticated using ((select auth.uid()) = id); - 04Replace using (true) on per-user tables with a condition on auth.uid().
- 05Run the query again and test with the anon key: an open table should now return no rows.
Related
Related guides
Sources
Official docs behind this checklist
Browse all launch guides · Read resources · How to make and check your next change
FAQ
FAQ for builders before launch
Does a view count?
Yes. A view in the public schema runs with its owner's rights by default, so it can return rows the table's policies would hide. Add views to the query with relkind 'v', or see the security_definer_view page.
Is a table with RLS on and no policies open?
No. Row level security with no policy returns no rows to anon and authenticated; only the service role can read it.