A table without RLS is open to anyone with the anon key
This note is a deep dive on one Supabase failure, not a Lovable overview. For the broader launch surface, start with Lovable app security risks before launch. Here the subject is Row Level Security, and the concrete risk is direct: a Supabase table with RLS disabled, or with a permissive USING (true) policy, is fully readable and often writable by anyone who holds the anon key.
In a Lovable app, that key is not a secret. The anon key ships in the client bundle of every deployed Lovable app, because the browser needs it to talk to Supabase. So "anyone with the anon key" means any visitor who opens DevTools, plus any crawler or script that loads the page. The key is doing its job by being public. The mistake is assuming it is a meaningful access boundary on its own.
Supabase exposes every table through an auto-generated REST API (PostgREST). RLS is what decides which rows that API will return to a given caller. With RLS off, the API answers for the whole table.
- RLS disabled on a table: the REST endpoint returns rows to the anon role.
- A policy of USING (true): technically RLS is on, but the rule allows every row through, so the effect is the same.
- This is an authorization gap (who can read which rows), not a broken login. The anon key authenticates the request as an anonymous caller exactly as designed.
Why this happens on Supabase by default
RLS is opt-in per table. When a table is created without explicit RLS, it has no row-level rules, and the auto-generated REST/PostgREST API will serve it to the anon role. Nothing in the create-table step forces you to add a policy first.
That default collides badly with AI-generated builds. A Lovable prompt that says "add a table for user notes" produces a working table and working UI fast. The generated flow reads and writes through the client SDK with the anon key, which makes the app function on the first try, which makes it look finished. The step that does not happen automatically is the per-table review where someone decides who should read each row.
- The table works in the app, so the missing policy is invisible from the inside.
- The anon key is wired up correctly, which hides that it is also the only thing standing in front of the data.
- Fast deploy means the public URL exists before anyone audits which tables are reachable. This is a generated-insecure-default pattern; see AI-generated insecure defaults.
Check each table from the outside
You can confirm this on your own app without any attack. There are two reliable paths.
From the Supabase dashboard: open the Table Editor and the Database linter (Advisors). Supabase flags tables that are exposed through PostgREST with RLS disabled. The Authentication and Database sections also show, per table, whether RLS is enabled and which policies exist. A table listed as RLS-disabled is the suspicious result.
From the public surface: find the project URL and anon key (they are in the client bundle, so open DevTools, Network tab, and watch the requests to *.supabase.co/rest/v1/). Then query a table's REST endpoint as the anonymous caller, for example a GET to /rest/v1/your_table?select=* with the anon key in the apikey header. Do this only against your own project.
- Suspicious result: rows come back from a table that should require a logged-in user. That means anon can read it.
- Safer result: the endpoint returns an empty array or a permission error for tables that should be private. An empty array with RLS on and no matching policy is the expected deny.
- Repeat per table. RLS is per-table, so one protected table tells you nothing about the next one. Check writes too: a POST or PATCH that succeeds with the anon key means the table is writable, not just readable.
Enable RLS, then write explicit policies
The fix has two stages, and the first one is the safe place to land.
Enable RLS on every table that holds user, account, payment, or admin data. Enabling RLS with no policy denies all access. That sounds like breakage, but it is the correct default to start from: the table is closed, and you then open exactly the paths you intend. It is far safer to start from deny-all and add policies than to leave a table open while you decide.
Then write explicit policies per table for the operations you actually want. A policy that scopes reads to the row owner (for example, matching the authenticated user id against an owner column) lets a logged-in user see their own rows and no one else's. Keep server-only operations off the anon role entirely; privileged work belongs behind the service_role key, which must never ship to the client.
- Turn RLS on for every non-public table first, even before the policies are perfect. Deny-all beats open.
- Write one policy per intended operation (select, insert, update, delete) rather than a single broad rule.
- Never expose service_role in client code or env vars that reach the bundle; it bypasses RLS entirely.
RLS controls which rows a caller can read or change. It does not validate the contents of a write. A user allowed to insert their own row can still submit malformed or abusive values, so input validation and constraints are a separate job from RLS.
Where VibeCodeGuard fits, and what it does not prove
VibeCodeGuard's Launch Check can detect a publicly accessible Supabase table by hitting the anon-key REST endpoint from the public surface. If a table returns rows to the anonymous caller, that is a direct signal of missing or disabled RLS, and the scan flags the exposure with severity and fix direction. It checks the thing an outside visitor can actually reach.
What it does not do is write your policies. Deciding which rows each role should see, and authoring correct per-table RLS, is work you do inside Supabase, and it should pair with manual review of your auth and data model.
Next step: list your tables, then go down the list and confirm RLS is enabled on each one that holds private data, starting from deny-all. Run a focused Launch Check against the public URL before sharing it, so an exposed table shows up in your scan and not in someone else's.
A clean Launch Check on the public surface does not prove every table is correctly scoped. The scan sees what is reachable from outside with the anon key; it does not read your policy logic or confirm that the policies you wrote match your intent. Treat a flagged table as a launch blocker, and treat a clean result as one signal, not a guarantee.
> launch check
Scan the public surface before launch.
Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.
> sources