How to secure a Lovable app before launch
Securing a Lovable app before launch mostly means securing its Supabase database. Turn on row-level security (RLS) for every table, write a policy for each operation that limits users to their own rows, and test the result by signing in as two different users. Then keep secret keys out of the browser, make edge functions identify the caller from the session, take payment status only from your payment provider's webhook, and run a scan before you publish.
1. Find every table without RLS
A Lovable app's browser code talks to Supabase directly with a public key, so RLS is what stands between that key and your data. In the Supabase SQL editor, list the tables in the public schema that don't have it:
select tablename
from pg_tables
where schemaname = 'public' and rowsecurity = false;
Anyone who opens your app can read and change every table this query returns. Turn RLS on for each one:
alter table public.notes enable row level security;
Supabase's Security Advisor and Lovable's publish scan report the same thing, so either can confirm the list is empty.
2. Write a policy for each operation
A table with RLS on and no policies returns nothing to anyone. When that breaks a page, the quickest change that makes the page work is a policy allowing everyone, and that is a common way RLS gets undone. Write one policy per operation instead, each tied to the signed-in user:
create policy "Read own notes" on public.notes
for select to authenticated using ((select auth.uid()) = user_id);
create policy "Add own notes" on public.notes
for insert to authenticated with check ((select auth.uid()) = user_id);
create policy "Change own notes" on public.notes
for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
create policy "Delete own notes" on public.notes
for delete to authenticated using ((select auth.uid()) = user_id);
using controls which existing rows a user can see or modify. with check controls which rows they can create or end up with, so a user can't insert a note under someone else's ID. Supabase recommends writing (select auth.uid()) rather than auth.uid() so the value is computed once per query instead of once per row.
Two policy shapes deserve a second look whenever you see them:
using (true)makes a table readable by everyone. It suits genuinely public data, such as published posts, and only forselect.using (auth.role() = 'authenticated')lets every signed-in user read every row. If sign-up is open, that includes anyone.
More patterns like these are in Supabase row-level security mistakes in AI-built apps.
3. Test signed out, as user A and as user B
Create two test accounts with some data in each, then check:
- Signed out, in a private window: no private data loads.
- As user A: only A's data appears.
- As user B: A's data can't be read or changed, including by editing an ID in the URL or in a request.
You can also query the API with only the public key. For rows a request isn't allowed to see, RLS returns an empty list rather than an error:
curl "https://YOUR-PROJECT.supabase.co/rest/v1/notes?select=*" \
-H "apikey: YOUR_PUBLISHABLE_KEY"
4. Keep secret keys out of the browser
Supabase issues two kinds of key (Supabase docs):
| Key | Looks like | Safe in browser code? | Respects RLS? |
|---|---|---|---|
| Publishable key | sb_publishable_... |
Yes | Yes |
| Legacy anon key | long token starting eyJ |
Yes | Yes |
| Secret key | sb_secret_... |
No | No, it bypasses RLS |
| Legacy service_role key | long token starting eyJ |
No | No, it bypasses RLS |
The publishable key is meant to be public; RLS is what makes that safe. A secret key in front-end code gives every visitor full access to your database. The two legacy keys look alike, and the difference is the role claim inside the token (anon or service_role). Supabase plans to retire the legacy keys by the end of 2026.
Build tools also decide what reaches the browser by variable name. Anything prefixed VITE_ (Lovable's setup), NEXT_PUBLIC_ or PUBLIC_ is bundled into front-end code. That is fine for your Supabase URL and publishable key, and a leak for a Stripe secret key (sk_live_...), an AI provider's API key or a webhook signing secret. Search your code for these, then open your published site's JavaScript in the browser's developer tools and search it too.
If a secret key was ever exposed, replace it even after removing it from the code, since old builds and your Git history still contain it. Create a new key, switch every server and function to it, confirm nothing uses the old one, then delete the old key.
5. Make edge functions identify the caller
An edge function that accepts user_id in the request body will act for whichever ID it receives. Create the Supabase client with the request's Authorization header, call supabase.auth.getUser(), reject the request if no user comes back, and use that user's ID.
6. Take payment status only from your payment provider
Mark an order as paid, or unlock a plan, only in a webhook handler that verifies the provider's signature (for Stripe, the Stripe-Signature header). A success message from the browser can be faked.
7. Keep roles out of user metadata
Supabase users can edit their own user_metadata. A policy or check that reads a role from it lets users promote themselves. Keep roles in app_metadata, which only your server can change, or in a table protected by RLS.
8. Keep private files in private buckets
Anyone with the link can read a file in a public storage bucket. Put private files in a private bucket, with policies on storage.objects that restrict each user to their own folder.
9. Scan before you publish
Lovable's quick scan runs when you open the publish dialog and covers database rules, critical dependency vulnerabilities and MCP exposure. Its deep scan reviews the whole codebase but only runs when you start it from the Security view, so start it before launch. Repeat steps 3 and 9 after any significant change to sign-in, data access or payments.
Running these checks on every change
Cyfendo connects to Lovable as an MCP connector and checks each task that touches sign-in, data access, Supabase policies, payments, uploads or secrets. For Supabase apps it builds a stand-in database from your migration files, with two test users, and repeats the requests from step 3. Problems it reproduces come back to the Lovable chat with fix steps, and Lovable can ask Cyfendo for a patch that is tested against the same requests. See the Lovable setup guide.