Supabase Row Level Security: Beginner's Guide 2026
Master Supabase RLS with production patterns, real errors, and step-by-step setup. Secure your database at the row level.
TL;DR
Supabase Row Level Security (RLS) enforces data access rules at the database level using PostgreSQL policies. Enable RLS on tables, create policies matching your auth model, test with rls_bypass_jwt to debug, and always verify policies don't inadvertently block legitimate queries.
---
What is Row Level Security?
Row Level Security (RLS) is a PostgreSQL feature that Supabase exposes through its dashboard and API. Instead of controlling access in your application layer, RLS policies live in the database itself—meaning even direct SQL queries respect your security rules.
When you enable RLS on a table, *no rows are accessible by default* until you explicitly create policies. This "deny by default" approach is a significant security advantage over relying solely on client-side or middleware checks.
Key principle: RLS policies execute *as the authenticated user* making the request. The auth.uid() function returns the current user's UUID.
---
Step 1: Enable RLS on Your Table
In the Supabase dashboard, navigate to your table and toggle "Enable RLS" in the table settings, or use SQL:
```sql ALTER TABLE posts ENABLE ROW LEVEL SECURITY; ```
Verify it's enabled:
```sql SELECT tablename, rowsecurity FROM pg_tables WHERE tablename = 'posts'; ```
Expected output: rowsecurity = true
---
Step 2: Create Your First Policy
Here's a basic "users can read their own posts" policy:
```sql CREATE POLICY "Users can view their own posts" ON posts FOR SELECT USING (auth.uid() = user_id); ```
Breakdown:
---
Step 3: Common Policy Patterns
Pattern 1: Owner-Based Access
```sql -- User can see only their own data CREATE POLICY "Users can view own profile" ON profiles FOR SELECT USING (auth.uid() = id);
-- User can update only their own profile CREATE POLICY "Users can update own profile" ON profiles FOR UPDATE USING (auth.uid() = id) WITH CHECK (auth.uid() = id); ```
Note: INSERT and UPDATE policies require both USING (existing row check) and WITH CHECK (new row validation).
Pattern 2: Public Readable, Private Writable
```sql -- Anyone (including anon) can read public posts CREATE POLICY "Public posts are readable" ON posts FOR SELECT USING (is_public = true OR auth.uid() = author_id);
-- Only authors can insert/update their posts CREATE POLICY "Authors can manage posts" ON posts FOR ALL USING (auth.uid() = author_id) WITH CHECK (auth.uid() = author_id); ```
Pattern 3: Role-Based Access
```sql -- Admins can see everything; users see only approved content CREATE POLICY "Admins and users see content" ON content FOR SELECT USING ( (SELECT role FROM auth.users WHERE id = auth.uid()) = 'admin' OR status = 'approved' ); ```
⚠️ Warning: Avoid querying auth.users directly in policies—it's slower and risks exposure. Instead, store role info in a public.user_metadata or custom claims table [Supabase Auth Best Practices](/?guide=supabase-auth-setup).
---
Real Errors You'll Encounter
Error 1: "new row violates row level security policy"
``` Error: new row violates row level security policy "Users can update own profile" for table "profiles" ```
Cause: Your INSERT or UPDATE WITH CHECK clause is too restrictive.
Fix: Verify the new data passes your WITH CHECK condition:
```sql -- If your policy is: WITH CHECK (auth.uid() = user_id AND status = 'active')
-- Ensure your insert includes both conditions: await supabase .from('profiles') .insert({ user_id: userId, status: 'active' }) ```
Error 2: "relation does not exist" or "permission denied for schema public"
``` Error: permission denied for schema public Error: relation "profiles" does not exist ```
Cause: RLS is enabled but no SELECT policy exists for the user role.
Fix: Create an explicit SELECT policy or temporarily add one for testing:
```sql CREATE POLICY "Anon can view profiles" ON profiles FOR SELECT USING (true); -- Use restrictive conditions in production ```
Error 3: "Column 'user_id' must appear in the USING clause or be constant"
``` Error: column "user_id" must appear in the USING clause or constant within it ```
Cause: Referencing columns that aren't checked in USING (common in UPDATE scenarios).
Fix: Include the column check:
```sql CREATE POLICY "Users can update own posts" ON posts FOR UPDATE USING (auth.uid() = author_id) -- Existing row check WITH CHECK (auth.uid() = author_id AND author_id = OLD.author_id); -- Prevent reassignment ```
---
Testing & Debugging RLS
Use the Supabase SQL editor with the rls_bypass_jwt option disabled (default). Test as different users:
```sql -- Set session variable to simulate user SET request.jwt.claims = '{"sub": "user-uuid-here", "role": "authenticated"}'::jsonb;
SELECT * FROM posts; -- Should respect policies ```
For JavaScript debugging, enable verbose logging:
```javascript const { data, error } = await supabase .from('posts') .select('*') .eq('author_id', userId);
console.log(error?.message); // Catch policy violations ```
---
Production Checklist
✅ Enable RLS on all user-specific tables (posts, comments, settings, etc.)
✅ Use auth.uid() for ownership checks—never trust client-sent user IDs
✅ Test policies with anonymous and authenticated roles separately
✅ Document each policy's purpose in comments
✅ Use -- in SQL comments to track policy intent:
```sql -- Policy: Only authenticated users can read approved posts -- Used by: Public feed, search results -- Test: anon users see nothing, users see approved=true CREATE POLICY ... ```
✅ Verify performance by checking query plans:
```sql EXPLAIN ANALYZE SELECT * FROM posts WHERE author_id = auth.uid(); ```
---
When NOT to Use RLS Alone
For these, combine RLS with [Application-Level Authorization](/?guide=auth-patterns).
---
Official References
---
What am I missing?
Have you hit edge cases with RLS policies? Found a more efficient pattern for role-based access? Discovered performance gotchas with large datasets? Drop insights in the comments—this guide improves with real-world corrections.
Also: which version of Supabase are you running? (verify in official docs for latest 2026 features and changes)