Back to blog

I Found 7% of AI-Built Supabase Apps Wide Open. Here's How to Lock Yours.

A 2026 audit of 1,764 vibe-coded apps found that 7% had completely open Supabase databases. Here are the four RLS mistakes AI tools make, with copy-paste fixes.

May 8, 20267 min read
supabase
rls
row-level-security
database-security

Header image

A friend shipped a marketplace app last month. Auth, Stripe, realtime chat — the whole thing built with Bolt in a weekend. He was proud of it. Rightly so.

I opened DevTools and grabbed his Supabase project URL from a network request. Then I pasted it into a simple curl command with his anon key. No auth. No token. Just a GET request.

Every user profile came back. Emails, signup dates, internal IDs. The whole table. Wide open.

He stared at his screen for about ten seconds. Then he whispered, "Row Level Security?"

Yeah. Row Level Security.

Why This Keeps Happening

The 2026 vibe-coded app audit didn't surprise me. Out of 1,764 apps scanned, 7% had no Row Level Security enabled at all. Not misconfigured — just straight-up absent. Another 15% of Bolt-generated apps shipped hardcoded Supabase service role keys in client-side JavaScript.

This isn't a Supabase problem. Supabase gives you one of the best security models in the business. The problem is that AI coding tools optimize for working code, and a Supabase project works beautifully without RLS. Queries succeed. Data flows. Your dashboard renders. Everything looks perfect — right up until someone discovers your project URL and runs SELECT * FROM users.

The emotional gut-punch is real. You didn't make a mistake. You used a tool. The tool generated code that works. And "works" is not the same as "secure."

What Row Level Security Actually Does

RLS is not middleware. It's not an application-layer check. It's Postgres itself, at the database level, filtering every query before it returns a single row.

When RLS is enabled on a table, the database runs a policy check against every row before including it in a result. If the policy says no, that row might as well not exist.

Here's what a correct policy looks like:

CREATE POLICY "Users can read their own data" ON public.profiles FOR SELECT USING (auth.uid() = user_id);

Figure 2: A correct RLS policy — only returns rows where user_id matches the authenticated user's ID.

This means: for every SELECT against profiles, only return rows where user_id matches the authenticated user's ID. If nobody's logged in, auth.uid() returns null, and null equals nothing. Zero rows returned.

The database enforces this. Not your app. Not your middleware. Not a client-side if statement. The database.

The Four Mistakes AI Tools Keep Making

I've reviewed hundreds of AI-generated Supabase schemas. The same four patterns appear over and over.

Mistake 1: RLS Never Enabled

-- What AI tools generate: CREATE TABLE public.user_profiles ( id uuid PRIMARY KEY, email text, credit_card_last_four text, api_keys text ); -- What's missing: ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;

Figure 3: AI-generated table without RLS enabled — every row is visible to anyone with the anon key.

Without that one line, every row is visible to anyone holding your anon key. And your anon key is public by design — it sits in your client code, visible in every browser DevTools panel.

Fix: Add ALTER TABLE public.<name> ENABLE ROW LEVEL SECURITY for every table. Then add at least one policy. A table with RLS enabled but zero policies defaults to denying everything — which is safe, but your app won't work until you add the right policies.

Mistake 2: The USING (true) Trap

-- Dangerously permissive — AI-generated anti-pattern CREATE POLICY "Allow all access" ON public.user_profiles FOR ALL USING (true);

Figure 4: The USING (true) anti-pattern — functionally identical to disabled RLS but looks like a configured policy.

USING (true) means "this check passes for every row." It's functionally identical to having RLS disabled, but it looks like you did something. AI tools generate this because it makes the app work immediately and the policy name sounds plausible.

Fix: Never use USING (true) on tables containing user data. Every policy should reference auth.uid() or check against the authenticated user's role.

Mistake 3: Service Role Key in the Browser

// ❌ Found in live Bolt.new apps — service_role key in client code const supabase = createClient( 'https://abc123.supabase.co', 'eyJhbGciOi...service_role...' // ☠️ BYPASSES ALL RLS );

Figure 5: Service role key exposed in client-side JavaScript — bypasses every RLS policy and grants full database access.

The service role key bypasses every RLS policy. Every. Single. One. Putting it in client-side JavaScript means anyone who views your page source can read, write, and delete your entire database.

Fix: Use sb_publishable_* keys on the client. Those respect RLS. Reserve sb_secret_* keys for server-side code only — API routes, edge functions, background jobs.

Mistake 4: Not Handling Unauthenticated Users

-- ❌ Returns zero rows for unauthenticated visitors CREATE POLICY "Users can read own data" ON public.documents FOR SELECT USING (auth.uid() = owner_id);

Figure 6: A policy that returns zero rows for unauthenticated users — auth.uid() is null, and null never equals anything.

When nobody's logged in, auth.uid() is null. And null = owner_id is never true — so this policy returns nothing. If you have public documents, nobody can read them.

Fix: Create separate policies for public and authenticated access:

-- Public documents: anyone can read CREATE POLICY "Anyone can read public docs" ON public.documents FOR SELECT USING (is_public = true); -- Private documents: owner only CREATE POLICY "Owners can read their docs" ON public.documents FOR SELECT USING (auth.uid() = owner_id);

Figure 7: Separate policies for public and authenticated access — Postgres combines them with OR logic.

Postgres evaluates all applicable policies and combines them with OR logic. If any policy passes, the row is included.

The Five-Question Checklist

After generating a Supabase app, go through this:

#QuestionIf No
1Does every table have ENABLE ROW LEVEL SECURITY?Your data is public
2Does every table have at least one policy?Your app is broken (all access denied)
3Does any policy use USING (true) on a sensitive table?RLS is cosmetic — data is public
4Is your service role key only in server-side code?Anyone can nuke your database
5Does every policy check auth.uid() or an explicit public flag?Edge cases will leak or break

Figure 1: The five-question RLS checklist — run through this after every Supabase generation session.

You can audit this manually in the Supabase dashboard under Authentication → Policies. Or you can run it through VibeShield and get answers in 30 seconds.


Summary

Found this useful? Scan your Supabase app at vibeshield.org — free, no account needed. The scanner checks RLS configuration, exposed keys, and cookie flags in one pass.

Read next: I Shipped an AI App in 4 Hours. Then I Found My Stripe Key in the Browser for leaked API keys. Or Your App Scored 48/100. Here's What That Actually Means to understand your security score.