Supabase "RLS disabled in public" warning: how to fix it
The “RLS disabled in public” warning means that anyone who knows your project URL can read, change and delete the rows in that table. To fix it, run alter table <table> enable row level security; for the table. Then add a policy for each action that your app needs. Then test the table with the public anon key.
This guide explains the warning, shows how to confirm it, and gives the fix in order of urgency. It also shows how to check for earlier abuse and how to stop the warning from coming back.
What does “RLS disabled in public” mean?
Row Level Security (RLS) is a Postgres feature. It checks every request against rules that you write. These rules are called policies. A policy decides which rows a caller can read, insert, update or delete.
Supabase exposes the public schema through an automatic REST API. Every table in public gets an endpoint. If RLS is off for a table, no rule limits the callers. Any caller that has a grant on the table can use it.
The Supabase linter reports this state as lint 0013_rls_disabled_in_public. The lint description gives the level as ERROR. It states that anyone with your project URL can read, edit and delete all data in the table.
The project URL and the anon key are not secret. The browser needs both, so the app sends them in every request. For this reason, assume that an attacker has them. Read what the anon key can access for more detail.
The warning appears in three forms. They have different urgency:
| Lint | Level | Meaning | Urgency |
|---|---|---|---|
0013_rls_disabled_in_public |
ERROR | The table is in an exposed schema and RLS is off. | Fix now. |
0007_policy_exists_rls_disabled |
INFO | A policy exists, but RLS is off, so the policy does nothing. | Fix now. The table is open. |
0008_rls_enabled_no_policy |
INFO | RLS is on and no policy exists. The API returns nothing. | Not a leak. Add policies. |
Many vibe coding tools create the first two states. You ask the tool for a table. The tool runs a create table statement in SQL. The tool adds a policy, or no policy, but does not enable RLS. The app works, so you do not see the problem.
How do I confirm that my table is open?
Use the Security Advisor first. Then test from outside, as an attacker does. The advisor shows the configuration. The test shows what a caller gets.
Check the Security Advisor
- Open your project in the Supabase dashboard.
- Select Advisors, then Security Advisor.
- Find each finding with the name “Table publicly accessible”.
- Write down the table names.
The advisor documentation also lists two other ways to run the check. You can run supabase db advisors in the CLI, or use the Management API.
Note: Some findings can be intentional. A table with public data, such as a list of countries, can stay open for reading. Even then, nobody must be able to write to it.
Test the table with the anon key
Use the same key that your deployed frontend ships. Do not use the service_role key for this test, because it bypasses RLS and hides the problem.
curl "https://<project-ref>.supabase.co/rest/v1/<table>?select=*&limit=1" \
-H "apikey: <anon-key>" \
-H "Authorization: Bearer <anon-key>"
Read the result:
- A JSON row means that anonymous callers can read the table.
- An empty array
[]means that RLS removed every row, or the table is empty. - A permission error means that the role has no grant on the table.
To test writing, use a test table or a row that you created for this test. Do not run a delete or update test against real user data.
curl -X PATCH "https://<project-ref>.supabase.co/rest/v1/<table>?id=eq.<test-row-id>" \
-H "apikey: <anon-key>" \
-H "Authorization: Bearer <anon-key>" \
-H "Content-Type: application/json" \
-d '{"name":"rls-test"}'
Then read the row again with the first command. If the value changed, an anonymous caller can write to the table.
How do I fix the warning now?
Do the steps in this order. Steps 1 to 3 close the hole. The other steps repair the app and the data.
Caution: After you enable RLS, your app can lose access to its own data. The API returns no rows until a policy allows the request. Do steps 1 to 3 in one session, and test the app after step 3.
-
List the tables. Use the Security Advisor list from the previous section. Add any table that you find in the
publicschema and that the app does not use. -
Enable RLS on each table. Open Database, then Policies, and select Enable RLS for the table. The Supabase API security guide describes this path. You can also run SQL:
alter table public.todos enable row level security; -
Add a policy for each action that the app needs. Without a policy, the API blocks all access. See the next section for examples.
-
Test again with the anon key. Run the same
curlcommands. The read test must return[]or an error for data that anonymous users must not see. -
Test the app. Sign in as a normal user and use each feature. Fix each place where the app now shows an empty list or an error.
-
Check the other tables. Look at the tables in other exposed schemas. Also check views and functions, because RLS does not apply to functions.
-
Put the change in a migration. Save the SQL in a migration file, so that every environment has the same rules.
If the table does not need any browser access, enable RLS and add no policies. The API then returns nothing. Your server code can keep access with the service_role key. Never put that key in the browser. See the service_role key guide for the rules.
If a table must stay public for reading, add an explicit policy for select. Do not add a policy for writes.
Which policies do I add after I enable RLS?
Write the smallest policy that lets the app work. The Supabase RLS documentation shows the syntax. A select policy uses the using clause. An insert policy uses the with check clause. An update policy uses both. A delete policy uses using.
Use this pattern for a table where each row belongs to one user. The table has a user_id column that holds the owner:
create policy "owner can read" on public.todos
for select to authenticated
using ((select auth.uid()) = user_id);
create policy "owner can insert" on public.todos
for insert to authenticated
with check ((select auth.uid()) = user_id);
create policy "owner can update" on public.todos
for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
create policy "owner can delete" on public.todos
for delete to authenticated
using ((select auth.uid()) = user_id);
The to authenticated clause limits each policy to signed-in users. Anonymous callers use the anon role, so they match none of these policies. The documentation states that auth.uid() returns null for an unauthenticated request. Because null = user_id is never true, the policy also blocks these requests.
The (select auth.uid()) form is the form that the documentation recommends. Postgres then calculates the value one time for each statement, not for each row.
Warning: Do not write using (true) to make an error disappear. This policy lets every caller read every row. The Supabase linter has a separate warning for this case, lint 0024_permissive_rls_policy.
For a full explanation of ownership policies, team access and roles, read the Supabase RLS guide.
What if my table has no user_id column?
Add the column before you write the policy. Set its default to auth.uid(), so that the database fills it for each new row:
alter table public.todos
add column user_id uuid default auth.uid() references auth.users (id);
Rows that exist already have null in the new column. Assign each old row to its owner before you rely on the policy. A row with null as owner matches no user, so nobody can read it through the API.
What if I do not use the Supabase API at all?
You can remove the exposure instead of writing policies. This option fits an app where only a server reads the database, with a direct Postgres connection.
According to the API security guide, you can turn off the Data API. Open the Data API overview in the dashboard and turn off “Enable Data API”. The automatic REST endpoints then stop responding, regardless of grants or RLS.
Use this option only if the app does not call supabase.from(...), REST or GraphQL. If you turn it off by mistake, the app breaks at once. You can turn it on again.
Note: RLS is still a good second layer, even for an app that uses a direct connection. A later change in the settings can expose the tables again.
How do I check whether someone used the open table?
The warning gives no proof of abuse. It only shows that abuse was possible. Check the logs and the data for signs of use that you did not make.
- Open the logs. In the dashboard, open the Logs page. The Supabase logs documentation lists the API Gateway and Postgres sources.
- Filter by method. Use the Method filter to show
DELETE,PATCHandPOSTrequests to the REST path of the table. The same page describes the Pathname filter. - Look for unusual callers. Compare the user agents and the IP addresses with your own traffic. Many requests from one source, or requests at night, need a closer look.
- Check the retention. The log retention depends on your plan. Old requests may no longer exist.
- Check the data. Look for rows that you do not recognize, rows with the same text, and missing rows. Compare the row count with your last known count.
- Check the sensitive tables first. Start with tables that hold users, orders, messages and files.
The Postgres logs do not always record each data change. The statement logging settings control what is recorded. If you cannot find a trace, you cannot prove that nobody used the table. Treat the data as read in that case.
If you find deleted or changed rows, restore the data. The Supabase backups documentation states that Pro projects keep 7 days of daily backups, Team projects keep 14 days, and Enterprise projects can access up to 30 days. A restore replaces the database with the state at the time of the backup. The project is not available during the restore. Changes after the backup time are lost.
For Free projects, the same page recommends that you export data regularly with the CLI command supabase db dump.
Warning: If personal data was readable, you may have a duty to tell your users or a regulator. Ask a lawyer if you are not sure.
How do I stop the warning from coming back?
The warning returns when a new table appears without RLS. Use these controls to stop that.
- Enable RLS in the same migration that creates the table. Put
create table,enable row level securityand the policies in one file. - Create tables in the dashboard Table Editor. The API security guide states that tables created in the dashboard have RLS on by default. Tables created in the SQL Editor or with other tools do not.
- Give new tables no grants by default. In the Data API settings, turn off “Automatically expose new tables”. The guide notes that this does not remove every default grant, so read its section on default privileges.
- Tell your AI tool the rule. Add this line to the project rules: “Every new table in
publicneeds RLS enabled and explicit policies in the same migration.” - Review the SQL that the tool writes. Search each change for
create table. For each match, look forenable row level security. - Check the Security Advisor after each release. The advisor reads the live configuration, not the code.
- Test the deployed app. Run the anon-key test against each table after a release. A scan of the live app also finds tables that you forgot.
For a Bolt project, read the Bolt security guide for a full checklist.
When should I get help?
Get help in these cases:
- The logs show deletes or updates that you did not make. Restore a backup, and ask an engineer to review the access.
- The open table held personal data, payment data or private messages. You may have legal duties.
- You cannot write a policy that matches your data model. A wrong policy can block real users or leave the hole open.
- The app stops working after you enable RLS, and you cannot find the missing policy. Read the error code. Error
42501means that a role has no grant on the table. - The
service_rolekey was in the browser code. Treat the whole database as exposed, and rotate the key.
After the fix, run the anon-key test one more time. The read request must return no data that an anonymous user must not see.
Common questions
Is the warning a real problem or a false alarm?
Why does my app show no data after I enable RLS?
Do I need RLS on a table that only my server reads?
What does the 'policy exists, RLS disabled' notice mean?
Can I hide the table without RLS?
Test your tables as anon
A VibeEval scan calls the REST endpoints of your Supabase project with the public key and reports the tables that return or accept data.
14-day free trial · No credit card · Cancel anytime