5 min readSecurity
Is your Lovable app secure? A Supabase security checklist
The ten checks I run on every Lovable and Supabase app before real users arrive: row-level security, keys, storage, auth and payments.
A Lovable app is as secure as its Supabase setup. The front end is public by design: anyone can read the code their browser downloads, including your Supabase URL and anon key. What keeps your data safe is row-level security on every table, policies that check who is asking, and secrets that stay on the server. Below are the ten checks I run, in the order I run them.
Most apps I look at from Lovable, Bolt and v0 use Supabase for the database and sign-in. The AI writes the tables and the policies as you prompt, which works well until one policy is wider than it should be. The app still looks fine in the demo, so nothing tells you. The first person to notice is usually someone poking at the network tab.
Why the anon key isn't the problem
Supabase's anon key (called the publishable key in newer projects) is meant to be public. It tells Supabase which project a request is for, and the request then runs as the anon role, or as authenticated once someone signs in. Postgres row-level security (RLS) decides which rows each of those can read or change.
So the question is never "can people see my anon key?" They can. The question is "with that key, what can they do?" If a table in the public schema has RLS switched off, the answer can be: read and change every row, through the API Supabase builds for you.
VITE_ variable or in a public repo. If it has been, rotate it today.The checklist
- RLS is on for every tableIn the Supabase dashboard, a table without RLS is marked in the table editor. To see them all at once, run the query below in the SQL editor: every row should say
true. - No policy says yes to everyone by accidentA policy with
using (true)lets anyone read that table. That is fine for public data like a product list, and a leak for anything personal. Read each policy and ask: who should see these rows? - Policies check who is askingPrivate rows need a policy that compares the row's owner with the signed-in user, like
(select auth.uid()) = user_id. Update policies need awith checktoo, or a user can move a row to someone else. - Users can't make themselves adminIf a
roleoris_admincolumn sits on a table users can update, a user can change it. Keep roles in a table only the server can write, or limit which columns an update policy allows. - The service role key only lives on the serverSearch your built JavaScript and your repo for it. My post on API keys in the front end shows how, in five minutes.
- Storage buckets aren't public by accidentFiles in a public bucket can be read by anyone with the link. Invoices, ID scans and private uploads belong in a private bucket, with policies on
storage.objectslike the ones on your tables. - Edge Functions check who is callingSupabase checks the caller’s token by default, but functions deployed with
--no-verify-jwt(common for webhooks) have to check everything themselves. Look for those first. - Payments are confirmed on the serverAn order should become "paid" because Stripe told your server so in a signed webhook, never because the browser reached a success page. Check the webhook verifies its signature.
- Sign-in settings match the appIn Authentication settings, the redirect URLs should list only your own domains, and email confirmation should be on if your app trusts email addresses.
- Supabase's Security Advisor is cleanThe dashboard has a Security Advisor that flags common problems, including tables without RLS. Run it and read every warning; it takes two minutes.
select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;A table of private rows, done properly, looks like this. Each user can read and change only their own profile:
alter table public.profiles enable row level security;
create policy "Users read their own profile"
on public.profiles for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "Users update their own profile"
on public.profiles for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);Test it the way a stranger would
Reading policies is good. Trying them is better. Make two test accounts on your live app. Sign in as the first, open your browser's developer tools, and watch the Network tab while you use the app. Find a request to /rest/v1/, copy it, and change it to ask for the second account's rows, or remove the filter altogether.
Then try it with no account at all. The request below asks for every profile using only the public key, which is exactly what anyone on the internet can do:
curl 'https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*' \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_ANON_KEY"If that returns other people's rows, the policy is too wide. Only ever do this on your own app.
If you find a hole
- Switch on RLS and add the narrowest policy that keeps the app working. Test again with both accounts.
- Rotate any secret key that was exposed, in the project’s API settings, and update it wherever the server uses it.
- Look through Supabase's logs for reads you can't explain.
- If personal data may have been seen by someone who shouldn’t have it, get advice the same day. In the UK and EU, some breaches have to be reported within 72 hours.
None of these checks is hard, but each is easy to miss when the app already looks finished. If you’d like someone else to go through them, this is the core of my code audit, and you can see what the report looks like in this sample.
Questions people ask
- Is Lovable secure?
- Lovable is a tool; how secure your app is depends on the database rules and keys in your project. A Supabase app can be properly secure, but only if every table has row-level security with policies that match who should see what. Check it, don't assume it.
- Is it safe that my Supabase anon key is visible in the browser?
- Yes. It's designed to be public, as long as row-level security is on for every table and the policies are right. The key to protect is the service role (secret) key, which skips row-level security.
- How do I check if RLS is enabled in Supabase?
- The table editor marks tables without it, and the Security Advisor in the dashboard lists them. For the full picture, run
select tablename, rowsecurity from pg_tables where schemaname = 'public'in the SQL editor. - Do automated security scans replace an audit?
- They catch common problems fast, like a table without row-level security, so run them. What they can't judge is whether a policy matches your business, for example whether one team should see another team's invoices. That needs someone to read the rules against how the app is meant to work.