Supabase security definer view warning: how to fix it
The warning means that a view in your public schema runs with the permissions of its owner and ignores the Row Level Security (RLS) policies of the tables that it reads. Fix it with alter view public.your_view set (security_invoker = on); and test the view as an anonymous user. The change makes the view use the permissions of the person who calls it. This guide shows how to confirm the problem, fix every view, and check if data was exposed.
What does the security definer view warning mean?
The warning comes from the Supabase database linter. It is lint 0010_security_definer_view. The linter documentation gives it the level ERROR. It describes the problem as “View bypasses row-level security”.
Postgres views behave like this by default. A view reads its base tables with the permissions of the view owner, not the permissions of the user who queries the view. The Postgres documentation for CREATE VIEW states this rule. It also says that RLS policies of the view owner apply to the base tables in this case.
In Supabase, the view owner is usually the postgres role. This role bypasses RLS. A user who queries the view therefore sees rows that the RLS policies of the table would hide.
The Supabase Data API makes this problem worse. The API exposes every table and every view in the public schema. An anonymous visitor with your public anon key can call the view with one HTTP request. If the view returns private rows, the visitor reads them.
Here is an example. You enable RLS on the orders table so that users see only their own orders. Then an AI tool creates a view for a dashboard:
create view public.order_summary as
select user_id, email, total, created_at
from public.orders;
The view reads orders as postgres. The policies of orders do not apply. Every caller of order_summary can read the orders of all users.
How do I confirm that a view is exposed?
Use the linter first. Then test the view yourself.
- Open the Supabase dashboard.
- Open the Advisors section and select Security Advisor.
- Find the entries named Security Definer View. Each entry names one view.
- Write down the name of each view.
The Security Advisor shows the lint results. If you prefer SQL, run this query to list the views and their settings:
select c.relname as view_name,
pg_get_userbyid(c.relowner) as owner,
c.reloptions
from pg_class c
where c.relnamespace = 'public'::regnamespace
and c.relkind = 'v';
A view with no security_invoker entry in reloptions uses the owner permissions.
Now test the view as an anonymous visitor. Use the project URL and the anon key from Project Settings, then API:
curl "https://YOUR-PROJECT.supabase.co/rest/v1/order_summary?select=*&limit=5" \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_ANON_KEY"
If the response contains rows, the view is open to everyone. If the response is an empty list or a permission error, the view is not open to the anonymous role. Repeat the test with the access token of a normal signed-in user. Check if the user sees rows that belong to other users.
Note: The anon key is public by design. See Supabase anon key: is it safe to expose?. The security of your data depends on RLS and grants, not on the secrecy of this key.
How do I fix a security definer view?
Set security_invoker on each view. The Supabase RLS documentation states that this option is for Postgres 15 and above. With this option, the view obeys the RLS policies of the underlying tables.
Caution: The view can return fewer rows after the change, or it can return an error. This is the correct result if the caller must not see those rows. Test every page of your app that reads the view before you deploy.
Follow these steps for each view:
- Check the Postgres version. Run
select version();. The version must be 15 or higher for the steps below. For older versions, see the section on older versions. - Set the option on the existing view. Run the command below.
- Check the base tables. Each base table must have RLS enabled, and it must have a policy for the roles that read the view.
- Test as an anonymous visitor. Use the
curlcommand from the previous section. - Test as a signed-in user. Check that the user sees only the allowed rows.
- Test the app. Open each page that uses the view.
alter view public.order_summary set (security_invoker = on);
If you create a new view, set the option in the same statement. The linter documentation recommends this: “always set with (security_invoker=on) when a view should respect RLS policies.”
create or replace view public.order_summary
with (security_invoker = on)
as
select user_id, email, total, created_at
from public.orders;
Make sure that the base table has a policy for authenticated users. The policy below lets each user read only their own orders:
alter table public.orders enable row level security;
create policy "users read own orders" on public.orders
for select to authenticated
using ( (select auth.uid()) = user_id );
With this policy and security_invoker, a signed-in user who queries order_summary gets only their own rows. An anonymous visitor gets no rows.
What if the view must show data from many users?
Some views must show combined data, such as a count of all orders for a public page. A security_invoker view then returns only the rows that the caller can see. Choose one of these options:
- Return only totals through a function. Write a
security definerfunction in a private schema that returns the count. Setsearch_path = ''in the function. - Use a separate summary table. A trigger or a scheduled job writes the totals into a table that has no private columns. Apply RLS to that table.
- Move the view out of the exposed schema. Put the view in a schema that the Data API does not expose. Your server code can then read it.
Do not return personal data from a view or function that is open to anonymous visitors.
What if the Postgres version is older than 15?
The Supabase documentation gives two options for Postgres 14 and older. First, revoke access to the view for the anon and authenticated roles. Second, move the view to a schema that the API does not expose.
revoke all on public.order_summary from anon, authenticated;
A better long-term action is to upgrade the Postgres version of the project. Use the upgrade option in the dashboard, and read the upgrade notes before you start. Take a backup first.
How do I fix many views at once?
A project can have many views. An AI tool can create ten views in one session. Use a script to list the commands, and then review each one.
- Run the query below. It prints one
alter viewcommand for each view inpublicthat has nosecurity_invokeroption. - Read the list. Remove the views that you want to move or delete.
- Run the remaining commands in one transaction.
- Test each view with the
curlcommand.
select format('alter view %I.%I set (security_invoker = on);', n.nspname, c.relname)
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relkind = 'v'
and coalesce(c.reloptions::text, '') !~ 'security_invoker=(on|true)';
Warning: Do not run all the commands without a review. A view that your public pages need can return no rows after the change. Check which views the app reads before you apply the commands.
Which views can stay as they are?
A view can stay as a security definer view only if all these statements are true:
- The view returns data that every visitor may read.
- The view has no column with personal data, tokens, or internal notes.
- The view has a clear reason to bypass RLS, and a person reviewed it.
- A test shows that the view returns the same rows for each caller.
If one statement is false, set security_invoker. If you want to keep the warning off the list for a reviewed view, document the reason in the migration file. Do not hide the warning without a review.
Why do AI tools create these views?
An AI coding tool often writes a view when you ask for a dashboard, a report, or a joined list. A view is a short way to join tables. The tool writes create view and does not add the option, because the default syntax works in the editor. The query runs as the postgres role in the SQL Editor, so the result looks correct to you and to the tool.
The test in the editor does not show the problem. The editor does not use the same role as your app. Test the view through the API with the anon key and with a user token. These two requests show what your visitors receive.
How do I check if someone read the data?
A view with this problem is open to anyone who finds the URL. Check for use that you did not expect.
- Open the API logs. In the dashboard, open Logs and select the API logs.
- Search for the view name. Look for requests to
/rest/v1/your_view. - Check the callers. Look for requests with no user token, and for IP addresses that you do not know.
- Check the request size. Look for requests with
select=*and no filter. Look for many requests in a short time. - Check the period. Start from the date that you created the view. A log that your plan has already removed cannot show older requests.
- List the columns. Write down which columns the view returned. Email addresses, names, and tokens need a user notice in many countries.
If the logs show reads from unknown callers, treat the exposed columns as leaked. Reset any secret that the view returned. Tell your users if the view returned personal data.
Check other tables at the same time. A view that bypasses RLS is a sign that the schema has no review process. Read the Supabase RLS guide and run the queries on each table in public. For the risk of leaked admin keys, read what to do about a leaked service_role key.
How do I stop new views from bypassing RLS?
Use these rules.
- Write
with (security_invoker = on)in everycreate viewstatement. Make this a rule in your project instructions for AI tools. - Run the Security Advisor after each migration. Fix each new entry on the same day.
- Keep views out of
publicwhen the Data API does not need them. Use a private schema for internal views. - Review AI-generated SQL for
create view. AI tools often create views for dashboards and omit the option. - Enable RLS on every base table. A view with
security_invokeris only as safe as the policies of the tables under it. - Do not select personal columns in a view that you do not need. A smaller view exposes less.
- Test each view as an anonymous visitor before release. Use the
curlcommand in this guide. - Scan the deployed project. The linter reads your schema. A scan of the live API shows what a visitor receives.
You can also write a check in your CI job. The query below lists views in public that do not have the option. The job fails if the query returns a row:
select c.relname
from pg_class c
where c.relnamespace = 'public'::regnamespace
and c.relkind = 'v'
and coalesce(c.reloptions::text, '') !~ 'security_invoker=(on|true)';
The pattern matches both spellings of the option.
When should I get help?
Get help in these cases:
- The view must show data of many users, and you do not know how to build a safe design.
- The API logs show reads of the view from unknown callers, and the view returned personal data.
- After the change, the app shows empty pages, and the base tables have many policies that you cannot read.
- The project uses Postgres 14 or older, and you cannot upgrade it.
- You use materialized views in
public. A materialized view does not follow RLS of its base tables, and it needs a separate design.
After the fix, open the Security Advisor and run the checks again. Confirm that the list of Security Definer View entries is empty and that each view returns the right rows for each user type.
Common questions
Is the security definer view warning an error or a warning?
Does security_invoker work on every Postgres version?
Will the view stop working after I set security_invoker?
Can I ignore the warning if the view only shows safe columns?
Do materialized views have the same problem?
Find views that leak rows
A VibeEval scan calls your public Supabase API without a login and reports tables and views that return rows to an anonymous visitor.
14-day free trial · No credit card · Cancel anytime