Supabase RLS check: is your data readable with the anon key?

By the FixMyVibe team · Last reviewed 17 October 2026
If a table in your exposed schema (by default public) has Row Level Security turned off, anyone who has your project URL and the public publishable/anon key can read and change its rows through the Data API. Turn RLS on for every such table and add policies that only allow what each user should see.
Why the public key makes RLS mandatory
Supabase exposes tables in your exposed schemas through its Data API. The publishable key (the legacy name is the anon key) is meant to ship in your frontend, so treat it as public. Supabase's own linter describes a public table without RLS as one where "anyone with your project URL can read, edit, and delete all data". RLS is the lock; the key is not.
Note: Supabase is replacing the legacy anon and service_role JWT keys with sb_publishable_… and sb_secret_… keys. The rules on this page apply to both generations.
How to check RLS yourself in 3 minutes
- Open your project in the Supabase dashboard and go to the SQL editor.
- Run this query:
select schemaname, tablename, rowsecurity from pg_tables where schemaname = 'public'; - Every row with
rowsecurity = falseneeds action. - Check which tables the public roles can actually reach (this matters more after Supabase's 30 October 2026 change, see below):
A table withselect c.relname as table_name, c.relrowsecurity as rls_enabled, has_table_privilege('anon', c.oid, 'select') as anon_can_read, has_table_privilege('authenticated', c.oid, 'select') as authenticated_can_read from pg_class c join pg_namespace n on n.oid = c.relnamespace where n.nspname = 'public' and c.relkind in ('r', 'p') order by c.relrowsecurity, c.relname;rls_enabled = falseand atruein either grant column is readable through the Data API. (You can also list grants withinformation_schema.role_table_grants.) - Open the Security Advisor in Studio (or run
supabase db advisorswith the CLI) and fix every ERROR, such as "Table publicly accessible".
Supabase's 30 October 2026 change: fixing "42501 permission denied" without reopening tables
Supabase announced on 28 April 2026 that new tables in the public schema will no longer be exposed to the Data API (REST, GraphQL, supabase-js) automatically. New projects have had this behaviour by default since a gradual rollout that started on 30 May 2026. On 30 October 2026 it is applied to all existing projects. Source: the Supabase changelog.
What that means in practice:
- Only new tables are affected. Tables that already exist keep their current grants and stay reachable. RLS behaviour does not change.
- A new
publictable needs an explicitGRANTbeforeanon,authenticatedorservice_rolecan use it through the Data API. Without one, the API returns{"code": "42501", "message": "permission denied for table your_table"}with a hint likeGRANT SELECT ON public.your_table TO anon;. - It does not apply to the
storage,authorrealtimeschemas or to custom schemas. It also does not apply if your app only talks to Postgres over a direct connection (psql, an ORM or an app server with a connection string).
The change itself makes new tables safer: a table that nobody granted stays invisible to the API. Supabase gives its own reason: the old default grants exposed every new table "including tables a developer forgot to protect".
The quick fix to avoid: granting all tables at once
When a new table returns 42501, it is tempting (for people and AI coding tools alike) to make the error go away with:
-- avoid this as a fix for 42501
grant select, insert, update, delete on all tables in schema public to anon, authenticated;This is not a new hole. It brings back the old risk for new tables: any table in public that has RLS turned off becomes readable and writable with your public key again. It also covers tables you meant to keep server-only. Supabase's own rollback FAQ includes a bulk grant, but it follows it with advice to revoke private tables one by one. Explicit, per-table grants are the safer default.
The safe fix: grant, RLS and policies per table, in one migration
Supabase recommends treating these three steps as a unit. For a table where each row belongs to a user (user_id):
begin;
-- 1. Enable RLS (no rows are visible until a policy allows them)
alter table public.notes enable row level security;
-- 2. Policies scoped to the signed-in user
create policy "read own notes" on public.notes
for select to authenticated
using ((select auth.uid()) = user_id);
create policy "insert own notes" on public.notes
for insert to authenticated
with check ((select auth.uid()) = user_id);
create policy "update own notes" on public.notes
for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
create policy "delete own notes" on public.notes
for delete to authenticated
using ((select auth.uid()) = user_id);
-- 3. Grant only what the app needs, only to the role that needs it
grant select, insert, update, delete on public.notes to authenticated;
-- grant select on public.notes to anon; -- only if logged-out visitors must read it
commit;Grant anon only on tables that logged-out visitors really need, such as a public product list, and never write privileges unless you mean it. If an existing table should not be reachable through the Data API at all, revoke it per table: revoke all on table public.your_table from anon, authenticated;. Then run the query from the 3-minute check above and test your app logged out, as user A and as user B.
Prompt for Lovable, Cursor, Claude Code and other AI tools
My Supabase app returns error 42501 "permission denied for table <table>".
Fix it safely. Do NOT use GRANT ... ON ALL TABLES IN SCHEMA public.
Write one migration for public.<table> only that:
1. enables row level security on the table,
2. adds policies so a user can only select/insert/update/delete rows
where user_id = auth.uid() (WITH CHECK on insert and update),
3. grants only the privileges the app needs on this one table, to
authenticated (and to anon only if logged-out visitors must read it).
Show me the SQL before applying it.FixMyVibe is not affiliated with Supabase. Facts in this section come from the official Supabase changelog linked above (checked 17 October 2026). For changes to your own project, Supabase's Security Advisor lists affected tables and suggests remediation SQL.
Policies that look secure but aren't
USING (true)on a SELECT policy lets every user with that role read every row. Fine for a public blog table, a leak for profiles or orders.WITH CHECK (true)or a missing WITH CHECK on INSERT/UPDATE lets users write any row, including rows that belong to someone else. Supabase flags both as lint 0024.- Policies on
user_metadata: users can edit their own user_metadata, so a policy like "is_admin in user_metadata" can be bypassed (lint 0015). - Views are security definer by default in Postgres and ignore the RLS of the underlying tables unless you create them
with (security_invoker = on)(lint 0010). - A policy exists but RLS is off: the policy does nothing until you run
alter table … enable row level security(lint 0007).
The secret (service_role) key: never in the browser
The secret key authorizes through the service_role Postgres role, which has the bypassrls attribute. Supabase's docs are explicit: never use a secret key in the browser or expose it to customers. If it ever shipped in a frontend bundle or a public repo, rotate it immediately (see leaked API keys).
What FixMyVibe checks (and what it doesn't)
The free scan reads the current files of your public repository, read-only. For Supabase projects it looks at the committed .sql files (for example supabase/migrations; folders such as node_modules, dist, build, .next and vendor, and files of 800 KB or more, are skipped) and flags:
- tables that your migrations create without enabling Row Level Security (critical);
- policies with
USING (true), which let every user with that role read or change every row (high); - policies that trust
user_metadata, which users can edit themselves; WITH CHECK (true)on INSERT or UPDATE policies, so users (or anyone, if the policy is not limited toauthenticated) can write rows that belong to someone else;- authorization helper functions that always return true;
- views created without
security_invoker, which ignore the RLS of the tables underneath; - SECURITY DEFINER functions without a fixed
search_path; - write privileges granted to
anon, bulkGRANT … ON ALL TABLES IN SCHEMA publictoanon, and bulk write grants toauthenticated. The severity depends on which privileges are granted and on whether your migrations enable RLS on the tables; - storage buckets created as public in your SQL;
- as information only, new tables in
publicwithout an explicit GRANT, which may stop working after Supabase's 30 October 2026 change described above.
It also checks Edge Functions that turn off JWT verification (verify_jwt = false) and have no auth check of their own, rated high if such a function uses the service_role key; Supabase secret/service_role keys and other secrets committed in the code; and service_role keys inside committed build output or source maps. FixMyVibe's own secret checks report the key type (for example sk_live_…), the variable name or the name of the detection rule, never the key value. In the Full and Pro audits, text written by the AI review is filtered for known key formats before it reaches your report.
What the free scan does not do: it does not connect to your database, so it cannot see tables, grants or policies you created by hand in the dashboard and never committed as migrations, and it cannot tell whether your migrations were actually applied. The optional live check in the Pro audit reads your Supabase API only after you verify that you own the site; it is read-only and stores no row data. FixMyVibe does not scan your git history, only the current files. It is an automated pattern check, not a penetration test. Run the SQL query above and the Supabase Security Advisor as well.
How to fix
- Enable RLS on each flagged table:
alter table public.profiles enable row level security; - Add a policy per action your app needs, scoped to the owner. The pattern from the Supabase docs:
create policy "own rows" on profiles for select to authenticated using ((select auth.uid()) = user_id); - For INSERT/UPDATE, add a
with check ((select auth.uid()) = user_id)so users cannot write rows for someone else. - Re-run the Security Advisor and test your app logged out, logged in as user A, and as user B.
FAQ
Is the Supabase anon (publishable) key a secret?
No. Supabase designs the publishable key (and the legacy anon key) to be shipped in browser and mobile code. Anyone can read it, so it only reaches what your Row Level Security policies allow. That is exactly why RLS is mandatory on every exposed table.
Does enabling RLS break my app?
It can, briefly. Once RLS is enabled on a table, the publishable key gets no rows at all until you add policies. Enable RLS, then add the policies your app actually needs (for example: users can read their own rows), and test each screen.
How do I know which tables are public?
Run the pg_tables query from this page in the Supabase SQL editor: every table in the public schema with rowsecurity = false is reachable through the Data API as long as the anon or authenticated role has a grant on it (tables created before Supabase's 30 October 2026 change have those grants by default). The second query on this page shows the grants. Supabase's Security Advisor also flags this as 'RLS disabled in public' (lint 0013).
What's the difference between the publishable/anon key and the secret/service_role key?
The publishable (anon) key is low-privilege and is filtered by RLS. The secret (service_role) key authorizes as the service_role Postgres role, which has the BYPASSRLS attribute: it ignores every policy. It must only be used on a server or in an Edge Function, never in the browser.
My Supabase app shows "42501 permission denied for table" after 30 October 2026. Should I grant all tables to anon?
No. From 30 October 2026 Supabase no longer grants Data API access automatically on new tables in the public schema; existing tables keep their grants. Running GRANT ... ON ALL TABLES IN SCHEMA public TO anon makes the error go away, but it also makes every new table that has RLS turned off reachable again with the public key, which is the old risk. Instead, per table: enable RLS, add policies scoped to auth.uid(), and grant only the privileges your app needs to the role that needs them. Source: Supabase changelog, https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically
Can FixMyVibe see my data?
No. The scan reads the code in your GitHub repository (for example your SQL migrations and client code). It does not connect to your database. The cloned code is processed in a temporary directory and deleted when the scan finishes.
Sources
- Supabase changelog: Breaking Change: Tables not exposed to Data and GraphQL API automatically (28 April 2026; enforced on existing projects 30 October 2026)
- Supabase docs: Row Level Security
- Supabase docs: Understanding API keys
- Supabase docs: Advisors (database linter), incl. 0013 rls_disabled_in_public and 0024 permissive_rls_policy
FixMyVibe is an automated code scan, not a full penetration test, and cannot guarantee that an app is secure. What happens to your code during a scan: see our privacy policy.