Lovable gets you from idea to a working app very fast. It writes a React frontend, connects it to Supabase, and can add login and payments for you. The question most builders ask right before launch is simple: is this actually safe to put in front of real users?
The honest answer is "it depends on a handful of specific things," and most of them can be checked in an afternoon. This guide walks through what to look at, why it matters, and how to fix it.
How a Lovable app is put together
A typical Lovable project has three parts:
- A React single-page app that runs entirely in the user's browser. Everything in it, including every environment variable it reads, can be seen by anyone who opens developer tools.
- A Supabase project that stores your data (Postgres), handles login (Supabase Auth), stores files (Storage), and runs server code (Edge Functions).
- Optional integrations like Stripe, OpenAI or email providers, usually called from Edge Functions.
The key point: your browser code talks directly to your Supabase database using the public "anon" (or "publishable") key. That is how Supabase is designed to work, and it's fine. But it means the database itself has to decide who can read and write each row. That job belongs to Row Level Security (RLS).
1. Check Row Level Security on every table
If a table in the public schema has RLS turned off, anyone with your anon key (which is in your frontend) can read and often modify the whole table through Supabase's REST API. No login required.
Open the Supabase dashboard, go to Table Editor, and look for tables marked "RLS disabled" or "Unrestricted." Or run this in the SQL editor:
select schemaname, tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by tablename;
Any row with rowsecurity = false needs attention. Turning RLS on is one line:
alter table public.orders enable row level security;
With RLS on and no policies, the table is locked to everyone except the service role. Then add policies for exactly what your app needs:
create policy "Users read their own orders"
on public.orders for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "Users create their own orders"
on public.orders for insert
to authenticated
with check ((select auth.uid()) = user_id);
Watch for policies that look like protection but aren't, such as using (true) on a table holding personal data, or a policy granted to anon when only signed-in users should have access. Our Supabase RLS checklist goes deeper.
2. Make sure no secret keys are in the frontend
Some keys are meant to be public. Others must never reach the browser.
| Usually safe in the browser | Never in the browser |
|---|---|
| Supabase URL and anon / publishable key | Supabase service_role / secret key |
Stripe publishable key (pk_live_...) |
Stripe secret key (sk_live_...) or restricted keys |
| Analytics site IDs | OpenAI, Anthropic, Resend, Twilio and other API keys |
The Supabase service_role key bypasses RLS completely. If it ships in your bundle, every protection above is irrelevant. The same goes for an OpenAI key: anyone can copy it and run up your bill.
Search your code for anything that looks like a secret:
grep -rnE "sk_live_|sk_test_|service_role|sk-[A-Za-z0-9]{20,}" src/
If a secret key was ever committed, rotate it at the provider. Deleting it from the code isn't enough, because it stays in your git history. Then move the call that needs it into a Supabase Edge Function and store the key as a function secret. More detail is in exposed API keys in NEXT_PUBLIC and VITE_ env vars.
3. Protect your Edge Functions
Edge Functions are where your secret keys live, so they need their own checks. A function that calls OpenAI or creates a Stripe session should confirm who is calling it:
// supabase/functions/generate/index.ts
import { createClient } from "npm:@supabase/supabase-js@2";
Deno.serve(async (req) => {
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{ global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } } },
);
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response("Unauthorized", { status: 401 });
// ...do the work for this user only
return Response.json({ ok: true });
});
Also think about cost. A function that calls a paid API should limit how often one user can call it.
4. Don't trust the browser for anything that matters
Because the frontend runs on the user's machine, anything it decides can be changed. Common mistakes:
- Hiding admin pages with an
if (isAdmin)in React. The page might be hidden, but the data behind it is still readable if RLS allows it. Enforce admin access in RLS policies or server code. - Marking an order as paid from the success page. Anyone can open
/successdirectly. Mark orders paid only from a verified Stripe webhook. See Stripe webhook signature verification. - Letting users edit a
roleorplancolumn on their own profile. If your update policy allows users to update their own row, make sure it doesn't let them change privileged columns. Put those in a separate table they can't write to.
5. Check Storage buckets
Supabase Storage uses RLS too, on the storage.objects table. A bucket marked public serves files to anyone with the URL. That's fine for logos, and it's not fine for invoices or ID photos. Keep private files in private buckets, and add policies that scope access to the owner, for example by putting each user's files under a folder named after their user ID.
6. Review login settings
In Supabase Authentication settings:
- Turn on email confirmation if your app relies on email ownership.
- Set your production Site URL and Redirect URLs so login links can't be sent to other domains.
- Turn off sign-ups if the app is invite-only.
7. Update dependencies
AI-generated projects pin whatever versions were current when they were generated. Run npm audit and upgrade anything with a known critical vulnerability, especially frameworks and auth libraries.
What we found on a sample app
We built a deliberately typical vibe-coded app (Next.js, Supabase, Stripe) with the kinds of mistakes described above and ran PreflightPR on it. The scan found 16 issues, 12 of them critical, and gave it a launch score of 0/100. The automated fix PR fixed 8 of them, and left a checklist for steps that need a human, like rotating leaked keys.
That's a sample we built, not a survey of real apps, but every issue in it is the kind of thing that shows up when code is generated quickly and nobody reviews it.
The short version
Your Lovable app is in good shape to launch if:
- Every table in
publichas RLS on, with policies scoped toauth.uid(). - Only the anon/publishable key is in the frontend.
- Edge Functions check the user before doing anything costly or private.
- Payments are confirmed by a verified webhook, not the browser.
- Private files live in private buckets.
- Dependencies have no known critical vulnerabilities.
Want a quick read on where you stand? Sync your Lovable project to GitHub, make sure the repo is public (or install the app for private repos), and run a free PreflightPR scan. It's an automated best-effort check, not a penetration test, but it catches the common, high-impact mistakes in about a minute.