VibeCheck

Guide · Lovable

Is my Lovable app secure?

Lovable gets an app from idea to live URL very quickly. Whether it is secure depends mostly on a few things the builder can get wrong without you noticing, because the app still works perfectly. Here is what to look at, in the order that matters.

Why a working app can still be leaking

Most Lovable apps are a React frontend plus a Supabase backend. The frontend runs in the visitor's browser, so everything in it is public: the JavaScript, the configuration, and every key that was bundled into it. Supabase is designed for this model. The browser talks to your database directly with a public key, and the database decides what each request is allowed to see.

That decision is made by Row Level Security (RLS) policies. If they are missing or too loose, your app still looks fine to you, but anyone who opens the browser developer tools can copy the public key and ask your database for every row in a table. No hacking skill is needed; it is a single HTTP request.

1. Is Row Level Security on for every table?

This is the most important check for any Supabase-backed app. In the Supabase dashboard, open the SQL editor and run:

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

Every row should say true. A table with false is readable, and often writable, by anyone who has your public anon key, which is everyone who has visited your site. Supabase's own Security Advisor (in the dashboard under Advisors) also flags tables in the public schema without RLS.

RLS being on is not the whole story. A policy such as using (true) turns the protection back off. Our Supabase RLS guide walks through reading your policies and the common mistakes.

2. Is a secret key in the frontend?

Supabase gives you two kinds of keys. The anon key (or the newer publishable key, starting with sb_publishable_) is meant to be in the browser. The service_role key (or the newer secret key, starting with sb_secret_) bypasses RLS completely and must only live on a server or in an edge function's secrets.

The same goes for Stripe secret keys (sk_live_), OpenAI or Anthropic keys, email provider keys and so on. If any of these was ever pasted into the chat and ended up in frontend code, assume it is public. The fix is always the same: rotate the key first, then move the code that uses it into a Supabase edge function. See which keys are fine in the frontend and which are not.

3. Can anyone list your storage buckets?

Supabase Storage buckets can be public or private. A public bucket means anyone with a file's URL can download it, which is fine for avatars and product images. The bigger problem is a policy on storage.objects that lets the anon role list files, because then someone can enumerate every invoice, ID scan or upload in the bucket without knowing any URL. Keep user documents in private buckets, with policies that check the folder or owner matches auth.uid().

4. Who can call your edge functions?

Lovable often moves sensitive calls (AI, payments, email) into Supabase edge functions, which is the right move. But a function that does not check who is calling is just a public endpoint with your secret key behind it. Note that the default JWT verification on edge functions accepts the anon key, because it is a valid token too. So "verify JWT" being on does not mean the caller is a logged-in user. Inside the function, get the user from the Authorization header with supabase.auth.getUser() and reject the request if there is none, or if that user should not be allowed to do this.

5. Are admin checks only in the browser?

A common pattern in generated code is something like if (user.email === "me@example.com") showAdminPanel(). That hides the button, but it protects nothing: the data behind the admin panel is still served by Supabase to whoever asks. Admin-only data needs an RLS policy (or a server function) that checks a role stored in the database, not in the browser. The same applies to "password protected" pages where the password is compared in JavaScript, because the password is then in the bundle for anyone to read.

6. Sign-up and auth settings

If your policies say "any authenticated user can read this table", then the real question is who can become authenticated. If sign-ups are open and email confirmation is off, anyone can create an account in seconds and read everything a user can. Check Authentication settings in Supabase: decide whether email confirmation should be required, and whether anonymous sign-ins should be enabled at all. Then make policies specific, for example auth.uid() = user_id, rather than "any logged-in user".

7. AI keys and direct browser calls

If your app calls OpenAI, Anthropic, Gemini or similar directly from the browser, the key is visible to every visitor and anyone can run up your bill. In code this often shows up as dangerouslyAllowBrowser: true. Route those calls through an edge function that checks the user and applies a sensible limit.

8. The smaller things

What to fix first

  1. Rotate any secret key that reached the frontend, then move its use server-side.
  2. Turn on RLS for every table and replace any using (true) policy on private data.
  3. Make document buckets private and stop anonymous listing.
  4. Add a user check inside every edge function that costs money or touches data.
  5. Move admin and password checks into the database or a server function.

You can ask Lovable to do most of this. Be specific: name the table, the policy you want ("users can only read and update their own rows, matched on user_id = auth.uid()"), and ask it to show you the SQL it ran. Lovable also has its own security review feature; running it is a good idea, and so is checking the result from the outside, the way an attacker would see your app.