---
title: "Supabase \"function search path mutable\": what it means"
description: "What the Supabase Advisor warning function_search_path_mutable means, who can exploit it, why search_path = public was not enough in my test, and the fix."
url: "https://madsalt.tech/notes/supabase-function-search-path-mutable/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/about/"
language: "en"
published: 2026-11-01
---

# Supabase "function search path mutable": what it means

TL;DR

The warning means a function has no fixed `search_path`, so every unqualified name inside it, like `admins`, is looked up the way the caller's session says. In my local test a `SECURITY DEFINER` function `is_admin()` returned `true` for a non-admin after that user created a temporary table called `admins`, and pinning `search_path = public` did not stop it. What worked: `set search_path = ''` with every table written as `public.admins`.

Supabase's Security Advisor lists this as lint 0011, "Function Search Path Mutable", level WARN, with the detail "Function `public.x` has a role mutable search_path". It flags every function without the setting, `SECURITY INVOKER` ones included, so on a project with many helpers the list gets long: one open-source project [fixed 99 of them in a single migration](https://github.com/bdougie/contributor.info/commit/3c73dda01df80faccb75278ec0f826a84e01a6d9).

## What does "role mutable search_path" mean?

`search_path` is the list of schemas Postgres searches for a name that has no schema in front of it. A function without its own setting uses the caller's list. Supabase's [lint page](https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable) recommends pinning it to an empty string, which forces every reference inside the function to be fully qualified.

For a `SECURITY DEFINER` function this matters most: it runs with its owner's rights, usually `postgres`, which bypasses RLS. If the caller can steer which `admins` it reads, the caller steers a check that runs as `postgres`.

## Can someone exploit it in my test?

On a local Postgres 17.10 I made a table `public.admins` with one admin, and four versions of the same function, `select exists (select 1 from admins where user_id = uid)`, all `SECURITY DEFINER`. Then I logged in as a low-privilege role that can run SQL and has the default right to create temporary tables, and created `pg_temp.admins` containing user A, who is not an admin:

![is_admin(A) after a temp table named admins. SECURITY DEFINER function, local Postgres 17.10. No search_path: pg_temp.admins → admins → temp → true; search_path = public: pg_temp.admins → admins → temp → true; search_path = '' + public.admins: pg_temp.admins → public.admins → false.](https://madsalt.tech/charts/en/supabase-function-search-path-mutable.svg)

*My reproduction on a local Postgres, 24 October 2026.*

| Function setting                                  | Lint condition | `is_admin(A)` after the temp table |
| ------------------------------------------------- | -------------- | ---------------------------------- |
| none                                              | flagged        | `true`                             |
| `set search_path = public`                        | passes         | `true`                             |
| `set search_path = ''`, body uses `public.admins` | passes         | `false`                            |
| `set search_path = public, pg_temp`               | passes         | `false`                            |

The second line is the one that surprised me. A Supabase maintainer [wrote in 2024](https://github.com/supabase/splinter/issues/100) that a fixed non-empty path carries no security risk, and the lint accepts it. But Postgres searches the temporary schema first unless `pg_temp` is named in the path, as its [docs on writing SECURITY DEFINER functions](https://www.postgresql.org/docs/current/sql-createfunction.html#SQL-CREATEFUNCTION-SECURITY) warn. So `search_path = public` still lets a temporary table shadow an unqualified name.

## Who can actually do this on Supabase?

Not a visitor through your app. The Data API cannot create temporary tables or change the session's `search_path`, so the publishable key alone does not get there. It needs a direct database connection: a second service with its own login, a BI or reporting tool, a contractor's read-only role, a leaked connection string. On most small apps the risk is low; on any project where people or tools other than you hold database logins, fix the definer functions first.

## How do you fix it?

Recreate the function with an empty path and schema names in the body:

```sql
create or replace function public.is_admin(uid uuid)
returns boolean
language sql stable security definer
set search_path = ''
as $$ select exists (select 1 from public.admins where user_id = uid) $$;

revoke execute on function public.is_admin(uuid) from public, anon;
```

The `revoke` matters as much as the path: a definer function in `public` is callable through the Data API, so without it any guest can ask `/rpc/is_admin` whether a given user is an admin. Grant it back only to the roles that need it, or keep helpers like this in a schema the API does not expose.

Do not run only `alter function public.is_admin(uuid) set search_path = ''`. It silences the Advisor and breaks the function: on my run the next call failed with `42P01: relation "admins" does not exist`. Extension operators need their schema too, such as `operator(extensions.<=>)` for pgvector.

To list what the lint will flag in `public`, with definer functions first:

```sql
select p.proname, p.prosecdef as security_definer
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
  and not exists (select 1 from unnest(coalesce(p.proconfig, '{}')) c
                  where c like 'search_path=%')
order by p.prosecdef desc, p.proname;
```

## Does the fix cost anything?

Sometimes. Any `set` clause stops Postgres from inlining a plain SQL function into the query that calls it, as [reported on GitHub](https://github.com/supabase/supabase/issues/33131). I measured both shapes on 200,000 rows with an index on the filtered column (median of 7 runs):

| SQL function, then filtered by owner                | No `set`             | `set search_path = ''` |
| --------------------------------------------------- | -------------------- | ---------------------- |
| Filter outside: `visible_items() where owner = ...` | 0.053 ms, index used | 36.7 ms, full scan     |
| Filter inside: `items_of(owner)`                    | 0.056 ms             | 0.082 ms               |

If a set-returning SQL function is queried with a filter outside it, move the filter in as a parameter before you pin the path.

## Check your project

- In the Advisor, fix the `SECURITY DEFINER` functions on the list first; invoker functions run with the caller's rights anyway.
- The free [Supabase security check](https://madsalt.tech/tools/supabase-security-check/) shows Advisor findings next to its own rules, including definer functions callable through the API and definer functions that check `current_user`.
- More on what definer functions can skip is in [a NULL in a PL/pgSQL IF NOT skipped the check](https://madsalt.tech/notes/plpgsql-if-not-null-fails-open/), and policies that call such helpers are in [Supabase RLS policy examples](https://madsalt.tech/blog/supabase-rls-policy-examples/).
