A guide
Is the data in my Supabase app public?
It is public if a table has row-level security (RLS) turned off, or a policy that lets anyone read it. Your app hands the publishable key, the anon key in older projects, to every visitor, so whatever that key can read, anyone can read. Start with the Security Advisor in your Supabase dashboard, then test your own project with only that key.

Why the public key is not the problem
Supabase builds an API from your database that the browser calls directly. The publishable key is made to sit in the browser: Supabase's docs call it safe to expose, because it reaches only what RLS allows. If you built with Lovable, Bolt or another tool that connected your app to Supabase, all of this applies to you.
What row-level security does
RLS is a Postgres feature that adds a rule to every query on a table. Rows for which the rule is true are visible; the rest are not. With RLS on and no policies at all, the publishable key reads nothing. So turning RLS on closes the table, and your policies decide what opens again.
The common mistakes
- RLS is off. The Security Advisor flags it as an error: anyone with your project URL can read, edit and delete every row.
- A policy that just says true. A read policy for the anon or public role (a policy with no role given shows as public) with using (true) lets every visitor read every row. That suits a public leaderboard, not customers or orders.
- Signed in is treated as allowed. A policy for the authenticated role with no row check lets every signed-in user read everyone's rows. If anyone can sign up, that means anyone.
- An old open policy left behind. Permissive policies on a table are combined with OR, so one open policy keeps the table open after you add a strict one.
- Rules based on user metadata. Supabase warns that a policy built on the user_metadata claim can create security issues. Keep roles in a table of your own, protected by its own policies.
- A view that skips RLS. Views bypass RLS by default. On Postgres 15 and later, create them with security_invoker = true so they follow the tables' policies.
- The secret key in the app. The secret key, service_role in older projects, bypasses every policy. If it is in your browser code, your policies do not protect anything until you rotate it.
How to test your own project
Do this only on your own project. Never point these steps at someone else's app. Each step can show that something is open. None can show that everything is closed: a clean result means only that this step found nothing, and it is not a security audit. Storage, database functions and schemas other than public are outside this guide.
- Open the Security Advisor in the Supabase dashboard and fix every error it lists, starting with "RLS Disabled in Public".
- In the SQL editor, list the public tables with RLS off: select tablename from pg_tables where schemaname = 'public' and not rowsecurity;
- List your views, because they do not show in the query above: select table_name from information_schema.views where table_schema = 'public'; and, for materialized views, select matviewname from pg_matviews where schemaname = 'public'; A view skips RLS unless it was created with security_invoker = true, and a materialized view keeps its own copy of the rows, which the tables' policies do not filter. Test every one with the request below.
- List your policies and read each rule, for insert, update and delete as well as select, because the two requests below only test reading: select tablename, policyname, roles, cmd, qual, with_check from pg_policies where schemaname = 'public';
- Ask the API the way a stranger would, with only the publishable key, as in Supabase's own quickstart: curl 'https://YOUR-PROJECT.supabase.co/rest/v1/YOUR_TABLE?select=*' -H 'apikey: YOUR_PUBLISHABLE_KEY'. Run it for every table and view that holds private data. Rows mean anyone can read them. No other answer proves the table is closed. A 'permission denied for table' error (code 42501; a view says 'for view') means this request was refused, and a request for fewer columns might not be. An empty list comes from a closed table, from an empty one, and from one a policy opens only in part. An 'Invalid API key' answer means the test did not run: fix the key and try again. What the key can read is decided by the policies you listed above, so the request adds to reading them and does not replace it.
- Then ask as a signed-in user. The request above cannot see a policy that lets every signed-in user read everything, and neither can your app's screens, which usually ask only for the current user's rows. Create two test users and give each one rows of their own. Sign in to your app as the first, open the browser's developer tools, find a request to your project's /rest/v1/ address in the Network tab and copy the token from its Authorization header, the part after Bearer. Use a request made after you signed in: before that, the header carries the public key instead of a user's token.
- Run the same curl with that token added and no filter in the address: -H 'Authorization: Bearer FIRST_USER_TOKEN'. Rows of the second user mean every signed-in user can read them. The API caps how many rows one request returns, so also ask for the second user's rows directly, adding &user_id=eq.SECOND_USER_ID with your own column name: any that come back mean the same. If the first user's own rows do not come back, or the answer says the token expired, the test did not run as that user: copy a fresh token (it lasts an hour by default) and try again. Repeat as the second user. Seeing only your own rows shows what these two users can read in this table today, not that every policy is right.
Where LOJIK fits
You can run every step above yourself. If you want a second look, LOJIK's Rescue page is for apps built with AI tools that got stuck.
The page opens with a free self-check. No sign-in.
When LOJIK is not the right fit
LOJIK is probably not the right fit if:
- A developer or a security team already reviews your policies.
- You want someone to test a project that is not your own. Nobody should, without the owner's written permission.
- A customer or a regulator asks for a formal security audit.
Questions
Is it safe that the anon key is in my app's code?
Yes, when RLS is on and your policies are right. The publishable key, the anon key in older projects, is made to be public and reaches only what RLS allows. The secret key is the one that must never be there.
Will turning on RLS break my app?
It can, until you add policies. With RLS on and no policy, the publishable key reads nothing, so screens that read that table go empty. Add the policies in the same change.
I built my app with Lovable. Does this apply to me?
If your app keeps its data in Supabase, yes. Open the Supabase project your app uses and run the same checks.
Product names belong to their owners. LOJIK is not affiliated with them, and they are named only to say what they are.
Sources
- Supabase docs: Row Level Security
- Supabase docs: Understanding API keys
- Supabase docs: Advisors
- Supabase docs: REST API
- Supabase docs: API quickstart
- Supabase docs: JSON Web Tokens
- Supabase docs: User sessions
- PostgreSQL docs: CREATE POLICY
- PostgreSQL docs: pg_tables
- PostgreSQL docs: pg_policies
- PostgreSQL docs: information_schema.views
- PostgreSQL docs: pg_matviews
Checked against these sources on Nov 11, 2026.