Supabase Auth in Vibe-Coded Apps: Row Level Security You Forgot to Enable - VibeDoctor 
← All Articles 🔐 Auth & Identity Critical

Supabase Auth in Vibe-Coded Apps: Row Level Security You Forgot to Enable

Supabase RLS is the #1 thing vibe coders skip. Without it, any authenticated user can read and modify every row in your database.

SEC-001 SEC-002

Quick Answer

If you built a Supabase app with Bolt, Lovable, or Cursor and did not explicitly create Row Level Security (RLS) policies, every authenticated user in your app can read, modify, and delete every other user's data. RLS is disabled by default on new Supabase tables, and AI coding tools almost never enable it.

What Row Level Security Is and Why It Matters

Row Level Security (RLS) is PostgreSQL's built-in access control system that restricts which rows a user can access in each table. In Supabase, the client library connects directly to your PostgreSQL database using a public API key. Without RLS, any authenticated user can query any row in any table using the Supabase client.

According to Supabase's own documentation, RLS is the primary mechanism for securing data access in Supabase applications. Yet in practice, AI coding tools generate Supabase client code without creating the corresponding RLS policies. The app works - every query returns data - but there is zero data isolation between users.

The 2024 Verizon Data Breach Investigations Report found that broken access control was the number one web application vulnerability category, responsible for 94% of tested applications having some form of broken access control. Missing RLS in Supabase is the textbook example of this vulnerability.

The Default State: No Protection

When you create a new table in Supabase, RLS is disabled. This means the anon key and any authenticated user's key can perform any operation on any row.

// ❌ BAD - Without RLS, this returns ALL users' messages
const { data } = await supabase
  .from('messages')
  .select('*');
// Returns messages from every user, not just the current user

// Even worse - anyone can delete anyone's data
await supabase
  .from('messages')
  .delete()
  .eq('id', anyMessageId);  // No ownership check
-- ✅ GOOD - Enable RLS and create proper policies
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;

-- Users can only see their own messages
CREATE POLICY "Users read own messages" ON messages
  FOR SELECT USING (user_id = auth.uid());

-- Users can only insert messages as themselves
CREATE POLICY "Users insert own messages" ON messages
  FOR INSERT WITH CHECK (user_id = auth.uid());

-- Users can only delete their own messages
CREATE POLICY "Users delete own messages" ON messages
  FOR DELETE USING (user_id = auth.uid());

How AI Tools Get Supabase Auth Wrong

AI coding tools make three consistent Supabase auth mistakes:

  1. Client-side filtering instead of RLS: The code adds .eq('user_id', userId) to queries, creating the illusion of data isolation. But an attacker can remove this filter by calling the Supabase API directly.
  2. Using the service role key on the client: The service role key bypasses RLS entirely. AI tools sometimes use it in client-side code, giving every user full database admin access.
  3. Missing policies for specific operations: Even when RLS is enabled, policies for UPDATE and DELETE operations are often missing. Users can read only their data but modify anyone's.
// ❌ BAD - Client-side filtering is NOT security
const { data } = await supabase
  .from('profiles')
  .select('*')
  .eq('user_id', currentUser.id);  // Attacker can just change this ID

// ❌ VERY BAD - Service role key in client code
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_SERVICE_KEY  // Bypasses ALL RLS
);

Testing Your RLS Policies

You can verify your RLS setup directly in the Supabase SQL editor:

-- Check which tables have RLS enabled
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';

-- Test as a specific user (replace with a real user ID)
SET request.jwt.claims = '{"sub": "user-uuid-here"}';
SELECT * FROM messages;  -- Should only return this user's messages
Table Type RLS Policy Pattern Example
User-owned data user_id = auth.uid() profiles, settings, messages
Team-shared data team_id IN (user's teams) projects, documents, tasks
Public read, owner write SELECT: true, UPDATE/DELETE: user_id = auth.uid() blog posts, public profiles
Admin-only auth.jwt() ->> 'role' = 'admin' admin settings, user management

How to Fix Your Supabase App

  1. Audit every table: Go to Table Editor in Supabase dashboard. If any table shows "RLS disabled", fix it immediately.
  2. Enable RLS on all tables: ALTER TABLE tablename ENABLE ROW LEVEL SECURITY;
  3. Create policies for each operation: SELECT, INSERT, UPDATE, DELETE. Do not assume one policy covers all operations.
  4. Remove the service role key from client code: Search your frontend code for service_role or the service key value. Move it to server-side only.
  5. Scan your codebase: Tools like VibeDoctor (vibedoctor.io) automatically detect missing authentication patterns and exposed credentials in your Supabase-backed app. Free to sign up.

FAQ

Is the Supabase anon key safe to expose?

Yes - the anon key is designed to be public. It only has the permissions you define through RLS policies. Without RLS, the anon key has full read/write access to all tables. With proper RLS, the anon key can only access what your policies allow. The service role key bypasses all RLS and must never be exposed on the client.

Does enabling RLS break my existing queries?

Yes, intentionally. When you enable RLS on a table with no policies, all queries will return zero rows (default deny). This is the correct behavior - it forces you to explicitly define who can access what. Add policies for each operation your app needs before enabling RLS in production.

Can I test RLS policies without affecting production?

Yes. Supabase provides a local development environment via supabase start that mirrors your production setup. Write and test your RLS policies locally, verify they work with your app, then apply them to production using migrations.

Do RLS policies affect performance?

Minimal impact for well-written policies. Simple user_id = auth.uid() policies add negligible overhead because PostgreSQL can use indexes on the user_id column. Complex policies with subqueries may need optimization, but for most apps, the performance impact is undetectable.

Does Lovable set up RLS when it generates Supabase apps?

Sometimes, but inconsistently. Lovable may generate basic RLS policies for simple user-owned data, but complex access patterns (teams, shared resources, admin access) are almost always missing. Always verify your RLS policies regardless of which tool generated the code.

Scan your codebase for this issue - free

VibeDoctor checks for SEC-001, SEC-002 and 128 other issues across 15 diagnostic areas.

SCAN MY APP →
← Back to all articles View all 129+ checks →