SupabaseNovember 23, 202512 min read

Supabase Row Level Security: The Complete Guide for Vibe Coders

Row Level Security (RLS) is your database's last line of defense. Without it, anyone with your Supabase anon key can access all your data. This guide will teach you everything you need to know.

What You'll Learn

  1. What RLS is and why you absolutely need it
  2. How to enable RLS on your tables
  3. Writing effective security policies
  4. Common RLS patterns with examples
  5. Testing your RLS policies
  6. Common mistakes to avoid

What is Row Level Security?

Row Level Security (RLS) is a PostgreSQL feature that lets you control which rows a user can access in a table. Think of it as a bouncer for every row in your database—it checks if the user is allowed to see or modify that specific piece of data.

Why This Matters

Your Supabase anon key is publicly visible in your JavaScript. Without RLS, anyone can use it to read, modify, or delete all data in unprotected tables. This is the #1 security issue we find in AI-generated apps.

Without RLS: Anyone Can Do This

// An attacker opens DevTools, finds your keys, and runs:

const supabase = createClient(YOUR_URL, YOUR_ANON_KEY);

const { data } = await supabase.from('users').select('*');

console.log(data); // All user data exposed!

How to Enable RLS

Enabling RLS is a two-step process: first you enable it on the table, then you create policies that define who can access what.

Step 1: Enable RLS on the Table

-- In Supabase SQL Editor

ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

Important!

Once RLS is enabled, no one can access the table until you create policies. This is secure by default, but it means your app will break until you add policies.

Step 2: Create Policies

Policies define the rules for accessing data. Each policy specifies:

  • Operation: SELECT, INSERT, UPDATE, DELETE, or ALL
  • TO: Which role (authenticated, anon, or a custom role)
  • USING: A condition that must be true for access (for reading)
  • WITH CHECK: A condition that must be true for writes

Common RLS Patterns

Pattern 1: Users Own Their Data

The most common pattern—users can only see and modify their own records.

-- Users can only view their own profile

CREATE POLICY "Users can view own profile"

ON profiles FOR SELECT

TO authenticated

USING (auth.uid() = user_id);


-- Users can update their own profile

CREATE POLICY "Users can update own profile"

ON profiles FOR UPDATE

TO authenticated

USING (auth.uid() = user_id)

WITH CHECK (auth.uid() = user_id);

Pattern 2: Public Read, Authenticated Write

Content that anyone can read but only logged-in users can create (like blog posts).

-- Anyone can read posts

CREATE POLICY "Posts are viewable by everyone"

ON posts FOR SELECT

TO anon, authenticated

USING (true);


-- Only authenticated users can create posts

CREATE POLICY "Authenticated users can create posts"

ON posts FOR INSERT

TO authenticated

WITH CHECK (auth.uid() = author_id);

Pattern 3: Team/Organization Access

Users can access data belonging to their team or organization.

-- Team members can view team documents

CREATE POLICY "Team members can view documents"

ON documents FOR SELECT

TO authenticated

USING (

team_id IN (

SELECT team_id FROM team_members

WHERE user_id = auth.uid()

)

);

Pattern 4: Admin Override

Admins can access everything, regular users are restricted.

-- Admins can do anything, users only their own data

CREATE POLICY "Admin or owner access"

ON orders FOR ALL

TO authenticated

USING (

auth.uid() = user_id

OR

EXISTS (

SELECT 1 FROM profiles

WHERE id = auth.uid() AND role = 'admin'

)

);

Testing Your RLS Policies

Always test your RLS policies before going to production. Here's how:

Manual Testing

  1. 1
    Open your app in an incognito window (no auth)
  2. 2
    Open DevTools Console and try querying your tables
  3. 3
    Create a second test user and verify they can't see user 1's data
  4. 4
    Try INSERT, UPDATE, DELETE operations where they shouldn't work

Test Query Example

// In browser console, test as unauthenticated user:

const { data, error } = await supabase

.from('profiles')

.select('*');


// With proper RLS, this should return empty or error

console.log(data); // Should be [] or null

Common Mistakes to Avoid

Mistake 1: USING (true) on Sensitive Tables

Using USING (true) allows anyone to access all rows. Only use this for genuinely public data.

Mistake 2: Forgetting WITH CHECK

USING controls reads, but INSERT/UPDATE need WITH CHECK. Without it, users might insert data they can't later read.

Mistake 3: Not Enabling RLS on Join Tables

If your main table has RLS but a related table doesn't, attackers can query the unprotected table to infer data.

Mistake 4: Using Service Key in Frontend

The service role key bypasses ALL RLS. Never use it in client-side code—use the anon key and rely on RLS.

RLS Security Checklist

RLS enabled on ALL tables with user data
Every table has at least one policy
Policies use auth.uid() to restrict access
Both USING and WITH CHECK are set for write operations
Join tables and lookup tables are protected
Tested as unauthenticated user
Tested as different authenticated users
Service role key is NOT in frontend code
Verified policies work in production
VAS scan confirms no data exposure

Test Your Supabase RLS Automatically

VAS doesn't just check if RLS is enabled—it actively tests for data exposure by querying your tables with the anon key. Find out exactly which tables are leaking data.

Related Articles