Supabase SECURITY DEFINER: Lock Down Your Functions

A SECURITY DEFINER function runs as its owner, usually postgres, so it skips RLS, and anon can call it by default. Prefer SECURITY INVOKER, or set search_path = '' and revoke EXECUTE from anon.

Gary 路 Published 路 Updated 路 9 min read

Isometric illustration of a database cylinder with an open glowing side door being fitted with a padlock, beside a row of emerald security gates, on a dark slate background.
On this page
  1. What to do today (checklist)
  2. What does SECURITY DEFINER mean?
  3. Why does SECURITY DEFINER bypass RLS?
  4. How do I find SECURITY DEFINER functions in my project?
  5. How do I fix a SECURITY DEFINER function?
  6. Fix 1: Switch to SECURITY INVOKER
  7. Fix 2: Pin the search path
  8. Fix 3: Check the caller inside the function
  9. Fix 4: Revoke EXECUTE
  10. What is "function search_path mutable" in Supabase?
  11. Does RLS protect SECURITY DEFINER functions?
  12. When is SECURITY DEFINER the right choice?
  13. Does Lovable create SECURITY DEFINER functions?
  14. FAQ
  15. Check your app

A Supabase SECURITY DEFINER function runs with the privileges of the role that created it, not the person calling it. In Supabase, that's usually postgres, which bypasses Row Level Security. And by default, any role can call a function, including anon. So a definer function in your public schema can hand data to anyone with your public key. Here's how to find yours and lock them down.

By Gary, Greenlight My App 路 Updated Oct 29, 2026 路 How we checked this: we read Supabase's Database functions guide and its Advisors docs (lints 0011, 0028 and 0029) on Oct 29, 2026. We have not run these queries or fixes in a test project for this post yet, so treat them as untested by us. Drafted with AI tools and reviewed by a person before publishing.

What to do today (checklist)

List your SECURITY DEFINER functions, decide which really need it, and lock down the rest. Most fixes are one line of SQL each.

StepSQL or placeFixes
1. Open Security AdvisorSupabase StudioShows lints 0011, 0028, 0029
2. List definer functionsQuery belowFinds every one in public
3. Switch to invoker where you canalter function ... security invoker;RLS applies again
4. Pin the search pathset search_path = ''Lint 0011
5. Check the caller insideauth.uid()Per-user data
6. Revoke EXECUTErevoke execute ... from public, anon;Lints 0028, 0029

What does SECURITY DEFINER mean?

Postgres runs a function either as the user calling it (invoker) or as the user who created it (definer). security invoker is the default. security definer lets a low-privilege caller do something that needs more privileges, which is useful, but risky.

Supabase's Database functions guide (external) says to prefer security invoker. When you do use security definer, it says you must set the search_path.

A safe-looking definer function, written the way Supabase's docs show it:

SQL

create or replace function public.hello_world()
returns text
language plpgsql
security definer set search_path = ''
as $$
begin
  return 'hello world';
end;
$$;

That one is harmless because it doesn't touch any data. The trouble starts when a definer function reads or writes tables.

Why does SECURITY DEFINER bypass RLS?

Because the function runs as its owner, and in Supabase that's usually postgres, which bypasses Row Level Security. Supabase's docs warn that a definer function can return rows the caller isn't allowed to read.

Row Level Security checks who is running a query. Inside a definer function, the "who" is the owner, not your user. So your policies don't apply.

Supabase's Advisors docs (lint 0028) explain why this becomes a public hole:

  1. SECURITY DEFINER bypasses RLS when the owner is a privileged role.
  2. Postgres' default lets PUBLIC execute new functions, and Supabase also grants default privileges for new functions to anon, authenticated and service_role.

Put together, a helper function you wrote for an admin task can be called by anyone at /rest/v1/rpc/your_function. It doesn't need to appear anywhere in your app for the call to work.

security invokersecurity definer
Runs asThe callerThe owner (usually postgres)
RLS applies?YesNo, if the owner bypasses RLS
Default in PostgresYesNo
Who can call it by defaultAny roleAny role

Source: Supabase Database functions and Advisors docs, read Oct 29, 2026.

How do I find SECURITY DEFINER functions in my project?

Run a query on pg_proc for functions where prosecdef is true. Supabase's Advisors docs give this query, which also shows each function's owner. Or open the Security Advisor in Studio and look for lints 0028 and 0029.

SQL

select
    p.proname,
    pg_get_function_identity_arguments(p.oid) as args,
    pg_catalog.pg_get_userbyid(p.proowner) as owner,
    p.prosecdef as security_definer
from pg_catalog.pg_proc p
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
  and p.prosecdef
order by p.proname;

Run it in the SQL Editor. For each row, ask: does this function really need to run as its owner? If the owner is a high-privilege role, the docs say the function can read and write everything that role can.

You can also run the advisors from the command line with supabase db advisors, according to the docs.

How do I fix a SECURITY DEFINER function?

Work through four fixes. Switch to security invoker if the function doesn't truly need extra privileges. If it does, pin search_path, check the caller inside the function, and narrow who can execute it.

Fix 1: Switch to SECURITY INVOKER

Many functions are marked definer by habit. If yours doesn't need to bypass RLS, switch it:

SQL

alter function public.get_my_orders() security invoker;

Now RLS on the underlying tables applies again.

Fix 2: Pin the search path

Supabase's lint 0011 ("function search path mutable") recommends search_path = ''. This forces every table name inside the function to include its schema, like public.orders:

SQL

create or replace function public.get_my_orders()
returns setof public.orders
language sql
security definer
set search_path = ''
as $$
  select * from public.orders
  where user_id = (select auth.uid());
$$;

Without a fixed path, the docs warn that a malicious user could point the function at objects they control.

Fix 3: Check the caller inside the function

Supabase's docs say an execute grant controls which roles can call a function, not which rows they get back. Granting to authenticated still returns any user's row to every signed-in caller, unless the function checks. The where user_id = (select auth.uid()) line above is that check.

Fix 4: Revoke EXECUTE

Revoke from both public and anon, then grant to the roles that need it:

SQL

revoke execute on function public.get_my_orders from public;
revoke execute on function public.get_my_orders from anon;
grant execute on function public.get_my_orders to authenticated;

Supabase's docs also show how to restrict all future functions by default. Note that one statement must be global, without in schema, because Postgres grants execute to PUBLIC through a global default:

SQL

alter default privileges revoke execute on functions from public;
alter default privileges in schema public revoke execute on functions from anon, authenticated;

If you do this, remember to grant execute on helper functions your RLS policies call.

Our GRANT guide (external) covers how grants work for tables too.

Want to check what your live app exposes? Run Greenlight My App's free scan on your live URL.

What is "function search_path mutable" in Supabase?

It's Supabase's lint 0011. It means a function doesn't have a fixed search_path, so it uses the caller's session path. That can make it behave differently for different users, and could let someone redirect it to objects they control.

The fix is to add set search_path = '' to the function and use schema-qualified names, like public.profiles, inside it. The PostgreSQL CREATE FUNCTION docs (external) cover the SET clause in detail.

Does RLS protect SECURITY DEFINER functions?

No, not when the owner bypasses RLS, which is the usual case in Supabase. That's the point of this post: RLS can be perfect on every table and a definer function can still return every row. Our Postgres RLS guide (external) explains what RLS does cover.

RLS and grants are separate layers, too. Our GRANT vs RLS guide (external) explains how they fit. Definer functions are a third thing to check.

When is SECURITY DEFINER the right choice?

When a function genuinely needs to do something the caller can't, and it limits what it does. Supabase's docs give examples: a contact form that inserts into a table the caller can't write to, or a function that creates a user's profile.

Common safe uses:

  • Triggers on auth.users that copy a new user into public.profiles.
  • A "submit feedback" function that writes one row.
  • A count of public posts.

For each one, the docs say to validate inputs and limit what it does. A definer function that anon can execute is effectively a public API endpoint.

Does Lovable create SECURITY DEFINER functions?

We haven't tested it. AI builders often generate SQL for you, and definer functions are a common pattern for things like profile triggers. Run the query above on your project and check each result.

If you find one you didn't expect, don't delete it straight away. It may power sign-up or another feature. Apply the fixes above and test your app afterwards.

FAQ

Does RLS protect definer functions? No, not when the owner bypasses RLS, which is usual in Supabase. The function runs as its owner, so your policies don't apply. Check the caller inside the function and restrict who can execute it.

What is search_path mutable? It's Supabase's lint 0011. The function has no fixed search_path, so it uses the caller's. Add set search_path = '' and use schema-qualified names like public.orders inside the function.

Should I revoke from anon? Usually, yes. Revoke execute from both public and anon unless the function is meant to be a public endpoint. Then grant it to the roles that need it, such as authenticated.

Does Lovable create these? We haven't tested it. Run the pg_proc query in this post on your project, or open the Security Advisor, and review each definer function you find.

Is SECURITY DEFINER ever safe? Yes, when it's needed and narrow. Pin the search path, check the caller, validate inputs and limit execute rights. Supabase's docs list triggers and small write functions as typical uses.

Check your app

Database functions are one layer. Keys in your frontend are another. Run Greenlight My App's free scan on your live URL. It checks your homepage and up to five API paths it finds in your homepage and up to 15 of the scripts it loads for secret keys and other common problems. The scan is automated, read-only and educational, not a security audit, and "nothing found" isn't a guarantee that nothing is exposed.

Sources

On this site

Related posts

Greenlight My App is independent and not affiliated with or endorsed by Supabase, the PostgreSQL Global Development Group or Lovable. Product names are trademarks of their owners. Supabase facts are paraphrased from its Database functions and Advisors docs as read on Oct 29, 2026. We have not run these queries or fixes in a test project for this post yet; they are untested by us. Test changes on a copy of your database first. This page is general information, not professional security advice, legal advice, a security audit or a guarantee. Check your own setup against the vendor's current docs.