← Back to blog
Analysis Apr 7, 2026 · 4 min read

Why Supabase RLS is the #1 vibe-coding mistake

One setting. Disabled by default. Exposes every user's data. Repeated across hundreds of apps. Here's why.

If you've followed our scanning batches, you've seen the pattern. In our most recent run of 28 Supabase-backed apps (Lovable, Bolt, Replit), 5 apps had at least one table readable by anyone holding the public anon key. The worst single app had 11 tables exposed. Across the cohort: 13 individual table-leak findings.

That's 18% of apps with a critical RLS misconfiguration on a single failure mode. Not a long-tail bug — a recurring shape.

The model

Supabase exposes a single Postgres database via PostgREST. There are two keys:

Once a user logs in, their browser presents a third JWT signed by Supabase Auth that includes their sub (user ID). That's the value auth.uid() reads inside RLS policies.

All requests hit the same PostgREST endpoint. What the database returns depends entirely on which JWT is presented and what RLS policies say about it.

What RLS actually controls

RLS = Row Level Security, a Postgres feature (not a Supabase invention). For each table you can:

Two failure modes, both common:

  1. RLS off. Every table row is anon-readable.
  2. RLS on, permissive policy like USING (true). Same effect — every row is anon-readable.

Why "the default" is more nuanced than you'd think

Two ways to create a table in Supabase:

AI assistants — Lovable, Bolt, v0, Cursor with Supabase MCP — overwhelmingly go through the SQL path because that's what they're trained to write. So the apps we scan, which are predominantly AI-scaffolded, end up with the SQL-path default: RLS off.

The failure is per-table, not per-project

The pattern we see most often: a developer follows the official "build a Twitter clone" tutorial, enables RLS on the profiles table when prompted, writes a couple of policies. They internalize "I've configured RLS." Three months later they (or their AI assistant) add invoices, customer_contacts, subscription_history tables via SQL — RLS off, no policies, anon-readable.

One real example from our last batch: an app with profiles RLS-gated correctly, but 11 other tables added later — all wide open. Customer emails, account passwords (in a literal column), booking requests with phone numbers, chat messages.

Three concrete fixes

1. The "every new table" habit

If you write SQL by hand, make this the snippet you reach for:

CREATE TABLE x (id bigint primary key, owner_id uuid, ...);

ALTER TABLE x ENABLE ROW LEVEL SECURITY;

CREATE POLICY "owner_can_select" ON x
  FOR SELECT USING (auth.uid() = owner_id);
CREATE POLICY "owner_can_modify" ON x
  FOR ALL USING (auth.uid() = owner_id);

The ALTER + 2 policies are the minimum viable scaffold. Adjust the predicate per table.

2. A pre-deploy lint

Add a CI check that fails the build if any public-schema table has RLS off. Run this against your migration's resulting state, not against prod:

SELECT n.nspname, c.relname
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
  AND n.nspname = 'public'
  AND c.relrowsecurity = false;

Empty result = pass. Any rows = fail the build with the offending table names.

3. Continuous external scan

The CI check catches new tables before deploy. An external scan catches drift after deploy — schema migrations, manual SQL, third-party functions. We do this for free on one target; the paid plans run it weekly and email you if anything new broke.

The platform fix

If you're a Lovable / Bolt / Cursor template author or work on the Supabase team: surface the check above when an AI scaffold creates a new table via SQL. Five milliseconds of work, would prevent more critical disclosures than any single change in the AI-tooling ecosystem right now. The friction is purely social — devs see a "Enable RLS now?" prompt, click yes, write a policy. Don't ship.

The ecosystem will fix this eventually. Until then, scan before you launch.

Run the same scan on your app

One free scan, no credit card. Works with any URL or IP — finds the issues from this post and more.

Start free