Supabase Row Level Security: Beginner's Guide 2026
Master Supabase RLS with production patterns, real error messages, and exact PostgreSQL syntax. Secure your database at the row level.
TL;DR
Supabase Row Level Security (RLS) enforces database-level access control using PostgreSQL policies. Enable RLS on tables, define policies with CREATE POLICY, and authenticate users via supabase.auth.getUser(). Common mistakes: forgetting ENABLE ROW LEVEL SECURITY, using auth.uid() without proper user context, or missing USING/WITH CHECK clauses. Test policies with supabase-js v2.39.0+ (verify in official docs for latest).
---
What is Row Level Security?
Row Level Security is PostgreSQL's native feature that restricts database access at the row level—before data leaves the database. Instead of handling authorization in your application code, RLS lets you define policies that automatically filter queries.
Why it matters:
Enabling RLS on a Table
First, enable RLS on your table:
```sql ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY; ```
Critical: Once RLS is enabled, *all* queries are blocked by default until you create policies. You'll see:
``` Error: new row violates row-level security policy for table "posts" ```
Solution: Create at least one SELECT policy before testing.
---
Creating Your First Policies
Policy 1: Users Can Read Their Own Posts
```sql CREATE POLICY "Users can view their own posts" ON public.posts FOR SELECT USING (auth.uid() = user_id); ```
Breakdown:
FOR SELECT: Policy applies to read operationsUSING: The filter condition. If false, row is excluded from resultsauth.uid(): Built-in function returning authenticated user's UUIDuser_id: Column in your posts table storing the post creator's IDPolicy 2: Users Can Insert Their Own Posts
```sql CREATE POLICY "Users can create posts" ON public.posts FOR INSERT WITH CHECK (auth.uid() = user_id); ```
Key difference:
WITH CHECK: Validates data *before* insertion. If condition fails:``` Error: new row violates row-level security policy for table "posts" ```
Policy 3: Users Can Update/Delete Only Their Posts
```sql 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); ```
Both clauses needed: USING checks current row ownership. WITH CHECK ensures updated/new row stays owned by user.
Policy 4: Public Read Access
```sql CREATE POLICY "Posts are publicly readable" ON public.posts FOR SELECT USING (true); ```
Use true to allow all authenticated users (or anon if you trust them). Order matters—PostgreSQL evaluates policies with OR logic by default.
---
Production-Ready Implementation Pattern
```sql -- Users table (built-in from Supabase Auth) -- auth.users has auth_id UUID as primary key
CREATE TABLE public.posts ( id BIGSERIAL PRIMARY KEY, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, title TEXT NOT NULL, content TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() );
ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;
-- Explicit policies for each operation CREATE POLICY "Authenticated users can read published posts" ON public.posts FOR SELECT TO authenticated USING ( -- User sees own posts or posts marked public auth.uid() = user_id OR published = true );
CREATE POLICY "Users insert with their own ID" ON public.posts FOR INSERT TO authenticated WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users update own posts" ON public.posts FOR UPDATE TO authenticated USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users delete own posts" ON public.posts FOR DELETE TO authenticated USING (auth.uid() = user_id); ```
Pattern notes:
TO authenticated: Restricts to logged-in users. Omit for anon accessON DELETE CASCADE: When user deletes account, their posts auto-delete---
Common Errors & Solutions
Error 1: "Permission Denied for Table"
``` Error: permission denied for table posts ```
Cause: RLS enabled but no SELECT policy exists, or user doesn't match any policy.
Fix: Create a SELECT policy: ```sql CREATE POLICY "temp_all_read" ON posts FOR SELECT USING (true); ```
Error 2: "auth.uid() is Null"
``` Error: permission denied (called in SELECT FROM posts where auth.uid() returned NULL) ```
Cause: User isn't authenticated. You're accessing from anonymous client or auth session expired.
Fix: Verify auth before querying: ```typescript const { data: { user } } = await supabase.auth.getUser(); if (!user) throw new Error('Not authenticated'); ```
Error 3: "New Row Violates RLS Policy"
``` Error: new row violates row-level security policy for table "posts" ```
Cause: INSERT/UPDATE data doesn't satisfy WITH CHECK clause. Common: trying to set user_id to different user's ID.
Fix: Never trust client input for user_id:
```typescript
const { data, error } = await supabase
.from('posts')
.insert([
{
title: 'My post',
content: 'Content here',
user_id: user.id // Always from auth context, never request body
}
]);
```
---
Testing RLS Locally
Use Supabase's [SQL Editor](https://supabase.com/docs/guides/database/overview) to test policies:
```sql -- Test as specific user SET request.jwt.claims = json_build_object('sub', '550e8400-e29b-41d4-a716-446655440000');
SELECT * FROM posts; ```
Better approach: Use [supabase-js](https://supabase.com/docs/reference/javascript/introduction) (v2.39.0+) to test with real auth:
```typescript const { data, error } = await supabase .from('posts') .select('*');
if (error) console.error('RLS blocked:', error.message); else console.log('Posts visible:', data); ```
---
Key Takeaways
1. Enable RLS: ALTER TABLE table_name ENABLE ROW LEVEL SECURITY
2. Write policies: Use CREATE POLICY with USING for SELECT/DELETE, WITH CHECK for INSERT/UPDATE
3. Never trust clients: Set user context server-side, never from request body
4. Test thoroughly: Verify policies work before deploying to production
5. Reference docs: See [Supabase RLS Guide](https://supabase.com/docs/guides/auth/row-level-security) and [PostgreSQL Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html)
---
Related Guides
---
What am I missing?
Have you encountered RLS edge cases? Comment with:
Accuracy matters to this community—corrections welcome.