All guidesCheck your own repo
On this page

Supabase question

Supabase

Which of my Supabase tables can anyone read with the anon key?

By Ohad Krispin, founder of VibeRaven · LinkedIn

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 check

What 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.md

The 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/migrations

Signed 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

  1. 01Run the query above on the production project and list every table anon can read.
  2. 02For each one, decide whether the rows are meant to be public.
  3. 03
    For 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);
  4. 04Replace using (true) on per-user tables with a condition on auth.uid().
  5. 05Run the query again and test with the anon key: an open table should now return no rows.

Related

Sources

Official docs behind this checklist

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.