Why the anon key is not the problem (and RLS is)
Every Supabase app built with Lovable, Bolt, v0 or Cursor ships a project URL and an anon key to the browser. That is by design: the anon key is a public JWT with the anon role, and the Supabase client libraries need it to talk to your database's REST API (PostgREST). Seeing it in DevTools is not a leak.
What it does mean is that one thing now stands between the internet and every row of your data: Row Level Security (RLS). Anyone can copy your anon key and call https://<project>.supabase.co/rest/v1/<table> directly, outside your app, with no UI and none of your frontend checks. If a table in the public schema has RLS turned off, or has a policy like using (true), that request returns the whole table. In our scan of 99 launched vibe-coded apps, 37% exposed a Supabase anon key, so for more than a third of them, RLS was the entire security model.
What this checker does
- Finds Supabase config in your HTML and up to 25 same-site JavaScript bundles, and decodes the JWT to confirm its role.
- Flags a service_role key as critical. The
service_rolekey bypasses RLS completely. If it is in your bundle, anyone can read, edit and delete everything. Rotate it immediately. - Scans for other secrets (OpenAI, Stripe live, AWS, GitHub and more) in the same pass.
- Runs deeper checks only on sites you own. Testing whether your anon key can list your API schema means touching your database, so we only do it after you verify ownership with a meta tag, file or DNS record.
How to check RLS yourself in five minutes
- Open the Supabase dashboard → Table Editor. Every table without RLS shows an "RLS disabled" or "Unrestricted" badge. There should be none in
public. - Or run this in the SQL editor:
select tablename, rowsecurity from pg_tables where schemaname = 'public';Any row withrowsecurity = falseneeds fixing. - Enable it:
alter table public.profiles enable row level security;With RLS on and no policies, the anon role gets nothing, which is the safe default. - Add narrow policies, for example
create policy "own rows" on public.profiles for select using (auth.uid() = user_id);Write separate policies for insert, update and delete, and usewith checkon writes so users can't insert rows for someone else. - Search your policies for
using (true). It is fine for genuinely public data like a blog's published posts, and a disaster onusers,ordersormessages. - Test as an outsider: call the REST endpoint with only the anon key and no user session, and confirm you get an empty array or a 401.
Common RLS mistakes in vibe-coded apps
Policies that check the wrong column. AI tools sometimes generate auth.uid() = id on a table where id is the row id, not the owner. The policy looks right and lets nobody (or the wrong person) in. Trusting the client for roles. A policy that reads an is_admin column the user can update themselves is not a policy. Keep role data in a table users can't write, or in JWT custom claims. Storage buckets. Supabase Storage has its own policies on storage.objects; a public bucket full of user uploads is a common leak. Views and functions. Views run with the owner's permissions by default and can bypass RLS; use security_invoker = true on Postgres 15+. security definer functions exposed via RPC need the same care.
The fix prompt
If you build with an AI editor, paste this: "Audit my Supabase project. For every table in the public schema, enable Row Level Security and write policies so users can only select, insert, update and delete their own rows (auth.uid() = user_id). List any table with RLS disabled or a policy using (true), and check storage bucket policies too." Then re-run this checker and the outsider test above.