Supabase Row Level Security: Beginner's Guide 2026
Master RLS policies in Supabase. Learn exact syntax, common pitfalls, and production patterns with real error messages.
TL;DR
Row Level Security (RLS) in Supabase lets you control data access at the database level. Enable it on tables, write policies that referenceauth.uid(), and test with multiple users before deploying. Most errors stem from missing auth.uid() in policies or forgetting to enable RLS itself.What is Row Level Security?
Row Level Security is a PostgreSQL feature that Supabase exposes through its dashboard and APIs. Instead of controlling access in your application code, you define policies at the database layer. This means:
Enable RLS on a Table
In Supabase dashboard (v3.x), go to Table Editor → select your table → Authentication tab → toggle Enable RLS.
Or via SQL directly:
```sql ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY; ```
Critical: Enabling RLS without policies blocks all access. Write policies first in dev, then enable.
Core Concepts
Policy Structure
Each policy has:
public, authenticated, or specific role)SELECT, INSERT, UPDATE, DELETE, or ALLAuth Context
Supabase injects these into RLS context:
```sql auth.uid() -- Current user's UUID auth.role() -- Current role ('authenticated' or 'anon') current_user_id -- Function you can create ```
Production-Ready Patterns
Pattern 1: User Owns Their Own Data
Most common case—users see only their own records.
```sql CREATE POLICY "Users can read own posts" ON public.posts FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own posts" ON public.posts FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own posts" ON public.posts FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete own posts" ON public.posts FOR DELETE USING (auth.uid() = user_id); ```
Note: USING applies to existing rows (SELECT, UPDATE, DELETE). WITH CHECK applies to new rows (INSERT, UPDATE).
Pattern 2: Public Read, Authenticated Write
Posts are readable by anyone, but only authenticated users can create/edit their own.
```sql CREATE POLICY "Posts are public readable" ON public.posts FOR SELECT USING (true);
CREATE POLICY "Authenticated users can insert posts" ON public.posts FOR INSERT WITH CHECK (auth.role() = 'authenticated' AND auth.uid() = user_id); ```
Pattern 3: Admin Override
Admins see everything, users see only their own.
```sql CREATE POLICY "Admins can do anything" ON public.posts FOR ALL USING ( auth.uid() IN (SELECT id FROM auth.users WHERE role = 'admin') ) WITH CHECK ( auth.uid() IN (SELECT id FROM auth.users WHERE role = 'admin') );
CREATE POLICY "Users see own posts" ON public.posts FOR SELECT USING (auth.uid() = user_id); ```
For admin roles, [verify in official docs](https://supabase.com/docs/guides/auth/row-level-security) which version supports is_super_admin vs custom roles (as of Supabase v3.x, custom JWT claims are standard).
Pattern 4: Shared Access via Join Table
Users can access posts shared with them through a post_access table.
```sql CREATE POLICY "Users can read shared posts" ON public.posts FOR SELECT USING ( auth.uid() = user_id OR auth.uid() IN ( SELECT user_id FROM public.post_access WHERE post_access.post_id = posts.id ) ); ```
Real Error Messages You'll See
Error 1: "new row violates row-level security policy"
``` PostgREST error: new row violates row-level security policy "Users can insert own posts" on table "posts" Status: 403 ```
Cause: Your WITH CHECK condition failed. Usually auth.uid() is null or doesn't match user_id.
Fix: Verify you're signed in and passing the correct user_id:
```javascript const { data, error } = await supabase .from('posts') .insert([{ user_id: session.user.id, title: 'Hello' }]); ```
Error 2: "permission denied for schema public"
``` PostgREST error: permission denied for schema public Status: 403 ```
Cause: RLS is enabled but no policies exist for your role.
Fix: Create a policy for public or authenticated role, depending on your user.
Error 3: "Failed to retrieve the JWT from local storage"
``` Error: Failed to retrieve the JWT from local storage Status: 401 ```
Cause: User isn't authenticated but you're querying protected data. auth.uid() is null.
Fix: Check session exists before querying:
```javascript if (!session) { // redirect to login or fetch public data } ```
Testing RLS Policies
In Dashboard
1. Go to SQL Editor 2. Run a query as your user:
```sql SELECT * FROM public.posts LIMIT 1; ```
3. Check the Headers tab and verify Authorization: Bearer <token> exists
Programmatically
Test with different users:
```javascript // User A const sessionA = await supabase.auth.signInWithPassword({ email: 'a@test.com', password: 'password123' });
const { data: dataA } = await supabase .from('posts') .select('*');
// User B await supabase.auth.signOut(); const sessionB = await supabase.auth.signInWithPassword({ email: 'b@test.com', password: 'password123' });
const { data: dataB } = await supabase .from('posts') .select('*');
// Verify dataA !== dataB console.assert(dataA.length !== dataB.length, 'RLS not working!'); ```
Common Gotchas
1. auth.uid() is null: User isn't authenticated. Check your session token.
2. Circular policy logic: A policy that references the same table can cause infinite recursion. Use simple expressions or CTEs carefully.
3. Case sensitivity: SQL identifiers are lowercase unless quoted. user_id ≠ user_ID.
4. Performance: Complex policies with nested SELECTs slow down queries. Use indexes on referenced columns.
Performance Tips
Related Guides
Official Resources
What am I missing?
This guide covers 80% of RLS use cases. What's your production scenario?
Drop your questions and corrections in the comments. We'll update this guide as Supabase evolves.