Tools like Lovable and Bolt wire up Supabase for you, and they do it the same way: the Supabase URL and the anon key go straight into a client file that ships to the browser. That is not the mistake people think it is. The anon key is meant to be public. The mistake is what is missing behind it. The anon key's access is scoped entirely by Row Level Security, and if you never configure RLS, that public key is a full read and write handle on your database for anyone who opens your site and looks at the network tab.
In our scans of real app-builder projects, this pattern, a public key in front of an unprotected database, is the largest single driver of high-severity findings. One set of Lovable apps we measured had a high-severity flaw in 35.1% of them, and missing RLS was the recurring cause. The fix is not complicated. It is just a step the builder does not do for you.
Why the anon key is safe only with RLS
Think of the anon key as a name badge, not a key to the building. It tells Supabase which project you are, and then Row Level Security decides, per table and per row, what that badge is allowed to do. With no policies, the default behavior leaves the data reachable, so the badge walks straight in. With owner-scoped policies, the same badge can only touch the rows that belong to the authenticated user. Same key, entirely different exposure. The difference is the policies, which is why the checklist is really an RLS checklist.
Want to see exactly what Prbl flags? Watch it scan a demo app, no repo or account needed.
See a live scan →The checklist
- Enable RLS on every table. Not just the obvious ones. Any table reachable by the anon key needs it, including join tables and anything you added late.
- Write a policy that scopes each row to its owner. The common shape is: allow a user to select, insert, update, or delete a row only where the row's user id equals their authenticated id. A table with RLS on and no policy denies everything, which is safe but breaks the app, so the policy is not optional.
- Never ship an allow-all policy. A policy of
using (true)is the same as having RLS off. It is a common shortcut when something does not work, and it quietly reopens the door. - Keep the service_role key server-side, always. It bypasses RLS entirely. If it is anywhere in client code or a repo, rotate it now and assume the data was exposed.
- Confirm only the anon key is in client code. Search the bundle and the repo for the service_role key by shape; it should never appear.
- Validate writes in the policy, not just the UI. A policy that allows any insert lets a user write rows they should not. Scope inserts and updates the same way you scope reads.
The test that proves it
Do not trust that RLS is working because the app works. The app working only tells you the allowed path is open. Prove the disallowed path is closed: take your own public anon key and, as an unauthenticated or wrong-user client, try to read a table you should not be able to read. If rows come back, a policy is missing or too permissive. If you get nothing, the policy is holding.
// with the public anon key, as the wrong user, this should return no rows
const { data } = await supabase
.from("private_table")
.select("*");
// data is [] or an error -> RLS is protecting you
// data has rows -> a policy is missing or allow-allRun that against every table that holds user data. It takes a few minutes and it is the only check that actually confirms the policies do what you think.
Frequently asked questions
Is it bad that my Supabase anon key is visible in the browser?
No, that is by design. The anon key is meant to be public and shipped to the client; it identifies your project and nothing more. What matters is whether Row Level Security is configured, because the anon key's access is scoped entirely by your RLS policies. With good policies the public key is safe. Without them, the public key is a door into your whole database. The key being visible is not the problem; missing policies are.
How do I know if RLS is actually on?
Enabling RLS on a table and writing policies are two separate steps, and both are needed. A table with RLS enabled and no policy denies everything, which is safe but breaks your app. A table with RLS disabled, or with a permissive allow-all policy, is wide open. The reliable way to know is to test: use your own public anon key to try reading a table you should not be able to read, and confirm you get nothing back.
What is the difference between the anon key and the service_role key?
The anon key respects RLS and is safe in the browser. The service_role key bypasses RLS entirely and must never leave your server or reach the client, because anyone who has it has full access regardless of your policies. AI tools sometimes place the service_role key in client code or a repo by mistake. If that has happened, rotate it immediately and treat the data as exposed.
Do I need RLS if my app is small or not launched yet?
Yes, before you launch. Public Supabase projects are discoverable, and the anon key sits in your shipped bundle, so the window between going live and configuring RLS is a window where anyone can read and write your data. It is much easier to enable RLS while the schema is small than to retrofit it across dozens of tables later, so do it as you build each table, not at the end.
Check your own app
If your app was built with Lovable, Bolt, or another builder that generated a Supabase client, the anon key is in your bundle and the odds that RLS was fully configured for you are low. A scan reads the repo and flags an exposed key with no protection behind it, so you can see where you stand before someone else does. Run a free scan and go down the checklist.