Supabase Row Level Security Guide 2026
Learn RLS policies, auth integration, and production patterns. Avoid common pitfalls with real error messages and working code examples.
TL;DR
Supabase Row Level Security (RLS) lets you control table access at the database level using PostgreSQL policies tied to your auth system. Enable it per table, write policies that reference auth.uid() or JWT claims, test thoroughly in isolation, and always verify policies aren't blocking legitimate queries.
---
What is Row Level Security?
Row Level Security is a PostgreSQL feature that enforces access control at the row level—before data leaves the database. Instead of managing permissions in your application code, RLS policies live in SQL.
Why this matters:
[Learn more about authentication strategies](/?guide=supabase-auth)
---
Enable RLS on a Table
First, create a test table and enable RLS:
```sql CREATE TABLE documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, title TEXT NOT NULL, content TEXT, created_at TIMESTAMPTZ DEFAULT now() );
ALTER TABLE documents ENABLE ROW LEVEL SECURITY; ```
Critical: When you enable RLS on a table with no policies, ALL access is denied—including your own. This is intentional and safe.
---
Write Your First Policy
Create a policy allowing users to read their own documents:
```sql CREATE POLICY "Users can read own documents" ON documents FOR SELECT USING (auth.uid() = user_id); ```
Breakdown:
FOR SELECT: This policy applies to read operationsUSING (auth.uid() = user_id): Only rows where the current user's ID matches user_id are visibleAdd a policy for inserts:
```sql CREATE POLICY "Users can insert own documents" ON documents FOR INSERT WITH CHECK (auth.uid() = user_id); ```
The WITH CHECK clause validates that the authenticated user can only create records with their own user_id.
Allow users to update their own documents:
```sql CREATE POLICY "Users can update own documents" ON documents FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); ```
And delete:
```sql CREATE POLICY "Users can delete own documents" ON documents FOR DELETE USING (auth.uid() = user_id); ```
---
Real Error Messages You'll Encounter
Error 1: Policy Blocks All Access
``` Error: new row violates row-level security policy "Users can insert own documents" for table "documents" ```
Cause: Your WITH CHECK clause failed. Most common: you're not setting user_id to the current user.
Fix:
```typescript const { data, error } = await supabase .from('documents') .insert([ { title: 'My Doc', content: 'Hello', user_id: user.id // ← Must match auth.uid() } ]); ```
Error 2: Unauthenticated Requests Fail Silently
``` Error: 401 Unauthorized Details: "You must be logged in to perform this action" ```
Cause: auth.uid() returns NULL when no user is authenticated. Your policy then compares NULL = NULL, which evaluates to NULL (not true), so access is denied.
Fix: Create a public read policy if needed:
```sql CREATE POLICY "Public documents are readable" ON documents FOR SELECT USING (public = true); ```
Error 3: Service Role Bypass Confusion
``` Error: RLS policy blocks service role queries ```
Reality: Service role (with service_role key) bypasses RLS entirely. If you're seeing this, you're not actually using service role credentials. Verify you're using the correct API key.
---
Advanced: Role-Based Access Control
For team-based apps, use JWT claims to check user roles:
```sql CREATE POLICY "Admins see all documents" ON documents FOR SELECT USING ( auth.jwt() ->> 'user_role' = 'admin' OR auth.uid() = user_id ); ```
Configure custom claims in Supabase's auth.users.raw_user_meta_data via your auth provider, or [set them with Auth Hooks](/?guide=supabase-auth-hooks).
---
Testing RLS Policies
In SQL Editor (Supabase Dashboard)
Test as if you're a specific user:
```sql -- Impersonate user with ID 'abc-123' SET request.jwt.claims = jsonb_build_object('sub', 'abc-123');
SELECT * FROM documents; ```
In Your Application
```typescript import { createClient } from '@supabase/supabase-js';
const supabase = createClient(url, anonKey);
// After user logs in const { data: { user } } = await supabase.auth.getUser();
// This query respects RLS automatically const { data, error } = await supabase .from('documents') .select('*');
if (error?.code === 'PGRST116') { console.error('RLS policy blocked access:', error.message); } ```
---
Production Checklist
✅ Test unauthenticated access: Ensure queries fail or return empty sets
✅ Test cross-user access: User A cannot read User B's data
✅ Verify service role is restricted: Don't expose it in client code
✅ Use explicit policies: Don't rely on "no policies = no access"
✅ Index foreign keys: Add indexes to user_id for performance
✅ Document policy intent: Add comments to SQL policies
✅ Version control policies: Store them in migration files
---
Common Pitfalls
1. Forgetting user_id on insert: Set it automatically in trigger or application logic
2. Testing with wrong user: Always verify auth.uid() in your test session
3. Overly broad policies: "allow all" defeats the purpose
4. Performance issues: RLS can slow queries; verify query plans
---
References
---
What am I missing?
Have you hit edge cases with RLS? Found a better pattern for multi-tenant apps? Discovered performance gotchas? Drop your experience in the comments—help other builders avoid the same walls you've hit.
Specifically looking for: