Supabase Row Level Security: Beginner's Guide 2026
Master RLS policies in Supabase to control database access at the row level. Production-ready patterns, real errors, and security best practices.
TL;DR
Supabase Row Level Security (RLS) enforces access control at the database row level using PostgreSQL policies. Enable RLS on tables, create policies tied to auth.uid(), and test thoroughly—misconfigured RLS is a common security vulnerability. This guide covers setup, common errors, and production patterns.
---
What is Row Level Security?
Row Level Security is a PostgreSQL feature that Supabase exposes through its dashboard and client libraries. Instead of trusting your application code to filter data, RLS pushes access control to the database itself. Every query—regardless of origin—respects these policies.
Why it matters:
---
Enable RLS on a Table
First, enable RLS on your table. This is the critical first step:
```sql ALTER TABLE profiles ENABLE ROW LEVEL SECURITY; ```
Important: Once RLS is enabled with *no policies*, all queries return zero rows by default. You must create at least one policy or queries will fail silently.
---
Creating Your First Policy
Pattern 1: Users Can Only See Their Own Row
This is the most common pattern for user profile data:
```sql CREATE POLICY "Users can view their own profile" ON profiles FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can update their own profile" ON profiles FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); ```
Explanation:
FOR SELECT: Policy applies to read operationsUSING: The condition evaluated against the database row. Must return true to include the rowWITH CHECK: For UPDATE/INSERT, the condition checked before the operation completesauth.uid(): Built-in Supabase function returning the current user's UUIDPattern 2: Public Read, Authenticated Write
For content like blog posts:
```sql -- Anyone can read published posts CREATE POLICY "Public posts are viewable to all" ON posts FOR SELECT USING (is_published = true);
-- Only the author can update their post CREATE POLICY "Authors can update their own posts" ON posts FOR UPDATE USING (auth.uid() = author_id) WITH CHECK (auth.uid() = author_id);
-- Only authenticated users can create CREATE POLICY "Authenticated users can create posts" ON posts FOR INSERT WITH CHECK (auth.uid() = author_id AND auth.role() = 'authenticated'); ```
Pattern 3: Role-Based Access
For admin functionality:
```sql CREATE POLICY "Admins can see all users" ON profiles FOR SELECT USING ( auth.uid() IN ( SELECT user_id FROM admin_users WHERE role = 'admin' ) ); ```
---
Common Errors and Solutions
Error 1: No Rows Returned After Enabling RLS
``` Error: 0 rows returned from query ```
Cause: RLS is enabled but no SELECT policies exist.
Fix: Create at least one SELECT policy, or temporarily disable RLS for testing:
```sql ALTER TABLE profiles DISABLE ROW LEVEL SECURITY; ```
Error 2: "new row violates row-level security policy"
``` Error: PostgreSQL error: new row violates row-level security policy for table "posts" ```
Cause: Your INSERT/UPDATE policy's WITH CHECK condition failed.
Example problem: ```sql -- User tries to create post but author_id doesn't match auth.uid() const { error } = await supabase .from('posts') .insert([{ title: 'Test', author_id: 'someone-else-uuid' }]); ```
Fix: Ensure WITH CHECK logic matches your application:
```sql CREATE POLICY "Users create posts as themselves" ON posts FOR INSERT WITH CHECK (auth.uid() = author_id); ```
Error 3: "Permission denied for schema public"
``` Error: permission denied for schema public ```
Cause: User role lacks basic table access. RLS policies are secondary—base permissions matter first.
Fix: Verify in Supabase dashboard that your authenticated role has table privileges. In SQL:
```sql GRANT SELECT, INSERT, UPDATE ON profiles TO authenticated; ```
---
Production-Ready Pattern
Here's a complete, testable example combining multiple patterns:
```sql -- Table with timestamps and ownership CREATE TABLE documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), owner_id UUID NOT NULL REFERENCES auth.users(id), title TEXT NOT NULL, is_shared BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT now() );
-- Enable RLS ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Owners see their own docs CREATE POLICY "Owners see their documents" ON documents FOR SELECT USING (auth.uid() = owner_id);
-- Shared docs visible to all authenticated users CREATE POLICY "Authenticated users see shared documents" ON documents FOR SELECT USING (is_shared = true AND auth.role() = 'authenticated');
-- Only owners can insert CREATE POLICY "Owners can create documents" ON documents FOR INSERT WITH CHECK (auth.uid() = owner_id);
-- Only owners can update their docs CREATE POLICY "Owners can update documents" ON documents FOR UPDATE USING (auth.uid() = owner_id) WITH CHECK (auth.uid() = owner_id AND is_shared IN (true, false));
-- Only owners can delete CREATE POLICY "Owners can delete documents" ON documents FOR DELETE USING (auth.uid() = owner_id); ```
---
Testing RLS Policies
Always test policies before deploying. Use Supabase's SQL editor with the authenticated user context:
```sql -- Test as authenticated user SET LOCAL request.jwt.claims = '{"sub": "your-test-uuid"}'; SELECT * FROM documents; -- Should only return owned docs ```
Or test via client:
```javascript const { data, error } = await supabase .from('documents') .select('*');
if (error) console.error('RLS blocked query:', error); ```
---
Key Gotchas
1. RLS doesn't apply to service role: Queries using service_role key bypass RLS entirely. Keep this key secret.
2. Policies are additive: If multiple policies match, the query succeeds if *any* return true (OR logic).
3. NULL comparisons: auth.uid() = user_id returns false if user_id is NULL. Use COALESCE if needed.
4. Performance: Complex policies with subqueries can slow down queries. Index appropriately—[verify in official docs](https://supabase.com/docs/guides/auth/row-level-security) for performance recommendations.
---
Related Resources
---
What am I missing?
RLS patterns vary widely by use case. What policies are you building? Share your patterns, gotchas you've hit, or scenarios this guide should cover in the comments. Did a particular error message take you hours to debug? Let me know—I'll add it here.