Blog · October 30, 2026 · 9 min read

Supabase Security Advisor warnings, explained and fixed

What each Supabase Security Advisor warning means, which to fix first, and the SQL to fix it: RLS disabled, security definer views and functions, and more.

You open your Supabase dashboard and the Security Advisor shows a list of red and yellow items with names like security_definer_view and function_search_path_mutable. If an AI tool like Lovable, Bolt or Cursor built your database, you probably didn't write any of the things it is warning about.

This guide translates the security warnings into plain English, tells you which ones mean "anyone can read your data right now" and which ones can wait, and gives you the fix for each. It takes about 30 minutes for a typical vibe-coded app.

Where to find the warnings

Supabase calls these checks Advisors. The same checks run in four places:

Each finding has a level: ERROR, WARN or INFO. Start with the errors. Supabase is clear that some findings can be intentional, so read each one against what your app is supposed to allow before you change anything.

Fix these first (ERROR)

"Table publicly accessible" (rls_disabled_in_public)

This is the big one. A table in the public schema has Row Level Security (RLS) turned off. Anyone who has your project URL and the public anon key, which is in your website's JavaScript by design, can read, insert, update and delete every row.

alter table public.your_table enable row level security;

Turning RLS on with no policies blocks all API access to the table, so your app may stop showing data. That is safer than the alternative, but you then need policies. A typical "users see only their own rows" policy:

create policy "own rows only"
on public.your_table
for select
to authenticated
using ((select auth.uid()) = user_id);

If RLS and policies are new to you, read Supabase RLS in plain English first. It explains what using and with check actually do.

"Sensitive data publicly accessible" (sensitive_columns_exposed)

The same problem as above, on a table whose column names look sensitive: things like password, token, api_key, ssn, iban or card_number. Same fix, but treat it as urgent and assume the data may already have been read. If the table holds API keys or tokens, rotate them.

"User data exposed through a view" (auth_users_exposed)

A view in public selects from auth.users, the table behind Supabase Auth with every user's email and login details. Views ignore RLS by default (more on that below), so this hands your user list to anyone with the anon key.

The usual fix is to drop the view and use a public.profiles table that holds only the columns your app needs, filled by a trigger when a user signs up, with its own RLS policies. Ask your AI tool for exactly that, and tell it not to expose auth.users directly.

"View bypasses row-level security" (security_definer_view)

This one confuses almost everyone. In Postgres, a view by default runs with the rights of the person who created it, not the person querying it. Views created in Supabase are usually owned by a powerful role that can bypass RLS. So you can have perfect policies on orders, and a view called order_summary that quietly shows every customer's orders to anyone.

The fix is one line per view:

alter view public.order_summary set (security_invoker = true);

After that, the view respects the RLS policies of the tables underneath it. If the view suddenly returns fewer rows, that is the policies working, not a bug.

"Security policy relies on user-editable data" (rls_references_user_metadata)

A policy checks something like auth.jwt() -> 'user_metadata' ->> 'is_admin'. User metadata is meant to be edited by the user. Anyone signed in can call updateUser with is_admin: true from the browser console and pass that check.

Move roles and plans into a separate table (for example user_roles) that users cannot write to, and check that table in your policies instead. This is the same "make me admin" problem we test for in Is my Lovable app secure?.

Then these (WARN)

"Security policy allows unrestricted access" (permissive_rls_policy)

RLS is on, but a policy says using (true) or with check (true). That means "everyone allowed". For a public blog or product catalog that's fine for reading. For anything with user data it means RLS is on in name only.

Look especially for with check (true) on insert and update policies. That lets a user write rows as someone else, or change columns like plan or credits on their own row. Replace it with a real condition such as (select auth.uid()) = user_id.

SECURITY DEFINER functions callable by anyone (anon_security_definer_function_executable and authenticated_security_definer_function_executable)

A database function marked SECURITY DEFINER runs as its owner, which usually skips RLS entirely. Supabase exposes functions in public at /rest/v1/rpc/function_name, and by default new functions can be called by anon and authenticated. So a helper your AI tool wrote "for the admin page" can be a public back door to every row.

List them, with their owner:

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

For each one, pick one of three fixes:

-- 1. Nobody outside should call it
revoke execute on function public.my_function(int) from anon, authenticated, public;

-- 2. It doesn't need to bypass RLS
alter function public.my_function(int) security invoker;

The third option is to keep it, because some functions are meant to be public, like a contact form that writes to a table users can't touch directly. Then the function itself must check its inputs and, where it matters, check auth.uid() before doing anything. Revoke from public as well as anon, because Postgres's own default grant lives on public. And note that the two warnings come as a pair: revoking from anon alone still leaves the function open to anyone who signs up.

"Unsecured function search path" (function_search_path_mutable)

The function doesn't pin which schemas it looks in, so a name like profiles could resolve to a different object depending on who calls it. It is rarely exploitable in a small app, but the fix is cheap: add set search_path = '' to the function and write table names in full, like public.profiles. Ask your AI tool to do this for every flagged function and to fully qualify the names.

"Materialized view exposed in API" (materialized_view_in_api)

Materialized views can't use RLS at all. If anon or authenticated can select from one, every row is visible. Unless it really is public data:

revoke select on public.my_stats from public, anon, authenticated;

"Public bucket allows listing" (public_bucket_allows_listing)

A public storage bucket already lets anyone open a file if they know its URL. If it also has a broad select policy, anyone can list every file name in it too, which makes guessing unnecessary. Usually you can just drop that select policy. If the bucket holds anything personal, it shouldn't be public in the first place. Our guide to checking your Supabase storage buckets walks through it.

GraphQL warnings (pg_graphql_anon_table_exposed and pg_graphql_authenticated_table_exposed)

If the pg_graphql extension is on, anyone who can select from a table can see its name and columns through /graphql/v1, even when RLS returns zero rows. It is a map of your database, not the data itself. If your app doesn't use GraphQL, and most vibe-coded apps don't, turn the extension off under Database, Extensions. Otherwise revoke access on tables nobody outside should know about.

Extension installed in public (extension_in_public)

Housekeeping: an extension's functions show up in your API. Move it with alter extension name set schema extensions;. Low risk, low effort.

Usually fine (INFO)

A prompt you can paste into your AI tool

Run the Supabase security advisors and list every ERROR and WARN finding. For each one, explain in one sentence what it allows and whether it is intentional for this app. Then: enable RLS on every public table and add owner-based policies using (select auth.uid()); set security_invoker = true on every view in public; replace any view on auth.users with a profiles table; remove user_metadata from all policies; replace using (true) and with check (true) on tables with user data; for each SECURITY DEFINER function, either revoke execute from anon, authenticated and public, or switch it to security invoker, or add an auth.uid() check; add set search_path = '' to flagged functions. Show me the SQL before running it, then run the advisors again.

Then run the advisors again yourself. An AI tool saying "fixed" and the warning being gone are two different things.

What the Security Advisor can't see

The advisor checks your database configuration, and it is very good at that. It doesn't look at your app:

Check it from the outside

The Security Advisor looks at your database from the inside. Greenlight Scan looks at your published app the way a stranger would: it reads the JavaScript your site ships, finds your Supabase project and public key, flags secret keys and exposed files, and, once you verify you own the site, tests whether your tables answer to the public key. It only counts rows and never downloads your data. Run the free scan for a traffic-light result in about a minute. The $19 Full Scan unlocks every finding with the evidence and a fix prompt for your AI tool, plus re-scans for 30 days.

If your app takes payments or stores personal data, some of the riskiest problems, like a SECURITY DEFINER function or server route that never checks who owns a record, live in code an outside scan can't see. The $79 Launch Audit adds a deep AI review of the code in your GitHub repo, every finding double-checked by a second AI, and a launch verdict. It is automated, not a pentest.

Newsletter

The Friday leak report

One short email a week: the newest ways AI-built apps leak data, and the one-line fix for each. No spam, unsubscribe any time.