Your Supabase RLS says 'enabled'. Here's why it's still open.
There is a dangerous misconception in the Supabase ecosystem: developers see the green "RLS Enabled" badge in their dashboard and believe their data is protected. It is not. RLS enabled and RLS secured are two completely different things.
Understanding RLS in Postgres
Row Level Security in PostgreSQL works in two steps. First, you enable it on a table with ALTER TABLE ... ENABLE ROW LEVEL SECURITY. This tells Postgres to check policies before allowing access. Second, you create policies that define who can do what.
Here is the critical detail: when RLS is enabled but no policies exist, access is denied by default. This is actually secure. The problem starts when you add a policy like this:
CREATE POLICY "enable_all" ON my_table
FOR ALL USING (true) WITH CHECK (true);
This policy says: "For all operations (SELECT, INSERT, UPDATE, DELETE), allow access if true." Since true is always true, this grants unrestricted access to everyone, including anonymous users hitting your API with just the anon key.
Why AI tools generate this pattern
When you prompt an AI to create a table and "enable RLS," it does exactly what you ask. It enables RLS. But the AI also wants your app to work immediately, so it adds a permissive policy to avoid "permission denied" errors during development. The AI does not know whether your table contains private user data or public content. It defaults to making things work.
This is the core tension of vibe coding: the AI optimizes for functionality, not security. The app works, the demo looks great, and nobody realizes the database is wide open until someone exploits it.
What USING(true) actually exposes
With an anon key (which is always exposed in client-side JavaScript) and a USING(true) policy, an attacker can:
- Read every row in the table using
GET /rest/v1/table_name?select=* - Insert arbitrary data if the policy includes
WITH CHECK(true) - Update or delete any row if the policy covers those operations
- Enumerate your entire schema through the PostgREST API
This is not theoretical. We have seen real apps leaking email addresses, phone numbers, payment information, and private messages because of this exact pattern.
The secure alternative
Every policy should reference auth.uid() to scope access to the authenticated user:
-- Users can only read their own data
CREATE POLICY "read_own" ON my_table
FOR SELECT USING (auth.uid() = user_id);
-- Users can only insert their own data
CREATE POLICY "insert_own" ON my_table
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Users can only update their own data
CREATE POLICY "update_own" ON my_table
FOR UPDATE USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
For tables with mixed visibility (some public, some private fields), use a Postgres view or a separate public-facing table with limited columns.
Check your policies now
Open the Supabase SQL Editor and run:
SELECT schemaname, tablename, policyname, permissive, cmd, qual
FROM pg_policies
WHERE schemaname = 'public';
If you see qual = true on any row, that table is exposed. Or run a free Sekrd scan and we will check every policy for you automatically.
Don't ship until you're sekrd
Run a free scan to find the vulnerabilities your AI missed.
Scan Your App Free