Skip to content

Supabase's October 30 change: new public tables need a grant for Data API access. What to doSupabase's October 30 change is live: new public tables need a grant for Data API access. What broke and how to fix it

24 tools · 139 fixes · 42 issue guides · Sources: Supabase, GitHub, postgresql.org, Vercel + 53

Issues

Can anyone call your Supabase functions?

Yes. By default even anon can call a function in public at /rest/v1/rpc; postgres-owned security definer ones skip RLS. Revoke EXECUTE from PUBLIC and anon.

Share link · Share on X

By ShipSecurely Editorial · Updated · How we make these guides

Shows up in: Supabase, Postgres

What it is

Yes, by default. Any role, including anon and authenticated, can call a database function in public over the Data API at POST /rest/v1/rpc/<name>, and the October 30, 2026 change does not close that for functions. A security definer function owned by postgres also bypasses row level security, so it can return rows the caller could never select.

Supabase's lint docs describe how this happens. Someone writes a helper function, such as a role check, a counter, or an admin helper, meant to be called from an admin script. They do not think about the API surface, and the function becomes a public endpoint. PostgREST exposes it at /rest/v1/rpc/<name> automatically. The function does not need to appear anywhere in your documented API, because /rest/v1/rpc accepts any function name the calling role has EXECUTE on.

Can anyone call my Supabase functions?

Yes, unless you revoked it. Supabase's Database functions guide says that by default any role can run a database function, and that you call one from your app with rpc(). Lint 0028 says that if anon has EXECUTE on a security definer function, anyone with the public anon key can call it through POST /rest/v1/rpc/<name>.

Two defaults stack. PostgreSQL grants EXECUTE on a new function to PUBLIC, and a grant to PUBLIC goes to every role on the system. Securing your API says that on existing projects, new functions in public also get execute for anon, authenticated, and service_role through default privileges.

The function's security mode decides whose privileges the body runs with. CREATE FUNCTION says SECURITY INVOKER, the default, runs as the caller, and SECURITY DEFINER runs as the owner. The Database functions guide's warning is direct: a security definer function can return rows the caller isn't allowed to read. A function created in the Dashboard or by a migration is owned by postgres, which bypasses row level security, and any role can call it by default, including anon.

If a function should not be on the API for everyone, revoke EXECUTE from PUBLIC and from the client roles, then grant it back only where it belongs:

-- Use the name and argument types from the owner query further down
revoke execute on function public.your_function(uuid) from public, anon, authenticated;

-- Grant it back only to the roles that should call it, for example signed-in users
grant execute on function public.your_function(uuid) to authenticated;

Leave out the grant when no browser role should call the function. Revoking from anon alone is not enough. The lint docs say to revoke from PUBLIC as well, because the Postgres default grant lives there, and that revoking from anon alone usually leaves the function callable by every signed-up user.

As of October 21, 2026 (Pacific time), two snippets in lint 0028's Quick Reference table leave PUBLIC's execute in place, so anon can still call the functions they target. revoke execute on all functions in schema public from anon; removes only the grant to anon, so a function that still has the default grant to PUBLIC stays callable. alter default privileges in schema public revoke execute on functions from anon, public; is limited to one schema. The Database functions guide says a revoke limited to one schema has no effect on a global default, and Securing your API says Postgres grants execute to PUBLIC on new functions through a global default. Use the revoke block above for existing functions, and the statements in the next section for new ones.

Why doesn't the October 30 change cover functions?

The October 30 changelog is about tables. It names October 30, 2026 as the day the new behavior is enforced on existing projects, and its opt-in SQL revokes default privileges on tables and sequences. It has no statement for functions. Grants for new tables are in Supabase's Oct 30 change: new tables aren't exposed automatically.

Securing your API lists what stays in place when you disable Automatically expose new tables in the Data API settings. For functions it says: "New functions are callable by every role. Postgres grants execute to PUBLIC through a global default, and a revoke limited to one schema can't remove it." The same page's default privileges query leaves out that PUBLIC grant, so an empty result there does not mean new functions are locked.

To make new tables, sequences, and functions in public wait for an explicit grant, that guide runs these statements in the SQL editor:

alter default privileges for role postgres in schema public
  revoke all on tables from anon, authenticated, service_role;

alter default privileges for role postgres in schema public
  revoke all on sequences from anon, authenticated, service_role;

alter default privileges for role postgres in schema public
  revoke all on functions from anon, authenticated, service_role;

alter default privileges for role postgres
  revoke execute on functions from public;

The last statement has no in schema, on purpose. The Database functions guide says a revoke limited to one schema has no effect on the global default. Securing your API warns that the last statement also restricts functions postgres creates in other schemas, including helper functions your row level security policies call. Grant execute on each of those to the roles that call them, or the policies that use them fail.

Default privileges only change functions created later. The Database functions guide restricts the functions you already have with a separate revoke on every function in the schema, from both public and the roles you want to restrict:

revoke execute on all functions in schema public from public;
revoke execute on all functions in schema public from anon, authenticated;

That revokes every function in public, including ones your app or a policy calls on purpose. Grant those back one by one before you deploy.

Which of my functions can anon call?

Lint 0028 and lint 0029 give this read-only query to see who each security definer function in public runs as:

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;

The lint docs say that when the owner is a high-privilege role, the function can read and write everything that role can. args is the argument list to paste into revoke execute on function public.name(args).

This read-only query adds who can call each function in public. It uses has_function_privilege, which reports whether a role holds EXECUTE. A grant to PUBLIC counts, because that grant goes to every role.

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,
    has_function_privilege('anon', p.oid, 'EXECUTE') as anon_execute,
    has_function_privilege('authenticated', p.oid, 'EXECUTE') as authenticated_execute
from pg_catalog.pg_proc p
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
order by p.prosecdef desc, p.proname;

A row with security_definer true and anon_execute true is what lint 0028 reports. With authenticated_execute true it is lint 0029. A security invoker row with anon_execute true is still callable, but it runs as the caller, so row level security applies to the tables it touches. The lint docs leave those to the table lints, 0008_rls_enabled_no_policy and 0013_rls_disabled_in_public.

How to check in 5 minutes

  1. Open Security Advisor for the project. Look for lint 0028, "This SECURITY DEFINER function is callable without signing in.", and lint 0029, "This SECURITY DEFINER function is callable by signed-in users."

  2. In the SQL editor, run the owner query above. Every row is a security definer function in public. An owner of postgres means the function runs with that role's privileges and skips row level security.

  3. Run the second query. Write down every row where security_definer is true and anon_execute or authenticated_execute is true.

  4. For each one, search the app for rpc('function_name' and decide who should call it: signed-out visitors, signed-in users, only server code, or only a policy.

  5. Check whether a policy calls it. A helper that a policy runs for authenticated needs execute for authenticated, so move it to a private schema instead of revoking it.

How to fix

Supabase

Fix each function on its own. The lint docs give three options, and say to handle anon and authenticated together because the two lints are paired.

  1. Revoke EXECUTE. Use the revoke block in the full guide. Include public. If server code calls the function with the secret key, which is the service_role role, keep or add grant execute on function public.your_function(uuid) to service_role;.
  2. Switch to security invoker when the function does not actually need to bypass row level security, for example when it was declared security definer by habit. It still runs, as the caller, and row level security on the tables it touches takes effect: alter function public.your_function(uuid) security invoker;
  3. Keep it exposed on purpose. The lint docs name a contact form that inserts into a table the caller cannot write to, and a public RPC that returns the count of public posts. Validate inputs and limit what the function does. The docs say a security definer function exposed to anon is effectively a public API endpoint.

The lint text also lists a fourth way out: move the function out of your exposed API schema. That is the shape for a helper that a policy calls. The Row Level Security guide says never to create a security definer function in a schema listed under Exposed schemas, and its example puts the helper in private:

create schema if not exists private;

create function private.user_list_ids()
returns setof uuid
language sql
security definer
set search_path = ''
stable
as $$
  select list_id from public.list_members
  where user_id = (select auth.uid())
$$;

revoke execute on function private.user_list_ids() from public;
grant usage on schema private to authenticated;
grant execute on function private.user_list_ids() to authenticated;

Policies that run for authenticated can still call private.user_list_ids(). Securing your API describes the split: objects in an exposed schema define the API surface, and internal tables and helper functions stay in schemas that aren't exposed. The function filters on (select auth.uid()), so it only returns the caller's lists.

When a function has to stay security definer, the Database functions guide asks for three things. Set search_path = '' and schema-qualify every name in the body, such as public.profiles. Check ownership in the body: an execute grant decides which roles can call the function, and it does not limit the rows a permitted role gets back, so a grant to authenticated still returns any user's row to every signed-in caller. Narrow EXECUTE too. The same guide says pinning the search_path doesn't change who can call the function, and lint 0011, "Unsecured function search path", is a separate warning from 0028 and 0029.

begin;

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

revoke execute on function public.get_my_profile() from public, anon;
grant execute on function public.get_my_profile() to authenticated;

commit;

The where clause is the ownership check. It assumes profiles.id is the user's id. Postgres's CREATE FUNCTION page sets the privileges in the same transaction as the create, so there is no window where the new function is open to everyone.

Postgres

CREATE FUNCTION has a section on writing SECURITY DEFINER functions safely. It says search_path should exclude any schema writable by untrusted users, so nobody can create an object that masks one the function uses, and it notes that execute privilege is granted to PUBLIC for new functions by default. To restrict a SECURITY DEFINER function to some users, it revokes the default PUBLIC privileges, then grants EXECUTE selectively, inside the transaction that creates the function. PostgreSQL privileges lists EXECUTE on functions and procedures among the privileges PUBLIC gets by default.

Does turning off pg_graphql close the endpoint?

No. The lint docs say disabling pg_graphql closes the /graphql/v1 query and mutation surface, which is one of two ways the function is reachable. The function is still callable through PostgREST at /rest/v1/rpc/<name>, the lint keeps firing, and the remediation above is still required. Disable pg_graphql only when the app does not use GraphQL, and do not count it as the fix.

What about views?

Views are a separate lint. Lint 0010, "View bypasses row-level security", says Postgres views default to security definer and use the permissions of the view's creator. Lint 0002 says a public security definer view over auth.users exposes all user records to all API users. Lint 0010's fix for a view that should respect row level security is with (security_invoker=on). To check your views, see Do Supabase views bypass RLS?.

Copy the fix prompt

Paste this into Cursor, Lovable, Bolt, or Claude Code. Read the diff before you merge it.

You are auditing who can call the database functions in this Supabase project. Do not add features. Do not run anything against my database without asking me first.

1. List every function in the public schema, from the migrations in this repo and from the owner query I paste back. For each one, record its argument types, whether it is security definer or security invoker, and its owner.
2. Search the app for rpc(' calls and for row level security policies that call a function. For each function, say whether anon (signed-out visitors), authenticated (signed-in users), service_role (server code with the secret key), or only a policy should call it.
3. Write one migration. For every function that anon or authenticated should not call, revoke execute from public and from those roles in the same statement. Then grant execute only to the roles that need it. Never revoke from anon alone.
4. For every security definer function that stays, set search_path = '' and schema-qualify every name in the body. If it reads per-user data, add an ownership check such as where user_id = (select auth.uid()). A search_path change does not change who can call the function.
5. If a security definer function only exists for a policy, move it to a private schema that is not exposed, revoke execute from public, and grant usage on the schema and execute on the function to the role the policy runs for.
6. If a function does not need to bypass row level security, switch it to security invoker instead.
7. Do not treat disabling pg_graphql as a fix. The function stays callable at /rest/v1/rpc.

Output the SQL migration, then a table with one row per function: name and arguments, security mode, owner, who calls it, and the change you made.

Questions

Does RLS protect my RPC functions?

Not when the function is security definer and its owner bypasses row level security, as postgres does. Securing your API says row level security doesn't apply to functions, so execute goes only to the roles that need to call the function, and every security definer function needs review. The Row Level Security guide says a security definer function only skips row level security when its owner can. A function created in the Dashboard or by a migration is owned by postgres, which bypasses row level security. Lint 0028 says that if the owner has no more privileges than the caller, SECURITY DEFINER does not actually escalate. A security invoker function runs as the caller, so row level security applies to the tables it touches, and the lint docs leave that case to the table lints.

Will the October 30 change lock down my functions?

No. Securing your API says new functions are callable by every role, because Postgres grants execute to PUBLIC through a global default that a revoke limited to one schema can't remove. The October 30 opt-in SQL in the changelog covers tables and sequences only. Revoke EXECUTE on the functions you have, and use the alter default privileges statements above for future ones.

Is calling rpc() from the browser with the anon key safe?

Only for a function anon is meant to call. The publishable key with no user session is the anon role, per Supabase's API keys guide, so anyone with that public key can call whatever anon can call. Lint 0028 treats a security definer function exposed to anon as effectively a public API endpoint: it should validate its inputs and limit what it does. Everything else gets revoke execute ... from public, anon.

Sources

Related

Need someone to fix it for you? See /help.

The weekly fix

The weekly fix: 3 real security mistakes in vibe-coded apps and how to fix them, every Tuesday.

One email, every Tuesday. A welcome note follows the first signup.