Guides / Function Search Path Mutable

last checked oct 2026

Supabase "Function Search Path Mutable" warning: what it means and how to fix it

You open the Security Advisor and there's a yellow Function Search Path Mutable warning on handle_new_user, update_updated_at or some other function your AI tool wrote for you. It's one of the most common warnings on Supabase projects, and the fix is usually one line. The catch is that the one line can break the function if you don't also change what's inside it.

The short answer

The warning (lint 0011_function_search_path_mutable, level WARN) means the function doesn't pin its search_path, so it looks up unqualified names like profiles using whatever search path the caller's session has. Supabase recommends fixing it like this:

How urgent is it? On its own, it's hardening, not an open door. It matters most on security definer functions, which run with their owner's privileges. Postgres's own docs say a security definer function's search_path should exclude any schema untrusted users can write to, so nobody can plant an object that masks the one the function meant to use. Supabase's docs put it plainly: "When you use security definer, you must set the search_path."

Why the search path matters

When a function says insert into profiles, Postgres has to decide which profiles that is. It walks the schemas in search_path in order and uses the first match. The default is "$user", public, and a session can change it. A function without its own setting inherits the caller's, so the same function can resolve names differently depending on who calls it.

A few details from the Postgres docs make this sharper:

How to check

1. The Security Advisor

Open the Security Advisor in the Supabase dashboard. Each Function Search Path Mutable row names one function. Supabase also exposes the same checks through the CLI (supabase db advisors), the Management API, and the MCP server's get_advisors tool.

2. Find them with SQL

Run this in the SQL Editor. It reads only the system catalog, lists functions in public with no pinned search_path, skips functions that belong to extensions, and shows which ones are security definer so you can fix those first:

select
  n.nspname as schema,
  p.proname as function,
  pg_catalog.pg_get_function_identity_arguments(p.oid) as args,
  p.prosecdef as security_definer,
  pg_catalog.pg_get_userbyid(p.proowner) as owner
from pg_catalog.pg_proc p
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'            -- add your own schemas here
  and p.prokind = 'f'
  and not exists (
    select 1 from pg_catalog.pg_depend d
    where d.objid = p.oid and d.deptype = 'e'  -- skip extension functions
  )
  and not exists (
    select 1 from pg_catalog.unnest(coalesce(p.proconfig, '{}')) as c
    where c like 'search_path=%'
  )
order by p.prosecdef desc, p.proname;

3. See who can call them

For each security definer row, check whether the API roles can run it. Replace the function signature with one from the query above:

select
  pg_catalog.has_function_privilege('anon', 'public.handle_new_user()', 'execute') as anon_can_call,
  pg_catalog.has_function_privilege('authenticated', 'public.handle_new_user()', 'execute') as authenticated_can_call;

If anon_can_call is true for a function that reads or writes private data, that's a bigger problem than the search path. Supabase flags it separately as lints 0028 and 0029, and the fix is in the last step below.

How to fix it

Pin the search path and qualify every name

The classic case is the signup trigger that copies new users into a profiles table. Here's the version that trips the warning:

create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer
as $$
begin
  insert into profiles (id, full_name)
  values (new.id, new.raw_user_meta_data ->> 'full_name');
  return new;
end;
$$;

And the fixed version. Two changes: set search_path = '', and public.profiles instead of profiles:

create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
  insert into public.profiles (id, full_name)
  values (new.id, new.raw_user_meta_data ->> 'full_name');
  return new;
end;
$$;

create or replace keeps the existing trigger attached, as long as the function name, arguments and return type stay the same.

Don't skip the qualifying step. If you only add set search_path = '', the function still saves fine, because PL/pgSQL looks up table names when it runs, not when it's created. The first real signup then fails with relation "profiles" does not exist. Triggers on auth.users run in the same transaction as the signup, so Supabase warns that an error there can block signups. Sign up a test user right after you change one.

Things that need a schema prefix

If qualifying everything isn't practical, for example a function that leans on an extension's operators, Postgres's docs describe the alternative: list only schemas untrusted users can't write to, with pg_temp last, such as set search_path = extensions, pg_temp. Supabase's recommended default is still the empty path.

Fix a function without rewriting it

If the body already uses qualified names, you can pin the setting without touching the code:

alter function public.update_updated_at() set search_path = '';

To generate those statements for everything the check query found, run this. It only prints the statements. Read each function's body first, then run the ones you've checked:

select format('alter function %I.%I(%s) set search_path = %L;',
              n.nspname, p.proname,
              pg_catalog.pg_get_function_identity_arguments(p.oid), '')
from pg_catalog.pg_proc p
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
  and p.prokind = 'f'
  and not exists (select 1 from pg_catalog.pg_depend d
                  where d.objid = p.oid and d.deptype = 'e')
  and not exists (select 1 from pg_catalog.unnest(coalesce(p.proconfig, '{}')) as c
                  where c like 'search_path=%');

To read a function's current body before you change it: select pg_catalog.pg_get_functiondef('public.handle_new_user()'::regprocedure);

Then close the bigger hole: who can call it

Pinning the search path doesn't change who can run the function. Supabase's docs are explicit about that. For a security definer function that the API shouldn't call directly, such as a trigger function, revoke EXECUTE:

revoke execute on function public.handle_new_user() from public, anon, authenticated;

Then sign up a test user to confirm the trigger still fires. For a function your app does call over RPC, either switch it to security invoker so RLS applies (alter function public.my_fn() security invoker;), or keep it as definer, grant EXECUTE only to the roles that need it, and check ownership inside the body.

Confirm it's gone

Run the check query again. It should return no rows for the functions you fixed. Then re-run the Security Advisor. Finally, run through whatever the function powers: a test signup for an auth trigger, or a save for an updated_at trigger.

next step

Check your live app for free

Our free Leak Checker looks at your deployed app the way any visitor's browser does and flags exposed secret keys, public source maps and missing security headers. It can't see inside your database, so it won't catch this warning, but it's a quick first pass on what your frontend gives away.

If you want the database side checked too, the $99 Scan + Report covers your repo and live app, including database access rules, leaked keys and deploy settings.

Sources

Every product step above was checked against these official pages. Last checked Oct 2026. Products change; if a setting moved, the docs below are the source of truth.