Originally published on howsafeismyapp.com.
Here is the uncomfortable truth about most Lovable apps: the database is not behind your login form. It is behind Supabase's API — and whether that API hands out your data to strangers depends on one setting most AI-generated apps never mention: Row Level Security.
This is the most serious finding our scanner sees in the wild. It is also completely fixable in an afternoon. (Practical guidance, not legal advice.)
Why your frontend talks to the database directly
Traditional apps put a backend server between the browser and the database. Lovable apps usually don't: the browser talks to Supabase's REST API directly, authenticated with the so-called anon key that is shipped inside your JavaScript bundle.
That key is supposed to be public. Anyone who opens your app has it — and with it, anyone can send requests like:
GET https://your-project.supabase.co/rest/v1/users?select=*
What decides whether that request returns your entire users table or an empty response is not the key. It is Row Level Security.
What RLS actually does
Row Level Security is a PostgreSQL feature that Supabase builds on. With RLS enabled on a table, every query is filtered through policies — rules that say which rows a given user may read or write. A typical policy:
- a user may
SELECTrows inprofileswhereuser_id = auth.uid() - nobody may
SELECTfrompaymentsat all via the public API
Without RLS enabled, there is no filter. The anon key can read every row of every exposed table. Your login form is decoration; the data was never behind it.
Why AI-built apps get this wrong
Lovable and similar builders generate working code fast, and "working" means the app can read and write its own data. Two common paths to trouble:
- The generated setup never enables RLS on new tables, because everything works without it in development.
- RLS was enabled, but a debugging session ended with a policy like
USING (true)— "allow everyone" — that never got removed.
Neither produces an error. The app works perfectly. That is exactly why this finding survives into production. Our check page explains what the scanner looks for: Database readable without a login.
Check your own app in two minutes
- Open your app in the browser, then open DevTools → Network.
- Reload and look for requests to
*.supabase.co/rest/v1/...— note your project URL and theapikeyheader (that's the anon key; it's meant to be visible). - In a private browser window — logged out of everything — request one of your tables with that URL and key, for example with
curl. - If you get data back while logged out, that table is public. Anything with names, emails, or user content is now a data-protection incident waiting to be discovered by someone less friendly.
Under Art. 32 GDPR you are required to have appropriate technical measures for personal data; an openly readable users table is about as clear a failure of that as exists. If real personal data was exposed, Art. 33 GDPR (breach notification, 72 hours) becomes the relevant question — a conversation you want to avoid ever needing.
Locking it down
In the Supabase dashboard:
- Table editor → your table → enable RLS. Do this for every table in the public schema. Supabase warns you about tables without RLS — take the warning seriously.
-
Write policies from the user's perspective. Start restrictive: users read their own rows (
auth.uid() = user_id), insert only their own, update only their own. Add broader read access only where the product genuinely needs it (e.g. a public feed). - Tables the browser never needs (internal logs, admin data) get RLS enabled with no policies at all — that means no access via the public API, which is exactly right.
-
Never ship the
service_rolekey to the frontend. It bypasses RLS entirely. If it appears anywhere in your bundle, rotate it now — we cover this separately: Admin key exposed in frontend code.
Then re-run the two-minute check from above. The logged-out request should now return an empty array or an error — both are success.
After the fix: verify from the outside
The inside view (dashboard says RLS enabled) and the outside view (what a stranger's browser can actually fetch) can disagree — a forgotten table, a too-broad policy, a second Supabase project. The outside view is the one that matters, and it is what a passive scan checks: our scanner requests your app the way any anonymous visitor would and reports which tables answer. It takes about a minute, needs no login, and stores nothing.
Common questions
Is the Supabase anon key a secret?
No — it is designed to be public and ships in your JavaScript bundle by design. The protection layer is Row Level Security, not the key. The key that must stay secret is service_role — see admin key exposed in frontend code.
Will enabling RLS break my app?
Enabling RLS without policies makes tables return empty results via the public API — so your app's own queries stop working until you add policies for them. That is the point: write the policies your app needs, and everything else stays closed. Budget an hour, not a weekend.
How do I know whether someone already accessed my data?
Check the API logs in the Supabase dashboard for anonymous requests to sensitive tables. If personal data of real users was readable, look into the breach-notification duties of Art. 33 GDPR — the 72-hour window starts when you become aware, and honest, early handling counts in your favor.
Top comments (0)