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
- What RLS is and why you absolutely need it
- How to enable RLS on your tables
- Writing effective security policies
- Common RLS patterns with examples
- Testing your RLS policies
- 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
- 1Open your app in an incognito window (no auth)
- 2Open DevTools Console and try querying your tables
- 3Create a second test user and verify they can't see user 1's data
- 4Try 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
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.