How to revoke execute on a Postgres function
Revoking execute from anon alone doesn't stop a function being called through the Data API. How Postgres and Supabase grant EXECUTE by default, and how to lock a function down.
You ran revoke execute on function ... from anon, yet the function can still be called with the anon key through supabase.rpc(). That's because the grant that lets anon call it usually isn't a grant to anon.
Where EXECUTE comes from
Two separate defaults give callers access to a new function:
- Postgres grants EXECUTE to
PUBLICon every new function.PUBLICmeans every role, includinganonandauthenticated. Revoking fromanondoesn't touch this grant. - Supabase's default privileges in the
publicschema also grant EXECUTE on new functions toanon,authenticatedandservice_roledirectly.
And because public is an exposed schema, any function there that a role can execute is reachable at /rest/v1/rpc/<name>.
Lock down one function
Revoke from PUBLIC and the API roles together, then grant back only what you want. Include the argument types, since Postgres identifies functions by name and signature:
revoke execute on function public.approve_booking(uuid) from public, anon, authenticated;
-- only signed-in users may call it
grant execute on function public.approve_booking(uuid) to authenticated;
Check the result:
select
has_function_privilege('anon', 'public.approve_booking(uuid)', 'execute') as anon,
has_function_privilege('authenticated', 'public.approve_booking(uuid)', 'execute') as authenticated;
Through the API, a caller without EXECUTE now gets a 42501 permission error instead of a result.
create or replace function keeps the existing grants, but drop function followed by create function starts over with the defaults. Put the revoke in the same migration as the create.
Change the defaults for new functions
To stop new functions being callable by everyone, change the default privileges for the role that creates them (postgres when you use Studio or migrations):
-- the built-in PUBLIC grant can only be removed globally, not per schema
alter default privileges for role postgres
revoke execute on functions from public;
-- the Supabase grants to the API roles in the public schema
alter default privileges for role postgres in schema public
revoke execute on functions from anon, authenticated;
After this, every new function needs an explicit grant execute. Functions that already exist keep their current grants.
Default privileges are stored per database, so this affects only the project you run it in.
Keep internal functions out of the API
A function that should never be called over HTTP doesn't belong in public. Put it in a schema that isn't exposed:
create schema if not exists private;
create function private.recalculate_availability(room_id bigint)
returns void
language sql
set search_path = ''
as $$
update public.rooms
set free_slots = (select count(*) from public.slots s where s.room_id = recalculate_availability.room_id and s.booked_by is null)
where id = recalculate_availability.room_id;
$$;
Triggers, cron jobs and other functions can still call it. If an RLS policy calls it, the calling role needs usage on the schema and execute on the function; see security definer functions and RLS.
On SupaHost
The Data API's exposed schemas are fixed (public, storage, graphql_public, pgmq_public), so any other schema you create, like private above, is never reachable over HTTP. The MCP server's get_advisors tool also flags functions in public with a mutable search_path.