Supabase Row Level Security: Beginner Guide 2026
Learn to implement RLS policies in Supabase with production-ready patterns. Avoid common pitfalls and secure your PostgreSQL database properly.
TL;DR
Supabase Row Level Security (RLS) enforces database-level access control using PostgreSQL policies. Enable RLS on tables, create policies matching your auth logic, and test thoroughly before production. Common mistakes: forgetting USING clauses, not handling null auth tokens, and testing without SET role context.
---
What is Row Level Security?
Row Level Security is a PostgreSQL feature that restricts which rows users can access based on policies you define. In Supabase, RLS becomes your source-of-truth security layer—even if your frontend is compromised, unauthorized users can't read or modify protected data.
Think of it as middleware at the database level, not the application level. When a user runs a query, PostgreSQL automatically filters results based on policies attached to their role.
Current stable version: Supabase v2.x uses PostgreSQL 15.x (verify in official docs for latest patch)
---
Enable RLS on Your Table
First, enable RLS on tables that need protection:
```sql ALTER TABLE posts ENABLE ROW LEVEL SECURITY; ALTER TABLE comments ENABLE ROW LEVEL SECURITY; ```
Critical: Once RLS is enabled with no policies, ALL queries are blocked (even SELECT). This is intentional—you must explicitly grant access.
---
Create Your First Policy
Let's secure a posts table so users see only their own posts:
```sql -- Policy: Users can SELECT their own posts CREATE POLICY "Users can view own posts" ON posts FOR SELECT USING (auth.uid() = user_id);
-- Policy: Users can INSERT posts (they own) CREATE POLICY "Users can insert own posts" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Policy: Users can UPDATE 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);
-- Policy: Users can DELETE their own posts CREATE POLICY "Users can delete own posts" ON posts FOR DELETE USING (auth.uid() = user_id); ```
Key components:
USING clause: Applied to SELECT/DELETE; determines which rows user seesWITH CHECK clause: Applied to INSERT/UPDATE; validates the new dataauth.uid(): Supabase function returning authenticated user's UUID---
Common Error Messages & Solutions
Error 1: Policy Prevents All Access
``` PostgresError: new row violates row-level security policy for table "posts" ```Cause: Your WITH CHECK condition failed during INSERT/UPDATE.
Fix: Ensure the condition matches your actual data: ```sql -- Bad: tries to set user_id to someone else const { data, error } = await supabase .from('posts') .insert([{ title: 'Hello', user_id: 'other-uuid' }]);
-- Good: user_id from auth context const { data, error } = await supabase .from('posts') .insert([{ title: 'Hello', user_id: (await supabase.auth.getUser()).data.user.id }]); ```
Error 2: NULL Auth Token
``` PostgresError: row level security policy "Users can view own posts" is violated ```Cause: auth.uid() returned NULL because request lacked auth header.
Fix: Check client initialization: ```typescript // Ensure Supabase client reads auth token import { createClient } from '@supabase/supabase-js';
const supabase = createClient( process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY );
// Verify token exists const { data: { session } } = await supabase.auth.getSession(); console.log(session?.access_token); // Should not be null ```
Error 3: Public Read Policy Blocks Anonymous Access
``` PostgresError: permission denied for schema public ```Cause: No policy allows anonymous users to read public data.
Fix: Add a public read policy: ```sql CREATE POLICY "Public posts are readable" ON posts FOR SELECT USING (is_public = true); ```
---
Production-Ready Patterns
Pattern 1: Role-Based Access
For admin-only operations:
```sql -- Create custom claims in auth.users -- Verify in official docs: [Supabase Auth](https://supabase.com/docs/guides/auth/managing-user-data)
CREATE POLICY "Admins can delete any post" ON posts FOR DELETE USING ( (SELECT raw_user_meta_data->>'role' FROM auth.users WHERE id = auth.uid()) = 'admin' ); ```
Pattern 2: Shared Access via Foreign Keys
Allow access to resources shared with the user:
```sql CREATE TABLE projects ( id uuid PRIMARY KEY, name text, owner_id uuid REFERENCES auth.users(id) );
CREATE TABLE project_members ( project_id uuid REFERENCES projects(id), member_id uuid REFERENCES auth.users(id), PRIMARY KEY (project_id, member_id) );
CREATE POLICY "Users see projects they own or are members of" ON projects FOR SELECT USING ( owner_id = auth.uid() OR EXISTS ( SELECT 1 FROM project_members WHERE project_members.project_id = projects.id AND project_members.member_id = auth.uid() ) ); ```
Pattern 3: Testing RLS Policies
Always test with proper role context:
```sql -- In psql, simulate authenticated user SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claims" = '{"sub":"user-uuid-here"}'; SELECT * FROM posts; -- Should respect policy
-- Test as anon (no auth) SET LOCAL ROLE anon; SELECT * FROM posts; -- Should be blocked ```
---
Testing in Your Application
```typescript // Test that user can't bypass RLS via raw SQL const { data, error } = await supabase .rpc('check_post_access', { post_id: 'some-id' }); // RLS still applies to RPC functions
// Use service role ONLY in trusted backend // Never expose service_role_key to frontend const adminClient = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY // Backend only! ); ```
---
Debugging Checklist
SELECT tablename FROM pg_tables WHERE schemaname = 'public';SELECT * FROM pg_policies WHERE tablename = 'posts';auth.uid() matches actual user UUID columnSET ROLE contextVerify in [official docs](https://supabase.com/docs/guides/database/postgres/row-level-security) for latest syntax and gotchas.
---
Related Resources
---
What am I missing?
Have you hit edge cases with RLS? Found better patterns for team-based access? Spotted inaccuracies in this guide? Drop a comment below—experienced indie hackers reading this will appreciate real-world gotchas and solutions.