All posts
VibeZero Team5 min read

How to secure a Lovable app

Lovable ships four predictable security gaps: RLS left off, keys in the bundle, unchecked ownership, and stale deps. Here is the code that closes each one.

  • security
  • vibe coding
  • lovable

Lovable is very good at getting a working app in front of users, and the apps it generates fail in a small, predictable set of ways. That predictability is the good news: you are not hunting for unknown bugs, you are working through a short list. Four fixes cover most of what an attacker would find, and none of them is a rewrite.

This is the order to do them in, worst first.

1. Turn on Row Level Security, then write a policy

When Lovable creates a Supabase table it does not enable Row Level Security and does not write a policy for it. With RLS off, your anon key can read the whole table, and the “login” your app has is decoration: anyone can skip the UI and query the REST endpoint directly.

This is the most common finding in Lovable apps and it is the one that produces breaches. It is what CVE-2025-48757, disclosed by Matt Palmer in May 2025, describes: more than 170 deployed Lovable projects leaking emails, API keys, and payment data through misconfigured row-level security. The same pattern produced the Moltbook breach.

Check every table:

select tablename, rowsecurity
from pg_tables
where schemaname = 'public';

Any row with rowsecurity = false is readable by anyone holding your anon key, which ships in your frontend. Enabling RLS is one statement per table:

alter table public.documents enable row level security;

Enabling it alone denies everything, which will break your app until you add a policy. Write the narrowest one that works. For a table where each row belongs to one user:

create policy "owner reads own documents"
  on public.documents
  for select
  using (auth.uid() = user_id);

create policy "owner writes own documents"
  on public.documents
  for insert
  with check (auth.uid() = user_id);

2. Get the service-role key out of the browser

Asked to call a third-party API, Lovable will often write the call directly in the frontend. That works immediately and puts the credential in your JavaScript bundle, where anyone can read it from DevTools. The same happens to the Supabase service_role key, which bypasses RLS entirely and undoes everything you did in step one.

Find out what you have shipped:

npm run build
grep -rEo "eyJ[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9]{20,}" dist/

eyJ is the start of a base64url-encoded JWT header, which is what Supabase keys are; sk- prefixes OpenAI keys. Anything that matches is public. Rotate it before you do anything else, because a key in a deployed bundle should be treated as already compromised. Credential-harvesting bots scan fresh deploys.

The anon key is supposed to be in the frontend. That is safe only once step one is done, because RLS is the thing that makes it safe.

Then move the real call server-side. In Supabase that is an Edge Function holding the secret:

// supabase/functions/summarize/index.ts
Deno.serve(async (req) => {
  const authHeader = req.headers.get("Authorization");
  if (!authHeader) {
    return new Response("Unauthorized", { status: 401 });
  }

  const { text } = await req.json();

  const upstream = await fetch("https://api.openai.com/v1/responses", {
    method: "POST",
    headers: {
      // Set with: supabase secrets set OPENAI_API_KEY=sk-...
      Authorization: `Bearer ${Deno.env.get("OPENAI_API_KEY")}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ model: "gpt-5.2", input: text }),
  });

  return new Response(await upstream.text(), {
    headers: { "Content-Type": "application/json" },
  });
});

The frontend now calls your function with the user’s session, and the secret never leaves the server.

3. Check ownership, not just identity

Lovable reliably adds authentication. It far less reliably checks that the signed-in user is allowed to touch the specific record they asked for. The result is an app where every account is valid and every account can read everyone else’s data by changing an id.

The generated version usually looks like this:

// Authenticated, and still wrong: any signed-in user can pass any id.
const { data } = await supabase
  .from("documents")
  .select("*")
  .eq("id", documentId)
  .single();

If you did step one, RLS already blocks this at the database. That is the point of doing it first: the policy is the backstop that holds even when the application code forgets. Make the intent explicit anyway, so the query fails loudly in review rather than silently returning nothing:

const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error("not authenticated");

const { data } = await supabase
  .from("documents")
  .select("*")
  .eq("id", documentId)
  .eq("user_id", user.id)
  .single();

Two layers, and they fail in the same direction. Defense in depth here is cheap.

4. Update what shipped vulnerable

Your lockfile froze at the moment the app was generated, and nothing has bumped it since. The app keeps working perfectly while known CVEs accumulate underneath it. Generated manifests also tend to reach for versions the model remembers rather than the current patched release, so a brand-new project can start life already vulnerable.

npm audit --omit=dev
npm update

Re-run your tests after the update, then check that the audit is actually clean rather than merely quieter.

What this does not cover

These four close the gaps that get Lovable apps breached, and they are not a security review. Nothing above looks at your storage bucket policies, your CORS configuration, your webhook signature verification, or the auth flows where Lovable treated a multi-step process as separate features and left a gate skippable. Those are the common vulnerability classes that need something reading the whole repository rather than a checklist.

That is the honest limit of a post like this: a checklist tells you what to look for, and it cannot tell you what is actually in your code. If you want the specifics for your app, VibeZero scans a Lovable repo across five layers and reports what it finds (see Lovable security for what that covers). Either way, the fix is only real once something other than the tool that wrote the code confirms it, which is a problem worth understanding on its own.

Predictable is fixable

The reason this list is short is not that Lovable apps are unusually secure. It is that AI code generators fail in patterns, and patterns can be checked. Work through the four in order, starting with RLS, and you close the gaps that actually get exploited. The remaining risk is the interesting kind, the sort that needs a real look at your code rather than a list.

ShareXLinkedIn