You built it in a weekend. It looks good, the demo works, and now you have to decide whether to point real users and real data at it. That is a reasonable thing to be nervous about, because the platform cannot answer the question for you. Lovable says so itself. Its own blog puts it plainly: "The Security Scan catches many issues, but sometimes incorrect database security setups slip through… accidentally hardcoding API secrets… or leaving Supabase Edge Function endpoints unprotected." (Lovable's blog)
The measurements behind that gap are not theoretical. A June 2026 scan of 62 Lovable apps found 63% carried critical or high severity vulnerabilities, with an average of 10 findings per app (DEV.to, Jun 2026). VibeSafe studied 400 public repositories built with Lovable, Bolt and v0 and found 59% had at least one critical issue, while 18.5% had committed a .env file to the repository. Escape DAST analysed 5,600 vibe-coded apps and found 2,038 critical vulnerabilities and 400+ exposed credentials (via Lifehacker, Aug 2026). CVE-2025-48757, the 2025 Lovable breach, exposed 170 production apps (cited in the DEV.to checklist). That last one is context, not a threat to you. It shows what a shared class of infrastructure can do.
So the useful question is not whether Lovable is safe. It is which specific things to look at before you ship.
1. The Edge Function that trusts the browser
This is the classic Lovable shape: an Edge Function holding the admin key, reading the session out of the cookie, and taking the target account from the request body.
// supabase/functions/delete-user/index.ts
const supabaseAdmin = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
);
Deno.serve(async (req) => {
const { data: { session } } = await supabaseAdmin.auth.getSession();
const userId = session.user.id;
const { target_user_id } = await req.json();
const { error } = await supabaseAdmin
.from('profiles')
.delete()
.eq('id', target_user_id);
...
});
Three problems in a dozen lines. Edge Functions run with the service_role key, which bypasses Row Level Security completely, so nothing downstream will stop that delete. getSession() reads a cookie without re-checking its signature with the auth server, so the "who is calling" answer comes from the caller. And target_user_id comes from the request body, so the caller chooses whose account gets deleted.
The fix is to verify the caller on the server and never take an identity from the request:
const { data: { user }, error } = await supabaseAdmin.auth.getUser();
if (error || !user) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });
}
const { target_user_id } = await req.json();
Then check that the verified caller is allowed to delete that account, usually an admin role or their own account, before running the delete. The scanner's authorisation check flags the unverified session and the request body separately, and the row-level security check deals with the third.
2. The environment file that got committed
git ls-files | grep -i '^\.env'
If that prints a file, your database admin key and your live payment keys are in your git history, on your hosting provider and in every clone of the repository. Deleting the file does not remove it. The key has to be rotated in the provider's dashboard, and .env has to go into .gitignore. This was true in 18.5% of the 400 public repositories VibeSafe looked at.
3. The debug route
router.get('/api/debug', (req, res) => {
res.json({
ok: true,
env: process.env,
serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
stripeKey: process.env.STRIPE_SECRET_KEY,
});
});
One route, three secrets, handed to anyone who types the address. AI assistants add these while building and usually forget them. Deleting the route is the fix. If you still need it, block it in production.
4. The admin page with no check
router.get('/admin/users', async (req, res) => {
const { data } = await supabaseAdmin.from('profiles').select('*');
res.json({ users: data });
});
The path says admin. Nothing in the handler verifies that the caller is one. The scanner looks for admin-shaped routes and checks the handler for a role test.
5. The admin key in the browser bundle
// src/lib/supabase.js - this file is compiled into the JavaScript
// that every visitor downloads
const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY,
);
The service_role key bypasses every row-level rule. With a VITE_ prefix, the bundler inlines it into the JavaScript, so every visitor ends up holding a full database admin key. Escape DAST found 400+ exposed credentials across the 5,600 vibe-coded apps it analysed (via Lifehacker, Aug 2026). The fix is to rename the variable so it no longer carries a browser-exposed prefix, move every call that uses it to an Edge Function or a server route, rebuild, delete the old build folder, and rotate the key afterwards.
One more fact worth knowing before you keep building. Shipping more features makes this worse, not better: vibe-eval measured 37.6% more critical vulnerabilities after just five AI build iterations.
What to do next
These five checks are exactly what the $199 review covers, run across your code, migrations, git history and build output in 48 hours at a fixed price. You get the findings ranked by how much they would hurt, the fix for each one written out, and a recorded walkthrough so you can follow it. It is a code and configuration review, not a penetration test, and it is not a guarantee that your app is secure. It is a list of what was checked, what was found, and what is still unknown.
The scanner that finds this is open source: github.com/rafikulislammollaq-dev/vibeaudit
It also runs in the browser, where your project is read on your own machine and never uploaded: rafikulislammollaq-dev.github.io/vibeaudit/scanner
If you would rather have a person read the findings and explain them, that is a $199 review: rafikulislammollaq-dev.github.io/vibeaudit
Top comments (0)