Supabase Row Level Security: Beginner's Guide 2026
Master Supabase RLS with production-ready patterns, real error messages, and step-by-step setup. Secure your PostgreSQL data at scale.
TL;DR
Row Level Security (RLS) in Supabase lets you enforce data access rules directly in PostgreSQL. Enable it per table, write policies using auth.uid(), and test thoroughly before production. Common mistakes: forgetting to enable RLS, writing overly permissive policies, and not handling auth state in client code.
---
What is Row Level Security?
Row Level Security is a PostgreSQL feature that restricts which rows users can access based on their identity. Supabase makes this accessible through its auth system.
Instead of: ```javascript // ❌ Dangerous: filter in application code const { data } = await supabase .from('posts') .select('*') .eq('user_id', currentUserId) // User can modify this ```
You get database-enforced rules: ```sql -- ✅ Secure: enforced by PostgreSQL CREATE POLICY "Users can see own posts" ON posts FOR SELECT USING (auth.uid() = user_id); ```
---
Setup: Step-by-Step
1. Enable RLS on Your Table
In Supabase Dashboard → SQL Editor, or via migration:
```sql -- Create your table CREATE TABLE posts ( id BIGSERIAL PRIMARY KEY, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, title TEXT NOT NULL, content TEXT, created_at TIMESTAMP DEFAULT NOW() );
-- Enable RLS (critical step) ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Without policies, NO ONE can access the table ```
Important: Enabling RLS without policies locks everyone out—including authenticated users. Always create policies immediately after enabling.
2. Create Your First Policy
```sql -- Policy 1: Users can SELECT their own posts CREATE POLICY "select_own_posts" ON posts FOR SELECT USING (auth.uid() = user_id);
-- Policy 2: Users can INSERT their own posts CREATE POLICY "insert_own_posts" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Policy 3: Users can UPDATE their own posts CREATE POLICY "update_own_posts" ON posts FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
-- Policy 4: Users can DELETE their own posts CREATE POLICY "delete_own_posts" ON posts FOR DELETE USING (auth.uid() = user_id); ```
Key distinction:
USING = filters which rows the operation applies toWITH CHECK = validates data during INSERT/UPDATE3. Test in Your Client
```typescript // Initialize Supabase client import { createClient } from '@supabase/supabase-js';
const supabase = createClient(URL, ANON_KEY);
// Sign up user await supabase.auth.signUp({ email: 'user@example.com', password: 'secure_password_123' });
// Insert post (auth.uid() must match) const { data, error } = await supabase .from('posts') .insert([ { user_id: (await supabase.auth.getUser()).data.user?.id, title: 'My First Post', content: 'Hello World' } ]) .select();
if (error) console.error('Insert failed:', error);
// Select posts (RLS filters automatically) const { data: posts } = await supabase .from('posts') .select('*'); // Only user's own posts returned ```
---
Real Error Messages You'll See
Error 1: RLS Enabled, No Policies
``` POSTgREST Error: 42501 new row violates row-level security policy "posts" DETAIL: Failing row contains (1, null, 'Title', 'Content', 2026-01-15). ```Fix: Create at least one policy that allows the operation.
Error 2: Policy Logic Error
``` Error inserting row: Policy "insert_own_posts" with check expression "(auth.uid() = user_id)" was violated ```Fix: Verify auth.uid() matches the user_id value you're inserting.
Error 3: Anon Key vs Service Role
``` Error: 401 Unauthorized - Invalid API Key ```Fix: Use ANON_KEY for client-side code (policies enforced). Never expose SERVICE_ROLE_KEY in frontend code.
---
Advanced: Public Posts with Private Comments
```sql -- Posts table: public read, owner write ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "posts_anyone_select" ON posts FOR SELECT USING (true); -- Anyone can read
CREATE POLICY "posts_owner_write" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Comments table: only visible to post author and commenter ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "comments_visible_to_post_author" ON comments FOR SELECT USING ( auth.uid() = user_id OR auth.uid() = ( SELECT user_id FROM posts WHERE id = comments.post_id ) ); ```
---
Production Checklist
auth.uid() included in every policySERVICE_ROLE_KEY never exposed in frontend codeuser_id on most tables)DELETE policies explicitly defined---
Common Pitfalls
Pitfall 1: Writing policies without indexes ```sql -- Slow on large tables CREATE POLICY slow_policy ON users FOR SELECT USING (email ILIKE '%' || auth.jwt() ->> 'email' || '%');
-- Better: use indexed lookup CREATE POLICY fast_policy ON users FOR SELECT USING (id = auth.uid()); ```
Pitfall 2: Forgetting WITH CHECK on UPDATE
```sql
-- ❌ User can update other users' records
CREATE POLICY bad_update ON users
FOR UPDATE
USING (id = auth.uid());
-- ✅ Both USING and WITH CHECK required CREATE POLICY good_update ON users FOR UPDATE USING (id = auth.uid()) WITH CHECK (id = auth.uid()); ```
---
Related Resources
---
What am I missing?
Did I oversimplify a scenario? Got a production RLS horror story? Spotted an error in the SQL syntax? Please comment below—this guide is community-maintained and your corrections help hundreds of developers.
Specific areas needing expansion: