VibeRaven

In 47% of 512 Lovable Apps, the Anon Key Can Call a Security Definer Function

By Ohad Krispin, founder of VibeRaven · LinkedIn

Nearly half of the Lovable projects we read ship a Postgres function that skips RLS and that anyone with the public anon key can call. We read the functions in 18 of them to see what a signed-out caller could actually do.

Short answer

In a static read of 512 public Lovable projects on GitHub with a supabase/migrations folder (October 2026), 47.3% (242) define at least one security definer function in the public schema that a signed-out caller with the anon key can run at /rest/v1/rpc. We counted only functions from migrations dated before May 30, 2026, when Supabase began changing default grants for new projects. In 18 of these repos we read the functions by hand: in 7, a signed-out caller could create or change rows that belong to other users or to the app, and in only 2 did the function check the caller first, so that a signed-out call did nothing. The repos were read, not the live databases.

What we found

Out of 512 public Lovable projects with their own Supabase migrations, counting functions from migrations dated before May 30, 2026:

  • 47.3% (242) have at least one security definer function in public that no migration closes to anon. Trigger functions are not counted, because nothing can call them over the API.
  • 13.1% (67) have only role-check helpers such as has_role or is_admin. With one, a signed-out caller can ask whether a given user id is an admin. has_role alone appears in 117 repos.
  • 34.2% (175) have at least one function that is not a role check.
  • At least 7.8% (40) grant EXECUTE on such a function to authenticated and never revoke it from anon, so signed-out callers can still run it.
  • At least 2.5% (13) revoke EXECUTE from PUBLIC and stop there, which leaves the separate grant Supabase gives anon.

What a signed-out caller can do: 18 repos read by hand

A function anyone can call is a problem only if it does something for a caller who is not signed in. Inside a security definer function, auth.uid() is null for that caller, so a function that checks it first stops there.

We drew 20 repos at random from the 195 with a callable function other than a role check, cloned each at its last commit before the scan, and read every such function. One repo is no longer public. One had its functions in a migration written after May 30, 2026, so the repo alone could not tell us whether anon holds a grant. That left 18. Each repo is counted once, in the first row it fits.

One check that looks safe is not. A function guarded with IF p_user_id != auth.uid() THEN RAISE lets a signed-out call through: auth.uid() is null, so the comparison is null, and plpgsql treats a null condition as false. In that repo the function went on to award a badge to any user who met its requirements. A second repo has a role check of the same shape. IF auth.uid() IS NULL THEN RAISE, which other repos in the sample use, does stop the call.

What a signed-out caller can do with the flagged functions, in 18 repos read by hand
What a signed-out caller can doReposExamples, paraphrased, projects withheld
Create or change rows that belong to other users or to the app7Update any user's profile by id. Issue certificates and badges to any user. Post a notification into any user's feed. Change any user's usage credits, where a negative amount refunds them. Set the admin password for a data import. Create active employee accounts. Mint discount codes for any owner.
Read data with no check in the function4Ad spend totals, judges' score statistics, a user id looked up by username, and an admin login check with its password written in the migration.
Only low-impact writes5Audit log rows, rate-limit counters, cleanup of expired rows, and badges a user already qualifies for.
Nothing2The function changes only the caller's own row, found by auth.uid().

A grant to authenticated takes nothing away

Two lines look like they close a function and do not. GRANT EXECUTE ... TO authenticated reads as "signed-in users only", but a grant only adds a role, and anon keeps the EXECUTE it got from Supabase's default privileges. In the hand-read sample, 3 of the 7 repos where a signed-out caller could change data had granted that same function to authenticated.

REVOKE EXECUTE ... FROM PUBLIC on its own removes the Postgres default and leaves the grants Supabase adds for anon and authenticated on functions in public. Supabase's Advisors docs revoke from anon and PUBLIC together for this reason. The statements below show both patterns on a made-up function.

-- reads as "signed-in users only"; anon can still call it
grant execute on function public.set_import_password(text) to authenticated;

-- reads as "server only"; anon and authenticated can still call it
revoke execute on function public.set_import_password(text) from public;
grant execute on function public.set_import_password(text) to service_role;

Method

This uses the dataset behind our post on RLS policies in the same apps: public GitHub repos whose package.json includes lovable-tagger, Lovable's build tag, found through GitHub code search and kept when they had a supabase/migrations folder, read with VibeRaven 1.6.1 on October 3, 2026.

The check replays the migrations in order and flags a security definer function in public, other than a trigger function, unless EXECUTE ends up revoked from public, anon and authenticated. We counted functions in supabase/migrations files dated before May 30, 2026.

Role-check helpers were sorted by name: has_, is_, can_ and a few similar prefixes. The hand reading was done on October 19, 2026.

What we did not measure

No database was queried and no function was called. Callable here means the migrations leave EXECUTE in place under Supabase's default grants. A grant changed in the dashboard would not show up, and a repo is not always what is live.

Supabase began changing default grants for new projects on May 30, 2026. Its changelog describes the change for tables, and we did not test what a new project grants on functions, so functions from later migrations are left out of every number above.

The scan lists up to 10 functions per repo. The 67 role-only repos are complete, but the grant-to-authenticated and revoke-from-PUBLIC counts come from the listed functions only, so the real numbers can be higher. Sorting by name is rough: a function named like a role check can do more.

Eighteen repos is a small sample. Read its split as examples of what these functions do, and do not scale it to all 242. The sample of 512 is not random either: it comes from GitHub search, and public repos lean toward side projects.

Check your project and close what anon should not call

Run this in the Supabase SQL editor. It lists the security definer functions in public and whether a signed-out or signed-in caller can run them. If only your server calls a function, revoke EXECUTE from public, anon and authenticated in one statement. A helper your policies call, such as has_role, needs a different fix: move it to a schema the API does not expose and keep EXECUTE for the roles whose policies use it. The guide below walks through both.

For AI-built apps on Vercel + Supabase, VibeRaven flags these functions in the migrations before deploy, with the file and line: npx -y viberaven check. It is advice, not a gate, and a repository check, not a live database test, so run the query above against the live project too.

select p.proname,
       pg_get_function_identity_arguments(p.oid) as args,
       has_function_privilege('anon', p.oid, 'execute') as anon_can_call,
       has_function_privilege('authenticated', p.oid, 'execute') as signed_in_can_call
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
  and p.prosecdef
  and p.prorettype <> 'pg_catalog.trigger'::regtype
order by p.proname;

-- a function only your server calls:
revoke execute on function public.your_function(uuid) from public, anon, authenticated;

Related guides

Sources

How to use this VibeRaven guide

In 47% of 512 Lovable Apps, the Anon Key Can Call a Security Definer Function is meant to help builders decide what still needs proof before an AI-built app is trusted by real users. Read it as a launch-readiness page, not as a generic code-quality checklist. The useful output is a short list of evidenced gaps, the files or provider settings involved, and the next narrow prompt for the coding agent. The list further down is for you to check yourself; it is not VibeRaven output.

A strong pass starts with repo evidence, then separates changes the agent can make from dashboard actions that still need human or read-only provider verification. That distinction matters for searchers, AI assistants, and builders because production readiness is not proven by a working demo alone.

Things to check yourself before launch (not VibeRaven output)

  • Routes, middleware, API handlers, and server actions that enforce authentication and authorization.
  • Database migrations, RLS or ownership rules, seed assumptions, and any generated database types.
  • Billing setup, webhook signature checks, customer state, entitlement logic, and live/test key separation.
  • Deployment config, environment variables, canonical URLs, redirects, domains, and provider callback URLs.
  • Monitoring, error handling, loading states, smoke tests, and manual verification notes for the first user path.

Is every security definer function a problem?

No. One that checks auth.uid() or a role before it touches anything does nothing for a signed-out caller, and a has_role helper that policies call needs EXECUTE for the roles whose policies use it. The risk is a function that takes a user id or record id as an argument and acts on it with no check.

Does revoke execute from public close a function on Supabase?

Not on its own. Supabase also grants EXECUTE on functions in public to anon and authenticated, so the revoke has to name public, anon and authenticated.

Were any functions called or databases accessed?

No. Only public code on GitHub was read. No function was called, no database was queried and no project is named.