Supabase 'Unrestricted' table: what the red badge means
The red "Unrestricted" badge in the Supabase Table Editor means the Data API can reach that object with nothing filtering rows: for a table, RLS is off and anon or authenticated holds SELECT. Fix it with alter table ... enable row level security; plus a policy. For a view, materialized view or foreign table, the fix depends on the object type (below).
The badge sits next to the table name in the sidebar, and it tends to appear on every table at once. A GitHub discussion titled "Why are all my tables now marked "Unrestricted" in red?" (August 5, 2025) is still marked unanswered, and we found no Supabase docs page that explains the badge.
In the Supabase Security Advisor the same table shows up as lint 0013, RLS Disabled in Public. For the other warnings on that page, see every Security Advisor warning explained.
What does "Unrestricted" mean in Supabase?
It means Supabase's own linter found an object that anyone with your public key can read through the Data API with no row filter in front of it. Studio draws it from the Security Advisor's lint results. For a table, the lint is rls_disabled_in_public (lint 0013 in supabase/splinter), and Studio only shows the badge when that lint comes back at ERROR. The lint needs three things at once:
- Row Level Security is off on the table (
not c.relrowsecurity). anonorauthenticatedhasSELECTon it.- The table is in a schema exposed to PostgREST (
publicby default).
Together they mean the anon key in your browser bundle can read every row, and with Supabase's default grants (which include insert, update and delete) write them too. The Security Advisor lists the same finding as "RLS Disabled in Public".
Every tooltip state, decoded
Hover the badge to see which case you're in. These are the six states in Studio's current EntityListItem.tsx, verbatim. The first four show the red badge, the last two a grey globe.
| Tooltip text | Object | Badge | What it means |
|---|---|---|---|
| "This table can be accessed by anyone via the Data API as RLS is disabled." | Table | Unrestricted | Lint 0013: RLS off, anon or authenticated can SELECT. |
| "Data is publicly accessible via API as this is a Security definer view." | View | Unrestricted | The view isn't security_invoker, so it runs as its owner and skips RLS on the tables it reads. See the security definer view fix. |
| "Data is publicly accessible via API as materialized view is accessible via API." | Materialized view | Unrestricted | Materialized views can't have RLS, and anon or authenticated can select this one. |
| "Data is publicly accessible via API as RLS is not enforced on foreign tables." | Foreign table | Unrestricted | Foreign tables don't respect RLS policies, and this one is reachable over the API. |
| "This table can be accessed via the Data API but no RLS policies exist so no data will be returned" | Table | Globe | RLS is on with zero policies. Locked, not leaking: anon and authenticated get no rows. |
| "This table can be accessed via the Data API" | Table | Globe | No lint fired: typically RLS on with at least one policy. The intended state, if the policies are right. |
Until January 2026 the table tooltip read "Data is publicly accessible via API as RLS is disabled." Studio commit 1bcecb1 ("Table list entity api badge copy", #41547, January 5, 2026) replaced it with the current wording. Both strings mean the same thing.
The globe needs an exposed schema and at least one privilege for anon, authenticated or service_role. A table with no grants gets no icon.
How does a migration create an Unrestricted table?
It creates a table without enabling RLS and grants it to an API role. Since Supabase's Data API grant change, the grant is the part people now add by hand: on projects with the new default (new projects from the gradual rollout that began May 30, 2026; existing projects from October 30, 2026), a bare create table public.orders gets no Data API grants, so it shows no badge. Here's the shape that does:
-- 20261027120000_orders.sql
-- Orders for the checkout page. Granted so supabase-js can read it.
create table public.orders (
id bigint generated always as identity primary key,
user_id uuid not null references auth.users (id),
email text not null,
total_cents integer not null,
created_at timestamptz not null default now()
);
grant select on public.orders to anon, authenticated;
- Criticalsupabase/migrations/20261027120000_orders.sql:11
Table or view granted to the anon role without RLS
For a table, enable RLS (ALTER TABLE <t> ENABLE ROW LEVEL SECURITY;) and add scoped policies. For a view, create it WITH (security_invoker = true) and repeat that clause on every CREATE OR REPLACE VIEW. Materialized views and foreign tables cannot be protected: revoke the anon grant or move them to a schema the Data API does not expose. Prefer per-table grants over ON ALL TABLES IN SCHEMA public. Only keep an anon grant for genuinely public data that is still RLS-protected. - Warningsupabase/migrations/20261027120000_orders.sql:3
Table created without enabling RLS
Add ALTER TABLE <table> ENABLE ROW LEVEL SECURITY; plus access policies right after the CREATE TABLE, and grant anon / authenticated only the privileges the app needs, per table, after RLS is on. A later migration in the same scan that enables RLS (or drops the table) clears this warning. It still shows when the enable is in a file whose run order is unknown (a schema.sql with no version number) or not part of this scan (a pull-request scan sees only the files the PR changes).
GuardLayer reads that migration and reports two findings: supabase/grant-to-anon (critical) on the GRANT, because an unauthenticated role gets a table with no RLS in front of it, and supabase/rls-missing-on-table (warning) on the CREATE. The other badge states map like this:
alter table public.orders disable row level security;firessupabase/rls-explicitly-disabled(critical).- A view without
security_invoker, granted toanon, firessupabase/grant-to-anon(warning). - A materialized view granted to
anonfiressupabase/grant-to-anon(warning).
The limits: a table granted to authenticated only gets the warning and nothing critical, though Studio would badge it. GuardLayer reads your repo's migrations, not your live database, so it can't see dashboard edits or the globe states. It reports as a check on your push or pull request; blocking a merge on it is up to your branch protection.
After October 30, existing tables keep their grants, per Supabase's changelog, so the flip clears none of the red badges you already have.
How do I remove the Unrestricted badge?
For a table, enable RLS: alter table public.orders enable row level security;. The badge should give way to the "no RLS policies exist" globe, and the table returns no rows to anon and authenticated until you add a policy that grants the access you meant. The policy patterns are in disabling RLS and the fix. For the other objects:
- View: recreate it
with (security_invoker = true)so it runs with the caller's permissions (details in the security definer view post linked above). - Materialized view or foreign table: these can't be protected by RLS. Revoke
selectfromanonandauthenticated, or move the object to a schema the Data API doesn't expose.
Don't clear the badge by revoking grants on a table your app reads through supabase-js. That trades a data leak for a broken page.
"I enabled RLS and my app still works": which one are you?
In the #37688 thread, a user (scign, August 16, 2025) reported that enabling RLS from the button bar above the table removed the label, without adding any policy, and that their app "still works". That has two explanations, pointing in opposite directions.
Your app bypasses RLS. It reads the table with the service_role or secret key on the server, over a direct Postgres or ORM connection (the postgres role ignores RLS), or through SECURITY DEFINER functions. That can be fine, but then your server code, not your policies, guards those reads.
Your app is silently broken. With RLS on and no policy, a client-side select returns [] with no error. A page that renders an empty list looks like it "works". This is the empty array with no error case.
To tell which, run the read the browser makes, as the browser makes it:
curl "https://YOUR-PROJECT.supabase.co/rest/v1/orders?select=*&limit=1" \
-H "apikey: YOUR_ANON_KEY"
An empty array means anonymous callers are locked out. Check whether your UI relied on that read.
Quick self-check
This mirrors lint 0013 for the public schema. Run it in the SQL Editor:
select c.relname as table_name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relkind = 'r'
and not c.relrowsecurity
and (
has_table_privilege('anon', c.oid, 'SELECT')
or has_table_privilege('authenticated', c.oid, 'SELECT')
);
Every row it returns is a table that carries the Unrestricted badge. Then search your migrations for the cause:
grep -rniE 'disable[[:space:]]+row[[:space:]]+level[[:space:]]+security|grant[[:space:]].*to[[:space:]].*anon' supabase/migrations
FAQ
Is "Unrestricted" the same as "RLS Disabled in Public" in the Security Advisor?
For tables, yes. Studio shows the badge when lint 0013 (rls_disabled_in_public) reports the table at ERROR level. Views, materialized views and foreign tables get the badge from their own lints.
Why are all my Supabase tables suddenly Unrestricted? The badge reports the current state of each table. It doesn't mean the tables just changed. Tables created with SQL have RLS off until you enable it. On a project where new tables still received the default grants, every table created that way carries the badge.
Will the October 30, 2026 Data API change remove the badge? No. Supabase's changelog says existing tables keep their current grants. The change only affects tables created after it reaches your project.
Does adding a policy remove the badge? Not without enabling RLS. Policies on a table with RLS off aren't enforced, and lint 0013 looks at whether RLS is on, not at policies. Enable RLS first, then add the policy.
Does GuardLayer show the Unrestricted badge? No. It scans the SQL migrations in your repo and flags the statements that produce the badge before they reach your database. It doesn't connect to your project.
See which of your Supabase tables anyone can read, free
Connect your Supabase project read-only. GuardLayer checks RLS, policies, grants and functions as they are right now, and gives you the SQL to fix each problem. It never reads your rows and can't change anything.
Check my Supabase projectPrefer to check code instead? Scan your repo, no signup.