Is My Supabase Database Exposed? How to Check
Guides · Updated 3 October 2026
If your app uses Supabase, your project URL and anon key are public. Whether that is a problem depends on how the database is configured. Here is how to find out in a few minutes, and what to do if the answer is yes.
What the anon key allows
The anon key is meant to be public. It ships in your frontend code and anyone can read it in the browser. On newer projects it is called the publishable key. It does not identify a user. It lets a request reach the REST API that Supabase generates for every table in the public schema.
What happens next is up to Postgres. A request with no signed-in user runs as the anon role. If a table has row level security (RLS) on, the request gets only what the policies allow. If RLS is off and the anon role has privileges on the table, which is the default on existing projects, the request can read, insert, update and delete every row.
So the key is not the leak. A table without RLS is.
List the tables without RLS
Open the SQL Editor in the Supabase dashboard and run this. It lists every table in the public schema, whether RLS is on, and whether the anon and authenticated roles have privileges on it.
SELECT
tablename,
rowsecurity AS rls_enabled,
has_table_privilege('anon', format('%I.%I', schemaname, tablename), 'SELECT,INSERT,UPDATE,DELETE') AS anon_access,
has_table_privilege('authenticated', format('%I.%I', schemaname, tablename), 'SELECT,INSERT,UPDATE,DELETE') AS authenticated_access
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY rowsecurity, tablename;A row with rls_enabled false and anon_access true is open to the internet. With only authenticated_access true, it is open to anyone who can sign up.
Then check for tables that have policies while RLS is off. These look protected in the dashboard and are not.
SELECT c.relname AS tablename FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid WHERE c.relkind = 'r' AND n.nspname = 'public' AND NOT c.relrowsecurity AND EXISTS (SELECT 1 FROM pg_policy p WHERE p.polrelid = c.oid);
Test as an anonymous visitor
The SQL Editor runs as the postgres role and bypasses RLS, so it cannot tell you what a visitor sees. Ask the API instead, with nothing but the anon key. This request only reads one row.
curl "<project_url>/rest/v1/<table_name>?select=*&limit=1" \ -H "apikey: <anon_key>" \ -H "Authorization: Bearer <anon_key>"
- A JSON array with a row in it: anyone can read the table.
- An empty array: either RLS blocked the read or the table has no rows. Check which.
- An error with code 42501: the anon role is not allowed to read the table.
Repeat for each table that holds user data. You can also use role impersonation in the dashboard to run queries as anon or as a specific user.
If email confirmation is off, anyone can sign up with a made-up address and get the authenticated role. In that case, test again with a session from a fresh account, because many policies only check that a user is signed in.
Other places data leaks
RLS being on does not settle it. Check these too.
- Policies with USING (true). RLS is on, but the policy allows every row.
- Views. They bypass RLS by default, unless security_invoker is set on Postgres 15 and above.
- Materialized views. They cannot have RLS policies, so RLS does not protect one in the public schema.
- Functions. Every function in the public schema can be called at /rest/v1/rpc/<function_name>, and SECURITY DEFINER functions run with the privileges of their creator.
- Public storage buckets. Anyone with a file URL can read the file.
- Columns named like password, token or api_key in tables the API can read.
This query lists the policies that allow every row or apply to every role, including anon.
SELECT tablename, policyname, cmd, roles, qual, with_check
FROM pg_policies
WHERE schemaname = 'public'
AND (roles = '{public}' OR qual = 'true' OR with_check = 'true')
ORDER BY tablename, policyname;What to do if it is exposed
Enable RLS on the open tables first. This closes the hole at once: with RLS on and no policies, the anon key gets no rows.
ALTER TABLE public.<table_name> ENABLE ROW LEVEL SECURITY;
Your app will also get no rows from those tables until you add policies, so write them right away. For data that belongs to one user, an owner policy is the usual answer. Add matching policies for INSERT, UPDATE and DELETE.
CREATE POLICY "Users read own rows" ON public.<table_name> FOR SELECT TO authenticated USING ((SELECT auth.uid()) = user_id);
Then deal with what may already have happened. Assume anything in an open table could have been read or changed. Rotate any API keys or tokens that were stored there, and look for rows you did not create.
If the service role key was ever in frontend code, rotate it. It bypasses RLS, so none of the steps above protect you from it.
Stop it from happening again
Tables created in the dashboard Table Editor get RLS by default. Tables created with SQL, migrations or an ORM do not. Enable RLS and add the policies in the same migration that creates the table, and run the checks above after every schema change. Locksoup runs these checks automatically with a read-only role.
Related checks
Check your own project
Locksoup runs these checks on your Supabase database in about a minute, with a read-only role, and gives you the SQL to fix what it finds.