Blog · 2026-08-06
Row Level Security in Lovable apps — why your database might be public
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
1. Open your app in the browser, then open DevTools → Network. 2. Reload and look for requests to *.supabase.co/rest/v1/... — note your project URL and the apikey header (that's the anon key; it's meant to be visible). 3. In a private browser window — logged out of everything — request one of your tables with that URL and key, for example with curl. 4. 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:
1. 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. 2. 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). 3. 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. 4. Never ship the service_role key 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.