You just launched your Supabase project. It works. Users are signing up. You're proud of it.
Then you get a message: "Hey, I can see everyone's data."
This happens more than you'd think. And the cause is almost always the same: Row Level Security was enabled, but the policies were wrong — or missing entirely.
Let me show you exactly how this happens, how to check if your project is affected, and how to fix it.
What Is RLS and Why Does It Matter?
Supabase uses PostgreSQL's Row Level Security to control which rows a user can read, insert, update, or delete. When you enable it, access is denied by default — until you create policies that explicitly allow access.
The problem: enabling RLS and creating correct policies are two separate steps. You can do one without the other.
And Supabase's dashboard will show your table as "RLS enabled" — technically true, but completely misleading if you have no policies.
The Most Common Mistake: Enabling RLS Without Policies
Here's what a dangerous table looks like:
-- RLS is enabled
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
-- But no policies exist
-- Result: NO rows are returned for authenticated users
-- BUT: anon users can still read everything if you have a permissive grant
Or worse — the AI-generated version that "works" in testing:
-- The classic AI-generated mistake
CREATE POLICY "Enable read access for all users"
ON user_profiles FOR SELECT
USING (true); -- This allows EVERYONE to read EVERYTHING
I've seen this pattern in dozens of projects. Cursor, Lovable, and Bolt generate it because it makes tests pass. It's not secure.
What an Attacker Sees
Your frontend bundles your Supabase URL and anon key. This is normal — it's designed this way. The anon key is meant to be public.
The problem is what that key can access.
Here's how easy it is to check:
# Anyone can run this with your public anon key
curl 'https://YOUR_PROJECT.supabase.co/rest/v1/user_profiles?select=*' \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_ANON_KEY"
If your RLS policies are wrong, this returns all user data. Email addresses, names, profile information — everything in that table.
No authentication required. Just your public anon key, which is already in your JavaScript bundle.
The Four RLS Failure Modes
After analyzing many Supabase projects, these are the patterns I see most often:
1. Missing policies on new tables
You add a new table in a hurry. You enable RLS (or forget to). No policy. The table is either locked (nothing works) or wide open (everything is readable).
2. USING (true) policies
The "it works now" shortcut. Every row is readable by every user. Often copy-pasted from documentation examples that were never meant for production.
3. Permissive storage buckets
-- This bucket is publicly readable
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);
If you store anything sensitive in a "public" bucket — even with a non-obvious path — it's accessible to anyone with the URL.
4. Overpermissive service role usage
Some developers use the service_role key in their frontend for "simplicity." The service role bypasses RLS entirely. It should never be in client-side code.
How to Check Your Project Right Now
Step 1: Check your RLS status
-- Run this in Supabase SQL Editor
SELECT
schemaname,
tablename,
rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
Any table where rowsecurity = false is unprotected.
Step 2: Check your policies
-- See all policies on public tables
SELECT
schemaname,
tablename,
policyname,
permissive,
roles,
cmd,
qual,
with_check
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename;
Look for:
- Tables with no policies at all (but RLS enabled)
- Policies with
qual = '(true)'— these allow everyone - Policies that don't check
auth.uid()
Step 3: Check your storage buckets
SELECT id, name, public
FROM storage.buckets;
If public = true, anyone can list and access files in that bucket.
Step 4: Check your auth settings
In your Supabase dashboard → Authentication → Settings:
- Is email confirmation required? (it should be for production)
- Is there a password minimum length? (8+ characters)
- Are there rate limits on sign-ups?
The Correct Pattern
Here's what secure RLS looks like:
-- Users can only read their own profile
CREATE POLICY "Users can read own profile"
ON user_profiles FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
-- Users can only update their own profile
CREATE POLICY "Users can update own profile"
ON user_profiles FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- No insert from client (handle in Edge Function)
-- No delete from client (handle in Edge Function)
The key parts:
-
TO authenticated— only logged-in users -
auth.uid() = user_id— only their own rows - Separate policies for SELECT, INSERT, UPDATE, DELETE — don't use a blanket policy
For Vibe Coders Specifically
If you built your project with Cursor, Lovable, Bolt, or v0 — check your RLS policies manually. These tools are excellent at building features quickly, but they optimize for "it works" over "it's secure."
The generated code often includes USING (true) policies because they're needed to make the demo work in development. Before you go live, replace them with proper user-scoped policies.
This is not a criticism of AI coding tools — it's just how they work. Security is your responsibility, not the tool's.
Automating This Check
Manually auditing your Supabase project is doable but tedious. If you want to automate it — especially if you manage multiple projects — there are a few tools:
- SupaSec (open source): bundle scraping + basic RLS check
- FounderScan ($19 one-time): 12+ security checks including Supabase
- AEGIS (my project): continuous monitoring, active testing, plain-language report with fix commands
The important thing isn't which tool you use — it's that you check at all. Most projects I've looked at have at least one issue. Many have several.
TL;DR
- Enabling RLS ≠ being secure. You need policies too.
-
USING (true)is almost never what you want in production. - Public storage buckets are public. Don't store sensitive data in them.
- The
service_rolekey bypasses RLS. It belongs on the server, not in the browser. - Run the SQL queries above to check your project right now. It takes 5 minutes.
If you find something, fix it before someone else does.
I'm building AEGIS — automated Supabase security scanning with continuous monitoring. If you want to be notified when it launches, join the waitlist.
Top comments (0)