Lovable apps talk to Supabase straight from the browser using your project's public key. That's by design, and it means Row Level Security (RLS) is the only thing standing between a stranger and your data. In 2025, a CVE (CVE-2025-48757) described Lovable-generated apps whose RLS policies let unauthenticated visitors read or write tables. Lovable disputed it, but the fix lives in your database either way. Run these checks before you share the link.
Supabase has two kinds of keys. The publishable key (sb_publishable_..., or the older anon key) is meant to be public. The secret key (sb_secret_..., or the older service_role key) bypasses RLS entirely and must never ship to the browser. Lovable builds with Vite, so any environment variable starting with VITE_ ends up in the JavaScript every visitor downloads.
To check: open your live app, press F12, go to Sources (or Network and reload), and search the JS files for service_role and sb_secret_. If you find one, rotate it in the Supabase dashboard right away, move privileged work into a Supabase Edge Function, and redeploy.
In the Supabase SQL editor, run:
select c.relname,
has_table_privilege('anon', c.oid, 'select') as anon_can_read,
c.relrowsecurity as rls_on
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r'
order by 1;Any table with anon_can_read = true and rls_on = false is public. Turn RLS on with alter table public.your_table enable row level security; and then add policies.
"RLS enabled" isn't the same as "protected". A policy of using (true) allows every row. List them:
select tablename, policyname, cmd, roles, qual, with_check from pg_policies where schemaname = 'public' and (qual = 'true' or with_check = 'true');
Keep one only if that table is meant to be public (a product catalog, for example). For user data, scope it to the owner:
create policy "owners read their rows" on public.orders for select to authenticated using ((select auth.uid()) = user_id);
Write separate policies for insert, update, and delete, and use with check on insert and update so users can't write rows that belong to someone else.
The Supabase dashboard and SQL editor bypass RLS, so your data always looks fine there. Test the way an outsider would: open your app signed out in a private window, copy the public key from the Network tab, and request a private table:
curl "https://YOURPROJECT.supabase.co/rest/v1/orders?select=*" \ -H "apikey: YOUR_PUBLISHABLE_KEY"
Rows from real users mean the policy is broken. An empty list [] or a permission error is what you want.
In your Supabase project, open the Advisors section and run the Security Advisor. It flags tables without RLS, exposed views, and risky function settings. Review each finding before dismissing it.
Storage buckets have their own policies. A bucket marked public serves every file to anyone with the URL, so keep invoices, IDs, and uploads in private buckets. Also look for database functions marked security definer that the anon role can call, since they run with elevated rights.
Once the data layer is locked down, check the basics on the live site: HTTPS everywhere, plus headers like Strict-Transport-Security, X-Content-Type-Options: nosniff, and a sensible Content-Security-Policy. If your hosting doesn't let you set response headers directly, you can add them with a proxy such as Cloudflare in front of your custom domain, or deploy the exported project to Netlify or Vercel where headers live in a config file. See our security headers checker guide for copy-paste configs.
Want a second opinion in under a minute? Scan your site free with SiteShield. It checks your live app's HTTPS, security headers, privacy policy and cookie notice, and email SPF/DMARC (the Supabase RLS and key checks above are done in your Supabase project) and shows your score and every issue found. If you'd rather skip the trial and error, the $14 fix bundle gives you copy-paste fixes written for your exact site.
SiteShield by Telekeen · Telekeen Communications