# How to secure a Lovable app before launch

Cyfendo · October 20, 2026

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:

```sql
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:

```sql
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:

```sql
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 for `select`.
- `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](/blog/supabase-row-level-security-mistakes).

## 3. Test signed out, as user A and as user B

Create two test accounts with some data in each, then check:

1. **Signed out**, in a private window: no private data loads.
2. **As user A:** only A's data appears.
3. **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:

```bash
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](https://supabase.com/docs/guides/api/api-keys)):

| 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](/docs/integration/lovable).

Source: https://cyfendo.com/blog/how-to-secure-a-lovable-app
