← All posts
·13 min read·GuardLayer

Supabase Security Advisor: every warning explained

SupabaseSecurity AdvisorPostgresRLSSecurity

The Supabase Security Advisor runs Supabase's database linter (splinter) against your live database and lists every lint that matches, each tagged ERROR, WARN or INFO. Fix the ERROR lints that expose data first (0013 RLS Disabled in Public, 0023 Sensitive Columns Exposed, 0010 Security Definer View, 0002 Exposed Auth Users, 0015 RLS references user metadata), then work through the WARNs. The table below gives every security lint with its number, title, level, meaning and our fix guide.

If you came here from one warning, find its number or title in the table and follow the link. If you came from the leaked password warning, that one isn't a database lint at all, and its fix is a single switch in the Email provider settings.

What is the Supabase Security Advisor?

It is a page in Supabase Studio that runs a set of SQL checks against your project's database and lists every object that fails one. The checks come from splinter, Supabase's open-source Postgres linter. Each lint is a SQL query that returns a name, a title, a level (ERROR, WARN or INFO) and a categories value of SECURITY or PERFORMANCE. The Security Advisor shows the SECURITY lints. The Performance Advisor shows the rest.

The same results are available outside Studio. Supabase's MCP server has a get_advisors tool that takes a type of security or performance, and Supabase CLI v2.81.0 added a supabase db advisors command "for checking security and performance issues".

Two things follow from how it works. It reads the database as it is right now, so it only sees a problem after the migration has run. And it checks structure (is RLS on, who holds a grant, is search_path pinned), not whether your policies are correct.

Which warnings does the Supabase Security Advisor show?

These are the lints with the SECURITY category in splinter's lints/ folder, plus 0012, which the database linter docs list but the splinter repo doesn't contain. The title is the lint's title field and the level is the one its SQL sets.

#Lint nameTitleLevelWhat it meansFix guide
0002auth_users_exposedExposed Auth UsersERRORA view or materialized view in an API schema exposes auth.users to anon or authenticated.Query auth.users without leaking emails
0007policy_exists_rls_disabledPolicy Exists RLS DisabledERRORThe table has RLS policies, but RLS is off, so none of them are enforced.Disabling RLS: what it exposes
0008rls_enabled_no_policyRLS Enabled No PolicyINFORLS is on but there are no policies, so the API can't read or write any rows.RLS with no policies: is it safe?
0010security_definer_viewSecurity Definer ViewERRORA view runs with its owner's rights, so RLS on the tables it reads is skipped.The 1-line Security Definer View fix
0011function_search_path_mutableFunction Search Path MutableWARNA function has no search_path of its own and inherits the caller's.Function Search Path Mutable fix
0012auth_allow_anonymous_sign_insAnonymous sign-ins enabled (docs summary)INFOAnonymous users share the authenticated role, so existing policies may let them in.Anonymous sign-ins
0013rls_disabled_in_publicRLS Disabled in PublicERRORA table in an API-exposed schema has RLS off.Disabling RLS: what it exposes, the Unrestricted badge
0014extension_in_publicExtension in PublicWARNAn extension is installed in public, so its functions and tables show up in your API. Move it with alter extension ... set schema.none yet
0015rls_references_user_metadataRLS references user metadataERRORA policy reads user_metadata, which every signed-in user can edit.user_metadata vs app_metadata
0016materialized_view_in_apiMaterialized View in APIWARNanon or authenticated can select a materialized view, which has no RLS.none yet
0017foreign_table_in_apiForeign Table in APIWARNA foreign table is readable over the API, and foreign tables don't respect RLS.none yet
0018unsupported_reg_typesUnsupported reg typesWARNA column uses a reg* type that blocks pg_upgrade, and with it Postgres upgrades.none yet
0019insecure_queue_exposed_in_apiInsecure Queue Exposed in APIERRORA Supabase Queue is exposed over the Data API with no RLS on its queue table.none yet
0021fkey_to_auth_uniqueForeign Key to Auth Unique ConstraintERRORA foreign key points at a non-primary-key unique constraint in auth, which blocks Auth migrations.none yet
0022extension_versions_outdatedExtension Versions OutdatedWARNAn extension is older than its default version and may lack security fixes.none yet
0023sensitive_columns_exposedSensitive Columns ExposedERRORA table with RLS off has columns named like passwords, tokens, card numbers or IDs.Every table needs RLS
0024rls_policy_always_trueRLS Policy Always TrueWARNA write policy for anon, authenticated or public is always true.The USING (true) trap
0025public_bucket_allows_listingPublic Bucket Allows ListingWARNA public bucket has a broad SELECT policy on storage.objects, so anyone can list its files.Public storage buckets
0026pg_graphql_anon_table_exposedPublic Can See Object in GraphQL SchemaWARNanon can select the object, so pg_graphql lists it in the GraphQL schema whatever RLS says.none yet
0027pg_graphql_authenticated_table_exposedSigned-In Users Can See Object in GraphQL SchemaWARNThe same for authenticated, which on open signup means anyone with an email address.none yet
0028anon_security_definer_function_executablePublic Can Execute SECURITY DEFINER FunctionWARNanon can call a SECURITY DEFINER function over /rest/v1/rpc.Public Can Execute SECURITY DEFINER Function
0029authenticated_security_definer_function_executableSigned-In Users Can Execute SECURITY DEFINER FunctionWARNThe same for authenticated.Same guide; fix both together

Two notes on the table. First, the docs page now describes each lint in plain language ("Table publicly accessible" for 0013, "View bypasses row-level security" for 0010). If your Studio shows that wording instead of the titles above, match on the lint number. Second, the docs list 0007 as INFO, while the lint's SQL in splinter sets it to ERROR. The table follows the SQL.

What about leaked password protection, MFA and OTP warnings?

They check your project's Auth settings, not the database, which is why none of them appears in splinter's lints/ folder or in the database linter docs. You fix them with a dashboard setting, not SQL, and no migration can trigger or clear them:

  • Leaked password protection. Turn on "Prevent use of leaked passwords" in the Email provider settings. Supabase Auth then rejects new passwords found in HaveIBeenPwned's Pwned Passwords data. The feature needs the Pro plan or above. Our step-by-step guide to enabling leaked password protection covers where the switch is and why it can be greyed out.
  • MFA options and OTP expiry. These point at your multi-factor settings and at how long email links and one-time codes stay valid. Both are changed in the Authentication section of the dashboard. If users start seeing otp_expired after you shorten the expiry, here is why email links expire early.

Which Security Advisor warnings should I fix first?

Fix the ERROR lints that expose rows first. Each one lets API callers reach data they shouldn't, most of them with nothing more than your publishable (anon) key:

  1. 0013 RLS Disabled in Public and 0023 Sensitive Columns Exposed. The table is open. Enable RLS and add policies. 0023 is 0013 on a table whose column names suggest the data is sensitive.
  2. 0010 Security Definer View and 0002 Exposed Auth Users. A view is reading past RLS. Set security_invoker = on, or replace the view.
  3. 0015 RLS references user metadata. The policy is enforced, but users can change the value it checks. Move the claim to app_metadata or a table only your server writes.
  4. 0007 Policy Exists RLS Disabled. Policies were written and then switched off.
  5. 0019 Insecure Queue Exposed in API. Anyone with the URL can read or change queue messages.

Then the WARNs that grant access: 0028/0029 (callable definer functions), 0024 (always-true write policies), 0016/0017 (objects RLS can't protect), 0025 (listable buckets) and 0026/0027 (GraphQL visibility). 0011, 0014, 0018, 0021 and 0022 matter, but they rarely expose rows on their own.

0008 is INFO for a reason: a table with RLS on and no policies is locked, not open. Leave it if deny-all is what you want, or add an explicit using (false) policy to document it.

What does the Performance Advisor check?

It runs the PERFORMANCE lints from the same repo: 0001 Unindexed foreign keys (INFO), 0003 Auth RLS Initialization Plan (WARN), 0004 No Primary Key (INFO), 0005 Unused Index (INFO), 0006 Multiple Permissive Policies (WARN), 0009 Duplicate Index (WARN), 0020 Table Bloat (WARN) and 0030 Autovacuum Disabled (INFO). Two of them come from RLS: 0003 fires when auth.uid() in a policy is evaluated once per row instead of once per query, and 0006 when a table has more than one permissive policy for the same role and action. Both are covered in why Supabase RLS queries are slow.

Which advisor findings can GuardLayer catch before deploy?

GuardLayer reads your migration files and code, so it can flag several of these shapes in a pull request, before the migration reaches the database. Each mapping below is to a rule in the scanner. Coverage is narrower than the advisor's, because the scanner reads your repo's files, not the database itself:

Advisor lintGuardLayer ruleWhat the rule catches
0013supabase/rls-missing-on-table, supabase/rls-explicitly-disabledA create table in public with no enable row level security in the same file, and any disable row level security statement.
0007supabase/rls-explicitly-disabledThe disable row level security statement that turns existing policies off.
0010, 0016, 0017supabase/grant-to-anonA view without security_invoker, a materialized view or a foreign table that the same migration grants to anon or PUBLIC. Views granted only to authenticated, or granted elsewhere, aren't flagged.
0011supabase/security-definer-mutable-search-pathSECURITY DEFINER functions without a pinned search_path. The lint also flags invoker functions. The rule doesn't.
0015supabase/rls-policy-user-metadataA policy that reads user_metadata or raw_user_meta_data.
0024supabase/policy-using-trueusing (true) / with check (true) policies, including SELECT ones, which 0024 skips.
0025supabase/public-storage-bucketA bucket created or updated as public. It doesn't check for the listing policy, which is the other half of 0025.

The grant-to-anon rule keys on the GRANT itself because, from Supabase's October 30, 2026 Data API change, new tables in public reach the API only through explicit grants.

Here is one migration that would light up the advisor once applied: a table with no RLS (0013), a view without security_invoker granted to anon (0010), and a definer function with no search_path (0011, plus 0028 and 0029 under default grants). The scanner flags all three shapes in the file itself:

create table public.invoices (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references auth.users (id),
  amount_cents integer not null
);

create view public.invoice_totals as
  select user_id, sum(amount_cents) as total_cents
  from public.invoices
  group by user_id;

grant select on public.invoice_totals to anon;

create or replace function public.is_admin()
returns boolean
language sql
security definer
as $$
  select exists (select 1 from admins where user_id = auth.uid());
$$;
guardlayer scan · supabase/migrations/20261027090000_invoices.sqlLive engine output
Passed with warnings
76/100 · B
  • Warningsupabase/migrations/20261027090000_invoices.sql:2

    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).
  • Warningsupabase/migrations/20261027090000_invoices.sql:13

    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/20261027090000_invoices.sql:15

    SECURITY DEFINER function without a fixed search_path

    Pin the search_path on the function: add `set search_path = ''` to the definition (or `ALTER FUNCTION <name> SET search_path = '';`) and fully qualify every object reference inside it, e.g. public.profiles instead of profiles.

The scanner cannot see:

  • Dashboard settings. Leaked password protection, MFA options, OTP expiry and anonymous sign-ins live in Auth settings, not in your repo.
  • Live database state. Tables or views created in the SQL editor, grants changed by hand, extension versions, bloat, unused indexes and anything else that never passed through a migration file. The Live check below covers part of this.
  • Most of the remaining lints. There is no rule today for 0002, 0008, 0014, 0018, 0019, 0021, 0022, 0023, 0026/0027 or 0028/0029.

Run both. The scanner catches the shape in review, and the advisor confirms what the database actually holds. GuardLayer posts a check on the pull request. It only blocks a merge if you make that check required in your branch protection rules.

Can I check my live project without opening Studio?

Yes. GuardLayer's Live check connects to your Supabase project read-only (you approve it in Supabase) and reads the database catalog, never your rows. It reports the live shapes behind several advisor lints, with the SQL to fix each:

Advisor lintWhat the Live check reports
0013, 0023A table without RLS that the publishable key, or any signed-in user, can read, with its estimated row count.
0010A view that skips the RLS of the tables under it.
0015A policy that trusts user_metadata.
0024A policy with a condition of true, including SELECT ones, which 0024 skips.
0028A SECURITY DEFINER function anyone can call.
0025A public storage bucket.
Auth settingLeaked password protection switched off.

It also tells you whether sign-ups are open, because an open sign-up turns every "signed-in users can read" table into "anyone with an email address can read". It doesn't run splinter, so it covers fewer lints than the advisor. What it adds is a count: how many tables, and roughly how many rows, are reachable right now.

Does a clean Security Advisor mean my app is secure?

No. A clean advisor means no lint matched, and the lints test structure, not logic. A policy like using (auth.uid() is null or user_id = auth.uid()) passes every lint and still returns every row to logged-out callers. A SELECT policy with using (true) clears 0013 and 0008 and isn't in 0024's scope. None of the lints read your Next.js code, so a service role key in a client component or an unauthenticated server action is invisible to it.

Treat a clean advisor as the floor, and test your policies as an anonymous caller.

FAQ

Where is the Security Advisor in Supabase? In Studio, open your project and go to Advisors → Security Advisor in the sidebar. The same lints are returned by the MCP get_advisors tool and the supabase db advisors CLI command.

What is splinter in Supabase? Splinter is Supabase's open-source Postgres linter. Each lint is a SQL query in the splinter repo, and the Security and Performance Advisors show the results of those queries for your project.

Why is there no lint 0012 in the splinter repo? 0012 (auth_allow_anonymous_sign_ins) checks an Auth setting, whether anonymous sign-ins are enabled, rather than a database object. The database linter docs list it, but the repo's lints/ folder doesn't contain a file for it.

What do ERROR, WARN and INFO mean in the Security Advisor? They are the level each lint sets in its SQL. In practice, ERROR lints mostly mark data that is reachable right now, WARN marks a risky configuration, and INFO marks something to review.

Why does a warning come back after I fixed it? Usually because the fix ran in the SQL editor and a later migration recreated the object without it. Put the fix in a migration so every environment, and every rebuild, gets it.

Can I ignore a Security Advisor warning? Yes, if you understand it. 0008 on a server-only table and 0014 on a project with no exposed extension functions are common deliberate choices. Write down why in the migration, so the next person doesn't undo it.

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 project

Prefer to check code instead? Scan your repo, no signup.

Keep reading