Supabase Row Level Security: Beginner Guide 2026
Master RLS policies in Supabase with production-ready patterns. Learn policies, auth integration, and avoid common mistakes.
TL;DR
Supabase Row Level Security (RLS) enforces data access at the database level. Enable RLS on tables, create policies tied toauth.uid(), and test thoroughly. Common mistake: forgetting to enable RLS itself—policies won't work without it.What is Row Level Security?
Row Level Security is a PostgreSQL feature that restricts which rows users can access based on policies you define. Supabase makes RLS accessible through the dashboard and SQL, but the underlying mechanism is pure PostgreSQL (verify version support in [official Supabase docs](https://supabase.com/docs/guides/auth/row-level-security)).
Without RLS, any authenticated user with table access can read/modify all data. With RLS:
Enable RLS on a Table
First, enable RLS in the Supabase dashboard or via SQL:
```sql ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY; ```
Critical: Enabling RLS without policies blocks all access. You must create policies next.
Basic Policy Pattern: User Owns Data
Most common pattern—users see only their own rows:
```sql -- Create a users table with auth integration CREATE TABLE public.profiles ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, email TEXT, created_at TIMESTAMP DEFAULT NOW() );
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
-- Policy: Users can SELECT their own profile CREATE POLICY "users_can_read_own_profile" ON public.profiles FOR SELECT USING (auth.uid() = id);
-- Policy: Users can UPDATE their own profile CREATE POLICY "users_can_update_own_profile" ON public.profiles FOR UPDATE USING (auth.uid() = id);
-- Policy: Users can INSERT their own profile CREATE POLICY "users_can_insert_own_profile" ON public.profiles FOR INSERT WITH CHECK (auth.uid() = id); ```
Key distinction:
USING clause: filters rows visible to the userWITH CHECK clause: validates data on INSERT/UPDATEReal Console Errors You'll See
Error 1: RLS enabled but no policies ``` POSTGRES error: "new row violates row-level security policy for table" ``` Solution: Create at least one SELECT policy.
Error 2: Auth context missing
```
PostgreSQL error: "failed to fetch"
Network response: 401 Unauthorized
```
Cause: auth.uid() returns NULL (user not authenticated). Solution: Ensure user is logged in via supabase.auth.signIn().
Error 3: Policy uses wrong column ``` POSTGRES error: "column "user_id" does not exist" ``` Solution: Match policy column name to your schema exactly.
Advanced Pattern: Public + Authenticated Access
Scenarios where some rows are public:
```sql CREATE TABLE public.articles ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, author_id UUID REFERENCES auth.users(id), title TEXT, content TEXT, is_published BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT NOW() );
ALTER TABLE public.articles ENABLE ROW LEVEL SECURITY;
-- Anyone (including anonymous) can read published articles CREATE POLICY "public_can_read_published" ON public.articles FOR SELECT USING (is_published = true);
-- Authors can read their own drafts CREATE POLICY "authors_can_read_own_drafts" ON public.articles FOR SELECT USING ( (is_published = false AND auth.uid() = author_id) OR (is_published = true) );
-- Authors can update their own articles CREATE POLICY "authors_can_update_own" ON public.articles FOR UPDATE USING (auth.uid() = author_id); ```
Role-Based Access Control
Use custom claims in JWT tokens (requires [Supabase custom claims setup](https://supabase.com/docs/guides/auth/jwt)):
```sql -- Assume custom claim 'role' exists in JWT CREATE POLICY "admins_full_access" ON public.articles FOR ALL USING ( auth.jwt() ->> 'role' = 'admin' );
CREATE POLICY "moderators_can_delete" ON public.articles FOR DELETE USING ( auth.jwt() ->> 'role' IN ('admin', 'moderator') ); ```
Testing RLS Policies
1. Use Supabase Dashboard: Table editor shows RLS status and policies
2. SQL Editor with SET statements:
```sql
-- Simulate user authentication
SET request.jwt.claims = '{"sub": "550e8400-e29b-41d4-a716-446655440000"}'::jsonb;
SELECT * FROM public.profiles;
```
3. Client-side testing (JavaScript): ```javascript // Test as signed-in user const { data, error } = await supabase .from('profiles') .select('*');
// Test as anonymous const { data: anonData } = await supabase .auth.signOut() .then(() => supabase.from('profiles').select('*')); ```
Common Pitfalls
1. Forgetting ON DELETE CASCADE: Orphaned rows bypass RLS
```sql
-- Bad
CREATE TABLE posts (id UUID PRIMARY KEY, user_id UUID REFERENCES auth.users(id));
-- Good CREATE TABLE posts (id UUID PRIMARY KEY, user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE); ```
2. Complex policies without indexes: RLS performance degrades with large tables ```sql -- Add index for policy column CREATE INDEX idx_posts_user_id ON public.posts(user_id); ```
3. Using session instead of auth.uid(): Session data may be stale
```sql
-- Avoid
USING (user_id = (auth.jwt() ->> 'sub')::uuid);
-- Prefer USING (user_id = auth.uid()); ```
Performance Considerations
RLS adds overhead—every query checks policies. For high-scale apps:
EXPLAIN ANALYZEDebugging RLS Issues
Enable query logs: ```sql ALTER DATABASE postgres SET log_statement = 'all'; ```
Then check logs in Supabase dashboard → Logs → API.
Integration with [Supabase Auth](/?guide=supabase-authentication)
RLS works best with Supabase Auth (version 2.x+, verify in [official docs](https://supabase.com/docs/reference/auth)):
```javascript // User login triggers auth.uid() availability const { data, error } = await supabase.auth.signInWithPassword({ email: 'user@example.com', password: 'password123' });
// Now auth.uid() works in RLS policies const { data: userPosts } = await supabase .from('posts') .select('*'); // Only user's posts visible ```
Production Checklist
auth.uid() in USING/WITH CHECK clausesWhat am I missing?
RLS policies vary by use case—comment below if you've encountered edge cases like:
Corrections and additions welcome in the comments section.