Over the past two weeks we ran Security Scanner against ~50 apps published on Lovable. About 28 of them used Supabase as the backend (we detected the Supabase URL + anon key in the JS bundle). Of those Supabase-backed apps, roughly 1 in 5 had at least one Supabase table readable by anyone holding the anon key.
Here are the top 5 issue patterns, ranked by how often they showed up.
1. Row Level Security disabled on app-specific tables (~18% of Supabase-backed apps)
By far the most common critical finding. RLS is per-table in Postgres, and if a table is created via plain CREATE TABLE in the Supabase SQL Editor (which is what most AI assistants do when they scaffold a new feature), RLS is off by default. The dashboard's Table Editor flips it on; SQL doesn't.
Tables we've found anon-readable across this cohort include: client_emails (with a literal password column), booking_requests (customer emails + phone numbers), client_accounts (a credential vault — service names, emails, passwords for things like Namecheap), chat_channels, chat_messages, subscriptions, profiles.
Fix:
ALTER TABLE <table> ENABLE ROW LEVEL SECURITY;
CREATE POLICY "owner_select" ON <table>
FOR SELECT USING (auth.uid() = owner_id);
2. Supabase storage buckets publicly listable
If a table leaks, storage usually leaks too. The same anon key that can SELECT from an unprotected table can also POST to /storage/v1/object/list/<bucket> and enumerate every file in a bucket. On the worst app in our batch we found 8 buckets listable — user avatars, receipts, task attachments, chat files, business cover images, event attachments.
Fix: In the Supabase dashboard under Storage → Policies, scope the SELECT (and UPDATE/DELETE) policies to authenticated users or the specific owner. The default of "no policies + RLS-on" leaves the bucket inaccessible — the failure mode is a misconfigured "allow all" policy or RLS off on the storage tables.
3. Supabase anon key mistaken for a secret
The opposite mistake: people see SUPABASE_ANON_KEY in the JS bundle, panic, and try to "hide" it (sometimes by rotating it repeatedly, sometimes by adding env-var indirection, sometimes by writing a server proxy to forward calls). The anon key is designed to be public — it's a JWT with role: anon in the payload, and RLS does the actual authorization.
The Supabase key that should never ship to the browser is service_role — same JWT shape, but with role: service_role in the payload. That key bypasses RLS on every table. Decoding the middle segment of any eyJ... JWT and checking the role field disambiguates the two.
We didn't observe a service_role leak in this Lovable cohort — but we've seen the pattern in adjacent ones, and our scanner now decodes JWT payloads to flag service_role specifically.
Fix: Keep the anon key client-side, enable RLS, and never paste a service_role key into any client-visible env var (including Lovable secrets that get baked into the bundle).
4. Missing security headers (every app)
Lovable's edge doesn't set HSTS, X-Frame-Options, CSP, or Referrer-Policy on its *.lovable.app subdomains. Every single app we scanned has the same stack of MEDIUM findings for this. Browser-level clickjacking and MIME-confusion protections aren't there by default.
Fix: Mostly on Lovable to set as platform defaults. If you've moved your app to a custom domain behind Cloudflare, set the headers there instead — Cloudflare → Rules → Transform Rules → Modify Response Header.
5. Debug/admin routes visible in the bundle
Less common but high-impact. We've seen JS code branching on if (user.is_admin) where the admin UI was fully shipped to the browser — including the API calls it makes. Disabling the admin UI client-side is not security: an attacker reads the bundle, sees the /api/admin/delete-user call, and invokes it directly. If the endpoint doesn't recheck auth + role server-side, they're in.
Fix: Check auth.uid() + role inside every Supabase RPC function and every RLS policy. Assume the client is hostile.
The meta-lesson
Lovable + Supabase is a great stack. The failure mode isn't the stack — it's that developers add new tables over time and forget RLS is per-table. A template-level lint rule from Lovable ("this table has no RLS policy") would catch every CRIT we've reported.
Until that lands, run a scan before you launch anything that has a table you didn't create on day one.