Supabase Row Level Security 2026: Beginner Guide
Master RLS policies in Supabase. Real code examples, common errors, and production patterns for securing your database.
TL;DR
Row Level Security (RLS) in Supabase lets you enforce database-level access control at the row level. Write SQL policies that automatically filter queries based on the authenticated user. This guide covers setup, real errors, and patterns you'll actually use.What is Row Level Security?
Row Level Security is a PostgreSQL feature that Supabase exposes through its dashboard. Instead of trusting your application layer to filter data, RLS ensures the *database itself* only returns rows a user is allowed to see.
Without RLS: ```javascript // Vulnerable: relies on app logic const { data } = await supabase .from('posts') .select('*') .eq('user_id', currentUser.id); // Easy to bypass ```
With RLS: ```javascript // Secure: database enforces access const { data } = await supabase .from('posts') .select('*'); // Database filters automatically ```
The database will only return posts where user_id matches the authenticated user's ID.
Enabling RLS on a Table
In the Supabase dashboard (v1.0+, [verify in official docs](https://supabase.com/docs/guides/auth/row-level-security)):
1. Go to Authentication → Policies
2. Select your table (e.g., posts)
3. Click Enable RLS
Once enabled, all access is denied by default until you write policies.
Your First Policy: SELECT
Let's assume you have a posts table with columns: id, user_id, title, content.
Goal: Users can only see their own posts.
```sql CREATE POLICY "Users can select their own posts" ON posts FOR SELECT USING (auth.uid() = user_id); ```
Breakdown:
FOR SELECT: This policy applies to read operationsUSING: The condition that must be trueauth.uid(): Supabase function returning the authenticated user's UUIDuser_id: The column we're checkingTest it (Supabase JS v2.39.0+): ```javascript const { data, error } = await supabase .from('posts') .select('*');
if (error) { console.log(error); // Error: "new row violates row-level security policy for table \"posts\"" } ```
INSERT Policy: Let Users Create Posts
```sql CREATE POLICY "Users can insert their own posts" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id); ```
Note: WITH CHECK is used for INSERT/UPDATE instead of USING.
Common Error #1: ``` error: new row violates row-level security policy for table "posts" ```
This happens when: ```javascript // ❌ Trying to insert with wrong user_id const { error } = await supabase .from('posts') .insert([{ title: 'My Post', user_id: 'different-uuid', content: '...' }]);
// ✅ Always use the authenticated user's ID const { data: { user } } = await supabase.auth.getUser(); const { error } = await supabase .from('posts') .insert([{ title: 'My Post', user_id: user.id, content: '...' }]); ```
UPDATE and DELETE Policies
```sql CREATE POLICY "Users can update their own posts" ON posts FOR UPDATE WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete their own posts" ON posts FOR DELETE USING (auth.uid() = user_id); ```
Sharing Data: Public Read, Private Write
Common pattern: public can read posts, only authors can edit.
```sql -- Everyone can read CREATE POLICY "Posts are public for select" ON posts FOR SELECT USING (true);
-- Only authors can update/delete CREATE POLICY "Authors can update their posts" ON posts FOR UPDATE WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Authors can delete their posts" ON posts FOR DELETE USING (auth.uid() = user_id); ```
Working with Joins and Related Tables
Assuming comments table with post_id and author_id:
```sql CREATE POLICY "Users can read comments on public posts" ON comments FOR SELECT USING ( EXISTS ( SELECT 1 FROM posts WHERE posts.id = comments.post_id ) ); ```
This allows users to see comments on any post they can see.
Service Role and Bypass
Your backend should use the service role key (keep this secret!) to bypass RLS:
```javascript // Client - respects RLS const client = supabase.createClient(url, anonKey);
// Server - bypasses RLS const admin = supabase.createClient(url, serviceRoleKey, { auth: { persistSession: false } });
const { data } = await admin .from('posts') .select('*'); // Returns ALL posts, RLS ignored ```
Common Error #2: ``` error: Failed to fetch data Details: Auth session missing! ```
You're using the anon key for operations that need authentication:
```javascript // ❌ Without auth, RLS blocks everything const { error } = await supabase .from('posts') .select('*');
// ✅ Ensure user is signed in first const { data: { session } } = await supabase.auth.getSession(); if (session) { const { data } = await supabase.from('posts').select('*'); } ```
Common Error #3: Policy Logic Bugs
``` Unexpected error: prepared statement \"\" does not exist ```
Often caused by typos in column names or auth function calls:
```sql -- ❌ Wrong function name CREATE POLICY "broken" ON posts FOR SELECT USING (auth.userid() = user_id); -- typo: should be auth.uid()
-- ✅ Correct CREATE POLICY "working" ON posts FOR SELECT USING (auth.uid() = user_id); ```
Testing Policies
Use the Supabase dashboard SQL Editor to test:
```sql SET request.jwt.claims = '{"sub": "your-test-uuid"}'; SELECT * FROM posts; -- Simulates a request from that user ```
Or use different user sessions in your app.
Performance Considerations
USING clauses:```sql CREATE INDEX idx_posts_user_id ON posts(user_id); ```
What About Anonymous Users?
By default, unauthenticated users have auth.uid() = NULL. Your policies must explicitly allow them:
```sql CREATE POLICY "Public read access" ON posts FOR SELECT USING ( published = true OR auth.uid() = user_id ); ```
Further Reading
What am I missing?
Have you run into RLS issues not covered here? Specific patterns you use in production? Share your experiences, edge cases, and corrections in the comments—this guide improves with reader input.