HomeGuidesSupabase Security Advisor warnings explained and fixed
Guide
Supabase Security Advisor warnings explained and fixed
What each Supabase Security Advisor lint means, which ones are urgent, and the SQL fix: RLS disabled, mutable search_path, definer views, and more.
Independent notes from VibeLocked. Not a pentest, and not affiliated with the tools named below.
You open the Supabase dashboard and see a red badge: "RLS Disabled in Public," "Function Search Path Mutable," "Security Definer View," "Leaked Password Protection Disabled." If an AI tool such as Lovable, Bolt, Cursor, or v0 wrote your schema, a list like that is normal. Some of those findings mean anyone with your project URL can read or delete data today. Others are hardening you can do next week.
This guide goes through every Security Advisor finding a typical vibe-coded app gets, says in plain words what the real risk is, gives the SQL that clears it, and tells you which ones can wait. For the table policies themselves, keep the Supabase RLS checklist open next to this page.
Checked on October 19, 2026 against the Supabase advisor docs, the lint SQL in the open source splinter repo, the Supabase Studio source, Supabase CLI 2.121.0, and the PostgreSQL docs linked below. We also ran splinter against a Postgres 17 test database set up with Supabase's roles and grants, applied each fix, and confirmed the findings cleared. VibeLocked is independent and not affiliated with Supabase.
The short version
- ERROR means fix it today. These findings usually mean data is readable or writable over the API with the key that ships in your frontend.
- Some WARN findings are just as serious. A
SECURITY DEFINERfunction thatanoncan call, or a policy withusing (true), can expose every row. - Enabling RLS without a policy blocks your app. Write the policies in the same migration.
- Mutable search_path, extension in public, MFA options, and the Postgres version warning are hardening. Schedule them, do not ignore them.
- Re-run the advisor after every migration. AI tools add new tables and functions all the time, and each one can bring a finding back.
Where to find the Security Advisor
In the dashboard, open your project and go to Advisors, then Security Advisor. Supabase Studio sorts findings into three tabs, and its own descriptions are a good guide to urgency:
- Errors: "You should consider these issues urgent and fix them as soon as you can."
- Warnings: "You should try and read through these issues and fix them if necessary."
- Info: "You should read through these suggestions and consider implementing them."
Supabase's advisors page lists other ways to run the same checks:
- CLI:
supabase db advisors. In CLI 2.121.0 it takes--linkedor--local,--type security,--level, and--fail-on, sosupabase db advisors --linked --type security --fail-on errorexits non-zero when any ERROR is present. - MCP: the Supabase MCP server's
get_advisorstool withtypeset tosecurity. If Cursor or Claude Code talks to Supabase over MCP, ask it to run this after each migration. - Management API: the security advisors endpoint.
Do not confuse it with supabase db lint. The CLI reference says that command runs plpgsql_check to find errors in your functions. It will not report a table with RLS disabled.
Almost every numbered lint (0012 is the exception) is plain SQL in the splinter repo, and splinter.sql holds all of them in one query you can run in your own project's SQL editor. Findings without a number, such as auth_leaked_password_protection, are platform checks on your Auth and project settings. They are not in splinter.
Prioritized table of Security Advisor lints
Levels come from the lint SQL in splinter, which is what the Advisor reports. Platform checks (no number) are not in splinter; their level is what the Advisor returned for our test project or in public Advisor output we reviewed.
| Lint | Level | Priority | What it means |
|---|---|---|---|
0013_rls_disabled_in_public |
ERROR | Today | A public table has RLS off, so anyone with the URL and publishable key can read and write it |
0023_sensitive_columns_exposed |
ERROR | Today | Same as 0013, and the table has columns named like password, token, or ssn |
0002_auth_users_exposed |
ERROR | Today | A view exposes auth.users data to API roles |
0010_security_definer_view |
ERROR | Today | A view in public runs as its owner and ignores RLS |
0015_rls_references_user_metadata |
ERROR | Today | A policy trusts user_metadata, which users can edit |
0007_policy_exists_rls_disabled |
ERROR | Today | You wrote policies, but RLS is off, so they do nothing |
0019_insecure_queue_exposed_in_api |
ERROR | Today | A Supabase Queue is reachable over the API without RLS |
0021_fkey_to_auth_unique |
ERROR | This week | A foreign key blocks future Auth migrations |
0028_anon_security_definer_function_executable |
WARN | This week | Signed-out visitors can call a function that bypasses RLS |
0029_authenticated_security_definer_function_executable |
WARN | This week | Any signed-in user can call a function that bypasses RLS |
0024_rls_policy_always_true |
WARN | This week | A policy uses using (true) or with check (true) |
0016_materialized_view_in_api |
WARN | This week | API roles can read a materialized view, which cannot use RLS |
0017_foreign_table_in_api |
WARN | This week | API roles can read a foreign table, which cannot use RLS |
0025_public_bucket_allows_listing |
WARN | This week | Anyone can list every file in a public Storage bucket |
auth_otp_long_expiry |
WARN | This week | Email OTPs and links stay valid for more than an hour |
auth_leaked_password_protection |
WARN | This week (Pro plan) | New passwords are not checked against known leaked passwords |
0011_function_search_path_mutable |
WARN | Schedule | A function has no fixed search_path |
0014_extension_in_public |
WARN | Schedule | An extension's objects sit in the API-exposed public schema |
0022_extension_versions_outdated |
WARN | Schedule | An extension is behind its default version |
0026 and 0027_pg_graphql_*_table_exposed |
WARN | Schedule | Table and column names are visible through GraphQL introspection |
auth_insufficient_mfa_options |
WARN | Schedule | Fewer MFA methods are enabled than Supabase recommends |
vulnerable_postgres_version |
WARN | Schedule (plan downtime) | Your Postgres build has security patches available |
0008_rls_enabled_no_policy |
INFO | Check it | RLS is on but no policy exists, so the API sees nothing |
0012_auth_allow_anonymous_sign_ins |
INFO | Check it | Anonymous users share the authenticated role |
Two naming quirks you may hit. Supabase's advisor docs list 0007 as INFO, but the lint SQL in splinter has reported it as ERROR since April 2024, and it showed as ERROR in our test run. And lint 0024 reports itself as rls_policy_always_true, while its docs anchor is 0024_permissive_rls_policy.
Fix today: the ERROR findings
RLS Disabled in Public (0013) and Sensitive Columns Exposed (0023)
This is the one that matters most. Tables in public are served by the Data API. Supabase's lint docs say that without RLS, "anyone with the project's URL can CREATE/READ/UPDATE/DELETE (CRUD) rows in the impacted table," and call it "a critically unsafe configuration." The project URL and the publishable (or legacy anon) key are in your frontend bundle by design, so "anyone" really means anyone. Lint 0023 is the same finding on a table whose column names look like passwords, tokens, payment data, or government IDs.
The one-line fix is alter table public.notes enable row level security;. Do not run it alone on a live app. Supabase's RLS guide says: "Once RLS is enabled, no data is accessible through the API when using a publishable key, until you create policies." If your frontend reads that table, it suddenly gets empty arrays, and writes fail. Safe, but broken.
The current Supabase guide adds a second step: set the table grants to match what each role actually does, because adding policies does not take away the default grants. Do all of it in one migration:
-- 1. Turn on RLS
alter table public.notes enable row level security;
-- 2. Give each role only the operations it needs
revoke all on table public.notes from anon, authenticated;
grant select, insert, update, delete on table public.notes to authenticated;
-- 3. One policy per operation, scoped to the owner
create policy "Users read own notes"
on public.notes for select
to authenticated
using ( (select auth.uid()) = user_id );
create policy "Users insert own notes"
on public.notes for insert
to authenticated
with check ( (select auth.uid()) = user_id );
create policy "Users 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 "Users delete own notes"
on public.notes for delete
to authenticated
using ( (select auth.uid()) = user_id );
Notes on writing a correct policy:
to authenticatedmatters. Without atoclause the policy applies to every role, includinganon.- Write both clauses on updates.
usingdecides which rows a user can touch, andwith checkdecides what the row may look like afterwards. The PostgreSQL CREATE POLICY docs say that whenwith checkis missing on an update policy, theusingexpression is reused for it. Writing both keeps the rule explicit when someone later loosensusing, for example to a team check, and stops a user from moving a row to anotheruser_id. - Wrap
auth.uid()inselect. Supabase's performance lint0003_auth_rls_initplanexists because a bareauth.uid()runs once per row.(select auth.uid())runs once per query. - Public data gets an explicit public policy. A blog post table can have
to anon, authenticated using (true)for select, with no write grants foranon. Make that choice on purpose, table by table.
If you do not use the Data API at all (for example, every query runs from your own server with a direct database connection), the lint docs offer another fix: remove public from the exposed schemas in the API settings.
For 0023, also ask whether the column belongs there at all. Password hashes already live in auth.users, and API tokens belong on the server.
Before you call it fixed, test as the browser would. The SQL editor runs as postgres, which bypasses RLS, so simulate the API roles inside a transaction:
begin;
set local role anon;
select count(*) from public.notes; -- expect 0
rollback;
begin;
set local role authenticated;
set local request.jwt.claim.sub = '<user-a-uuid>';
select distinct user_id from public.notes; -- expect only user A
rollback;
We ran exactly this on our test database: after enabling RLS with no policy, anon saw 0 rows; after adding the select policy, user A saw only their own row. To keep it that way, turn these checks into a test that runs on every pull request (testing Supabase RLS in CI).
Policy Exists RLS Disabled (0007)
You (or your AI tool) wrote policies but never turned RLS on, so Postgres ignores them and the table is as open as in 0013. The fix is the same alter table ... enable row level security;. Read the existing policies first, because enabling RLS makes them live. If one of them says using (true), you have swapped one open door for another.
Security Definer View (0010) and Exposed Auth Users (0002)
A Postgres view checks the underlying tables with the view owner's rights by default. On Supabase the owner is usually postgres, which bypasses RLS, so a view in public can return every row your table policies were meant to hide. On Postgres 15 and above the fix is one line:
alter view public.order_summaries set (security_invoker = on);
A view over auth.users (0002) should usually become a public.profiles table filled by a trigger. Grants, the create or replace view trap, and tests for both are in Supabase views and functions that bypass RLS.
RLS references user metadata (0015)
A policy that reads auth.jwt() -> 'user_metadata' trusts data the user controls. Supabase's lint docs show the attack in one line: a signed-in user calls updateUser({ data: { is_admin: true } }) and the policy believes them. Move roles to app_metadata (only your server can set it), a custom access token hook, or a separate user_roles table that users cannot write. The client-side auth guide shows both setups.
Queue exposed (0019) and foreign key to auth unique (0021)
For 0019, enable RLS on the queue table (alter table pgmq.q_<queue_name> enable row level security;) and add policies, or remove pgmq_public from the exposed schemas. Lint 0021 is not a leak: a foreign key points at an auth unique constraint Supabase plans to remove, which would block Auth security updates. Point it at auth.users(id) instead.
Fix this week: WARN findings that are real exposure
SECURITY DEFINER functions anon or signed-in users can call (0028, 0029)
A SECURITY DEFINER function runs as its owner, usually postgres, so it skips RLS. Postgres grants EXECUTE on new functions to PUBLIC, the lint docs say Supabase also grants them to anon, authenticated, and service_role, and PostgREST serves them at /rest/v1/rpc/<name>. So an admin helper becomes an endpoint anyone can call. This is often how an RLS recursion fix goes wrong: the helper lands in public. Fixing the infinite recursion error puts it in a private schema instead. For 0029, the docs add that with open signup "the audience for authenticated is effectively the public internet."
The fix is per function. Most of the time, revoke:
revoke execute on function public.get_all_orders() from public, anon, authenticated;
If the function only needs normal user access, switch it to SECURITY INVOKER so RLS applies:
alter function public.get_all_orders() security invoker;
Two things we checked on Postgres 17 that save you worry:
- Trigger functions are a false alarm, but revoke anyway. The classic Supabase
handle_new_user()trigger shows up in 0028 and 0029. Postgres refuses to run a trigger function outside a trigger ("trigger functions can only be called as triggers"), and in our test, revokingEXECUTEfrom every API role did not stop the trigger from firing on insert. Revoking clears the warning and costs nothing. - Some functions are meant to be public. A "confirm my newsletter token" or "submit contact form" function may need to be callable by
anon. Then it is a public API endpoint: validate every argument, limit what it touches, never build SQL from its input, and accept the warning on purpose.
Supabase's securing your API guide shows how to change default privileges so the next function is not public by default.
RLS policy always true (0024)
A policy with using (true) or with check (true) for anon or authenticated means RLS is on but every row is allowed. The lint docs note it often comes from placeholder policies "during development" that nobody updated, which is exactly how AI-generated migrations look. Replace it with an owner check like the ones above. If the table really is public (a product catalog, published posts), keep using (true) on select only, never on insert, update, or delete, and accept the warning for that table.
Materialized views and foreign tables in the API (0016, 0017)
Neither can use RLS. If API roles have select, everyone sees every row. Revoke, including from public, which carries default permissions:
revoke select on public.order_totals from public, anon, authenticated;
Read them from server code instead, or through a function that applies the owner check.
Public bucket allows listing (0025)
A public bucket already serves files by URL. Adding a broad select policy on storage.objects also lets clients list every file name in it. Drop that policy unless listing is the point. The Supabase Storage bucket security guide covers private buckets and owner policies.
Auth OTP long expiry (auth_otp_long_expiry)
The advisor text says you "enabled the email provider with the OTP expiry set to more than an hour." Supabase's production checklist recommends "3600 seconds (1 hour) or lower." The setting is Email OTP expiration under Authentication, Sign In / Providers, Email, and it covers email OTP codes and links. A long-lived code gives an attacker more time to use a code from an inbox they briefly got into. Set it to 3600 or less. Studio also has a related auth_otp_short_length check for the Email OTP length field, which accepts 6 to 10 digits.
Leaked password protection disabled (auth_leaked_password_protection)
This setting rejects passwords found in the HaveIBeenPwned Pwned Passwords data on signup and password change. It is a cheap defense against credential stuffing, where attackers try passwords leaked from other sites. In Studio it is the Prevent use of leaked passwords toggle in the Email provider settings.
The plan matters. Supabase's password security docs say: "Leaked password protection is available on the Pro Plan and above." The warning still appears on Free plan projects; our Free plan test project showed it. If you are on Free, set the minimum password length to at least 8, require mixed character types, turn on CAPTCHA, note the warning as accepted, and turn the toggle on when you upgrade. The Supabase Auth settings guide and login brute force protection cover the rest of the login defenses.
Schedule these: hardening WARN findings
Function Search Path Mutable (0011)
This is the most common warning on AI-built projects, often on small trigger functions like touch_updated_at. A function without a fixed search_path resolves unqualified names (profiles rather than public.profiles) using the caller's settings. The lint docs explain the risk: someone could "direct the function to use unexpected objects, such as tables or other functions, that the malicious user controls." That matters most for SECURITY DEFINER functions, which run with the owner's privileges.
How urgent is it? The PostgreSQL schemas docs say that on Postgres 15 and later the default configuration no longer lets every role create objects in public, but a database upgraded from an older version may still allow it. Check your own project in the SQL editor with select has_schema_privilege('anon', 'public', 'create'), has_schema_privilege('authenticated', 'public', 'create');. If both are false, a plain invoker trigger function with this warning is hardening, not an open door. Fix it anyway, because it takes one line and it makes your definer functions safe:
alter function public.touch_updated_at() set search_path = '';
Include the argument types for functions that take arguments, for example alter function public.is_pro(uuid) set search_path = '';. Then check the body. With an empty search path, every table, view, and function in it must be schema qualified. Built-ins such as now() still work because pg_catalog is always searched. We confirmed on Postgres 17 that a function reading an unqualified notes table failed with relation "notes" does not exist after the change, and worked again once the body said public.notes.
One more trap: we also confirmed that create or replace function without a set search_path clause drops the setting. When an AI tool rewrites a function in a new migration, the warning comes back. Put the setting in the definition itself:
create or replace function public.touch_updated_at()
returns trigger
language plpgsql
set search_path = ''
as $$
begin
new.updated_at := now();
return new;
end;
$$;
To list every function in public that still lacks it:
select p.oid::regprocedure as function
from pg_catalog.pg_proc p
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
and not exists (
select 1 from unnest(coalesce(p.proconfig, '{}')) as c
where c like 'search_path=%'
);
Extension in Public (0014)
Extensions such as pg_trgm, vector, or citext installed into public put their functions and tables into your API surface. Supabase's extensions docs say most extensions are installed under the extensions schema. The risk is mostly clutter and unintended functions exposed over the API, so this can wait for a quiet day.
The fix for a relocatable extension is one line:
alter extension pg_trgm set schema extensions;
It cleared the lint on our test database. Three cautions:
- Calls may need the new schema. Code that calls
public.similarity()breaks after the move;extensions.similarity()always works. - Not every extension can move. The PostgreSQL ALTER EXTENSION docs say it "has to be relocatable for this command to succeed." Otherwise you drop and recreate it, which can remove dependent indexes and columns.
- Never run
drop extension ... cascadeon a live database. Try it on a branch or local copy first, after a backup.
Extension versions outdated (0022)
Run alter extension <name> update; after reading that extension's changelog and testing outside production. The lint docs note that Supabase's SLA for extension issues covers only the default version.
GraphQL schema exposure (0026, 0027)
If pg_graphql is installed, any table anon or authenticated can select shows up, with column names, in /graphql/v1 introspection, even when RLS returns zero rows. If your app does not use GraphQL (most vibe-coded apps use supabase-js against the REST API), the lint docs' simplest fix is drop extension pg_graphql;. If you do use it, revoke select from anon and authenticated on tables those roles never need.
Insufficient MFA options (auth_insufficient_mfa_options)
This finding points at Authentication, Multi-Factor. Supabase does not document the exact threshold. It is about which MFA methods your project offers, not whether your users use them. Supabase's TOTP docs say app authenticator (TOTP) MFA "is free to use and is enabled on all Supabase projects by default." Phone MFA is a paid add-on; Supabase's billing page lists it at $75 per month for the first project.
What protects your app: keep TOTP enabled, add an enrollment screen, and require MFA for admin accounts with a restrictive policy on (select auth.jwt()->>'aal') = 'aal2', as Supabase's MFA guide shows. Do not buy an add-on just to clear the badge. And turn on MFA for your own Supabase login, which controls the whole database.
Postgres version has security patches available (vulnerable_postgres_version)
The Advisor says your Postgres build "has outstanding security patches available." The fix is an upgrade, not SQL. Supabase's upgrading docs say to use the Upgrade project button on the General settings page, that "your project is taken offline" while it runs, and that a failed upgrade brings the original database back. Pick a quiet window, take a backup first, check the prerequisites (read replicas, reg* column types flagged by lint 0018), and test the app afterwards. Do it within weeks, not months.
INFO findings worth a look
RLS Enabled No Policy (0008)
RLS is on and no policy exists, so the Data API returns nothing for that table. That is safe, and a bug only if your app needs the data. It is correct for a table only your server touches with the secret key, such as billing_events written by a Stripe webhook. The lint docs suggest making that intent explicit with a deny policy, which also cleared the finding on our test database:
create policy "No API access"
on public.billing_events
for all
to anon, authenticated
using (false)
with check (false);
If the frontend does need the table, write real policies as in the 0013 section.
Anonymous sign-ins allowed (0012)
With anonymous sign-ins on, anonymous users get the authenticated role. Any policy that says only to authenticated lets them in. Where that is wrong, add a check on the JWT's is_anonymous claim, as Supabase's anonymous sign-ins docs describe.
Re-run the Advisor and keep it green
After you apply fixes, refresh the Security Advisor or run supabase db advisors --linked --type security. Then keep it green:
- Make every fix a migration, not a hand edit in the dashboard.
- Run the advisor after each migration your AI tool writes. In CI,
supabase db advisors --local --type security --fail-on errorfails the build on any new ERROR. - Write down accepted findings (an intentional public function, a public catalog) and why.
- Test as
anonand as two users, with the transactions above or pgTAP tests run bysupabase test db.
The Advisor checks the database. It cannot see a secret key in your frontend, which bypasses every policy on this page. Check your public repo with the free VibeLocked secret scanner and read anon key vs service role key if you are not sure which key your app ships.
Checklist
- Security Advisor shows no ERROR findings
- Every
publictable has RLS on, grants set per role, and one policy per operation - Tested as
anonand as two signed-in users; nobody sees another user's rows - No view in
publicwithoutsecurity_invoker; no view overauth.users - No policy reads
user_metadata - Every
SECURITY DEFINERfunction hasEXECUTErevoked frompublic,anon, andauthenticatedunless it is meant to be public - No
using (true)policy except deliberate public reads - Email OTP expiration is 3600 seconds or less
- Leaked password protection on (Pro plan), or minimum length, character rules, and CAPTCHA on Free
- Functions set
search_path = ''in their definitions - Extensions live in the
extensionsschema - Postgres upgrade scheduled if the Advisor reports patches
- Advisor runs after every migration, with accepted findings written down
Then run the ship checklist for a vibe-coded app before you share the link.
Copy-paste prompt for your AI coding tool
Paste this into Cursor, Claude Code, Lovable, or Bolt. Paste the advisor findings (lint names and object names), never keys, passwords, or production data.
Here are my Supabase Security Advisor findings: [paste lint names and object names only]. For each one, explain the risk in one sentence, then write one new SQL migration file that fixes it without editing old migrations. Rules: when enabling RLS, in the same migration revoke all from anon and authenticated, grant back only what the app needs, and add one policy per operation using (select auth.uid()) with to authenticated, plus with check on insert and update. Never use using (true) except for data I confirm is public. For views use security_invoker = on. For SECURITY DEFINER functions revoke execute from public, anon, and authenticated unless I say the function is public. For mutable search_path add set search_path = '' and schema qualify every name in the body. Do not use drop ... cascade. List every frontend query that will return empty results after these changes. Do not run anything against production; show me the migration first.
Review the migration yourself before it runs, and test it on a branch or local database first. More prompts for reviewing AI-written code are in the security review prompt guide.
Sources
- Supabase: Advisors (database lints 0001 to 0030, levels, fixes)
- Supabase: lint 0013_rls_disabled_in_public
- Supabase: lint 0011_function_search_path_mutable
- supabase/splinter: lint SQL and splinter.sql
- splinter: 0007_policy_exists_rls_disabled lint SQL (ERROR level)
- Supabase Studio: Advisor level tabs and their descriptions
- Supabase Studio: lint catalog (platform check names and settings links)
- Supabase CLI reference: supabase db advisors
- Supabase CLI reference: supabase db lint (plpgsql_check)
- Supabase: MCP server (get_advisors tool)
- Supabase: Row Level Security (enable RLS, grants, policies)
- Supabase: Securing your API (grants, default privileges)
- Supabase: Database functions (security definer and invoker)
- Supabase: Postgres extensions (extensions schema)
- Supabase: Password security (leaked password protection, Pro Plan and above)
- Supabase: Multi-Factor Authentication (aal2 policies)
- Supabase: MFA with TOTP (free, on by default)
- Supabase: Manage Advanced MFA Phone usage (pricing)
- Supabase: Anonymous sign-ins (is_anonymous claim)
- Supabase: Production checklist (OTP expiry 3600 seconds or lower)
- Supabase: Upgrading (Postgres upgrades and downtime)
- PostgreSQL: Schemas (public schema CREATE privilege)
- PostgreSQL: CREATE FUNCTION (Writing SECURITY DEFINER Functions Safely)
- PostgreSQL: ALTER FUNCTION (SET configuration_parameter)
- PostgreSQL: ALTER EXTENSION (SET SCHEMA requires a relocatable extension)
- PostgreSQL: Row Security Policies
- PostgreSQL: CREATE POLICY (USING reused as WITH CHECK on updates)
- Have I Been Pwned: Pwned Passwords
Questions
Which Supabase Security Advisor warnings should I fix first?
Fix every ERROR first, starting with rls_disabled_in_public (0013), sensitive_columns_exposed (0023), security_definer_view (0010), auth_users_exposed (0002), and rls_references_user_metadata (0015). Those mean data may be readable or writable over the API right now. Next fix the WARN findings that are real exposure, such as SECURITY DEFINER functions anon can call (0028, 0029) and policies that always return true (0024). Mutable search_path, extensions in public, MFA options, and the Postgres version warning can go on a schedule.
Will enabling RLS break my Supabase app?
It can. Supabase's docs say that once RLS is enabled, no data is accessible through the API with the publishable or anon key until you create policies. If your frontend reads that table, it will start getting empty results. Write a select policy (and insert, update, delete policies if the app writes) in the same migration that enables RLS, then test as anon and as two different signed-in users.
How do I fix function_search_path_mutable in Supabase?
Run alter function public.your_function(arg types) set search_path = ''; for each flagged function, then make sure every table and function inside the body is schema qualified, such as public.profiles. If a name is unqualified the function fails with relation does not exist. A later create or replace function without the set clause removes the setting again, so put set search_path = '' in the function definition in your migrations.
Is leaked password protection available on the Supabase Free plan?
No. Supabase's password security docs say leaked password protection is available on the Pro Plan and above. The Security Advisor can still show auth_leaked_password_protection on a Free plan project; we saw it on one. On Free, set a minimum password length of at least 8, require mixed character types, and add CAPTCHA, then record the warning as accepted until you upgrade.
Is supabase db lint the same as the Security Advisor?
No. supabase db lint runs plpgsql_check to find typing errors in your database functions. The Security Advisor lints run with supabase db advisors (for example supabase db advisors --linked --type security), with the get_advisors tool in the Supabase MCP server, or in the dashboard under Advisors, Security Advisor.