Supabase Row Level Security: Beginner Guide 2026
Master Supabase RLS with practical examples, common errors, and production patterns. Learn to secure your PostgreSQL data at the row level.
TL;DR
Supabase Row Level Security (RLS) enforces data access rules at the PostgreSQL level. Enable RLS on tables, create policies with CREATE POLICY, and use auth.uid() to reference current users. Common mistakes: forgetting to enable RLS before creating policies, using incorrect claim paths, and not testing with authenticated vs. unauthenticated sessions.
---
What is Row Level Security?
Row Level Security (RLS) is a PostgreSQL feature that restricts which rows users can access based on policies you define. Unlike application-level checks, RLS enforces rules at the database layer—no authenticated user can bypass it, even with direct API calls.
Supabase (verify current pricing in [official docs](https://supabase.com/docs/guides/auth/row-level-security)) exposes PostgreSQL's RLS through its client libraries and REST API. When you query via supabase-js or the PostgREST API, RLS automatically filters results based on the authenticated user's session.
---
Enable RLS on Your Table
First, you must enable RLS. Without this, policies won't apply:
```sql -- In Supabase SQL Editor ALTER TABLE posts ENABLE ROW LEVEL SECURITY; ```
Critical: If you create policies before enabling RLS, they exist but don't enforce. Enable RLS first.
Verify status in SQL Editor: ```sql SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE tablename = 'posts'; ```
---
Basic Policy Pattern
Policies specify which rows a user can SELECT, INSERT, UPDATE, or DELETE. Here's the production pattern:
```sql -- Users can only read their own posts CREATE POLICY "Users can read own posts" ON posts FOR SELECT USING (auth.uid() = user_id);
-- Users can only insert posts as themselves CREATE POLICY "Users can insert own posts" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Users can update only their own posts CREATE POLICY "Users can update own posts" ON posts FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
-- Users can delete only their own posts CREATE POLICY "Users can delete own posts" ON posts FOR DELETE USING (auth.uid() = user_id); ```
Breakdown:
FOR SELECT – read operationsUSING() – condition for what rows are visibleFOR INSERT/UPDATE – write operationsWITH CHECK() – validates data being writtenauth.uid() – built-in Supabase function returning current user's UUID---
Real-World Error Messages
Error 1: Policies Don't Apply Because RLS Isn't Enabled
``` PG::InsufficientPrivilege: ERROR: permission denied for schema public ```
Or silently returning all rows. Fix: Run ALTER TABLE tablename ENABLE ROW LEVEL SECURITY;
Error 2: Incorrect JWT Claim Path
``` PG::UndefinedFunction: ERROR: function auth.uid() does not exist ```
Occurs if you're using a custom PostgreSQL user instead of Supabase's auth context. Fix: Ensure queries run through Supabase client with valid JWT token, or use current_user_id() if defined in your schema.
Error 3: Missing User ID in INSERT
``` PG::ExclusionViolation: ERROR: new row violates row-level security policy ```
Happens when WITH CHECK (auth.uid() = user_id) fails because user_id is null or doesn't match. Fix: Always include the authenticated user's ID when inserting:
```javascript const { data, error } = await supabase .from('posts') .insert([{ title: 'My Post', user_id: (await supabase.auth.getUser()).data.user.id }]); ```
---
Advanced Patterns
Public + Private Data
Some rows are public, others private:
```sql CREATE POLICY "Public posts visible to all" ON posts FOR SELECT USING (is_public = true OR auth.uid() = user_id); ```
Anonymous users see only is_public = true. Authenticated users see their own posts too.
Team-Based Access
Users access rows belonging to their team:
```sql CREATE POLICY "Users access team documents" ON documents FOR SELECT USING ( team_id IN ( SELECT team_id FROM team_members WHERE user_id = auth.uid() ) ); ```
Admin Bypass
Admins bypass RLS (use carefully—verify in [official docs](https://supabase.com/docs/guides/auth/row-level-security#admin-bypass)):
```sql CREATE POLICY "Admins bypass RLS" ON posts FOR ALL USING ( auth.uid() IN (SELECT id FROM profiles WHERE is_admin = true) ); ```
---
Testing RLS Policies
Always test with authenticated and unauthenticated sessions:
```javascript // Test 1: Unauthenticated query const unauth = await supabase .from('posts') .select('*'); // Should return 0 rows or public rows only
// Test 2: Authenticated as user A const { data: userA } = await supabase.auth.signInWithPassword({ email: 'a@example.com', password: 'password' });
const { data: postsA } = await supabase .from('posts') .select('*'); // Should return only user A's posts
// Test 3: Attempt unauthorized write const { error } = await supabase .from('posts') .update({ title: 'Hacked' }) .eq('id', 'user-b-post-id') .select(); // Should error: "new row violates row-level security policy" ```
---
Common Pitfalls
1. Forgetting WITH CHECK on writes – Always include it to validate inserted/updated data matches your policy.
2. Using string IDs instead of UUIDs – auth.uid() returns UUID. Ensure your user_id column is also UUID type.
3. Testing only authenticated users – Verify unauthenticated access returns empty results or errors appropriately.
4. Policies stacking unpredictably – If multiple policies match, PostgreSQL ORs them. Use FOR ALL sparingly.
5. Not testing with real JWT tokens – Supabase CLI's supabase test command can help. Verify in [official docs](https://supabase.com/docs/guides/auth/row-level-security).
---
Debugging Checklist
ALTER TABLE ... ENABLE ROW LEVEL SECURITY)auth.uid() returning the expected UUID?WITH CHECK conditions?---
Next Steps
For deeper patterns, explore [Supabase RLS Best Practices](https://supabase.com/docs/guides/auth/row-level-security) and learn about [custom claims](/?guide=supabase-custom-claims). For multi-tenant apps, check [Multi-Tenant Security](/?guide=supabase-multitenant).
---
What am I missing?
RLS is powerful but context-dependent. What patterns have you found essential? Have you hit unexpected errors or edge cases? Drop your experiences, corrections, and questions in the comments—this guide improves with your feedback.