In spring 2025 a security researcher, Matt Palmer, scanned 1,645 apps from Lovable's public showcase. 170 of them had databases that anyone on the internet could read, and often write to, across 303 exposed endpoints. It was logged as CVE-2025-48757.
Nobody broke in cleverly. Row-level security had never been switched on, so the database answered every request it got from anyone holding the public key, and that key ships in every Lovable app's front end by design.
Lovable disputes the CVE, on the grounds that each customer is responsible for their own app's data. On the narrow question, that's fair. On the practical one it doesn't help much: if your app is one of the exposed ones, it's still your customers' data.
This post is the check we run at the start of every rescue, written so you can do it yourself. Most of it takes an hour. If you'd rather start with something shorter, our free production readiness check covers the big ones in about two minutes. For the wider picture of getting an AI-built app live, see what it takes to go live.
How a Lovable app talks to its database
A Lovable app is mostly front end. The browser talks straight to Supabase using two things baked into the JavaScript:
- The project URL
- The anon key, a public key that identifies your project
Both are visible to anyone who opens the developer tools. That's expected. Supabase's design assumes the browser is untrusted and puts all of the protection in the database itself, as row-level security (RLS) policies.
So the whole question of whether your data is safe comes down to this: does every table have RLS switched on, and do the rules say what you think they say?
Check 1: is RLS switched on for every table?
Open your Supabase project, go to the SQL editor and run:
select tablename
from pg_tables
where schemaname = 'public'
and rowsecurity = false;
Any table this returns is open to anyone with your anon key. Not just signed-in users. Anyone.
If it returns nothing, good, move on to check 2. If it returns tables, don't just switch RLS on in a panic: with RLS on and no policies, every request is refused and your app stops working. Write the policies first (check 2), then enable it.
Check 2: do the policies actually restrict anything?
RLS being on isn't enough. Policies written too loosely are the second most common problem we find, and they're harder to spot because the dashboard shows a reassuring green tick.
List them:
select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
order by tablename;
Look at the qual column (who can see a row) and with_check (what a user is allowed to write). Watch for:
true. A policy whose condition is justtruelets everyone through. It's often generated to "make it work" and never revisited.- Policies for
anonon tables holding personal data. Signed-out visitors rarely need to read anything except genuinely public content. - "Authenticated users can read all rows". This is the classic one. Every signed-in customer can read every other customer's records. The app looks fine, because the screen only asks for your own rows, but the database would hand over the rest to anyone who asked.
- Missing
with_checkon inserts and updates. Without it, a user may be able to write a row that belongs to someone else, or change theuser_idon their own.
A sound policy for user-owned data usually ties each row to the person signed in:
create policy "Users read their own orders"
on public.orders for select
to authenticated
using ((select auth.uid()) = user_id);
The real test: sign in as two different users in two browsers. In the second, try to load the first user's records, by changing an ID in the URL or replaying a request from the network tab with a different ID. If you get data back, your access rules live in the interface, not the database.
Check 3: is the service role key anywhere it shouldn't be?
Supabase gives every project a second key, the service role key. It bypasses every RLS policy. It's meant for trusted server code only.
Search your built front end for it. In Chrome, open developer tools, go to Sources, press Cmd+Option+F (Ctrl+Shift+F on Windows) and search for service_role, and for the first few characters of the key from your Supabase API settings.
While you're there, search for other secrets: sk_live (Stripe), sk- (OpenAI and others), and any API key you've pasted into a prompt. Anything found in the bundle has to be rotated, not just removed, because it's been public since the day it shipped.
Check 4: storage buckets
Files often get forgotten. Uploads, invoices, ID documents and avatars live in Supabase Storage, which has its own policies, separate from your tables.
In the dashboard, go to Storage and check each bucket:
- Is it public? A public bucket serves any file to anyone who has the URL. That's fine for marketing images and not fine for anything a customer uploaded.
- Do the policies scope files to their owner? The usual pattern stores files under a folder named after the user's ID and checks it in the policy.
Check 5: edge functions and anything that costs money
Lovable apps often use Supabase edge functions to call OpenAI, send email or take payments. For each one, check:
- Does it check who's calling? A function that doesn't verify the user's session is an open endpoint anyone can run, at your expense. That matters a great deal when it calls a paid AI API.
- Does the server decide the price? If the amount charged arrives from the browser, a customer can change it. The function should look prices up itself.
- Is the Stripe webhook verified? If your app trusts any request to its webhook URL, anyone can tell it they've paid.
- Is there a rate limit? Signup, login, password reset and anything that sends a message or spends money should refuse a flood of requests.
Check 6: can you get it back?
Supabase takes backups on paid plans. The question is whether anyone has ever restored one, and how long it took. An untested backup is a hope. Restore one into a separate project, check the data is there, and write down how long it took.
What to do with what you find
If checks 1 and 3 came back clean, you're ahead of a lot of apps. Fix what came up in the others and keep this list for every new table and function you add. RLS needs adding to each new table, and AI builders don't always remember.
If check 1 returned tables with real customer data, or check 3 found a key:
- Rotate the exposed keys today. Supabase, Stripe, OpenAI, whatever was there.
- Write the policies, then enable RLS, table by table, testing each with two accounts.
- Check the logs for access you don't recognise. Under UK GDPR, a breach that's likely to put people at risk has to be reported to the ICO within 72 hours of you becoming aware of it, so don't sit on it.
None of this means throwing the app away. The application code Lovable writes is usually a reasonable starting point. What's missing is the protection around it, and that's days of work, not months.
If you'd rather someone else did it
We do this every week. Our two-week kickoff sprint at £1,950 covers everything above, fixed rather than just reported, and ends with a written report you keep either way. If the app needs ongoing work after that, it carries on inside the subscription, and the £1,950 comes off your first month if you subscribe within 30 days.
Start with the free readiness check, or read more about taking a Lovable app to production.


