Supabase Row Level Security: Beginner's Guide 2026
Master Supabase RLS with production patterns, real error messages, and step-by-step setup to secure your PostgreSQL database at the row level.
TL;DR
Supabase Row Level Security (RLS) enforces database-level access control using PostgreSQL policies. Enable RLS on tables, create policies tied to authenticated users or custom claims, and test thoroughly—misconfigured policies are a top security mistake. This guide covers setup, common errors, and production patterns.
---
What is Row Level Security?
Row Level Security is a PostgreSQL feature that Supabase exposes through its dashboard and API. Instead of protecting data at the application layer, RLS makes the database itself aware of who can access what rows.
Without RLS:
With RLS:
Current version context: Supabase uses PostgreSQL 14+ (verify in official docs for your project's specific version). RLS syntax is stable across versions.
---
Step 1: Enable RLS on Your Table
First, enable RLS on the table. This blocks *all* access by default until you create explicit policies:
```sql ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY; ```
Run this in the Supabase SQL Editor or via migrations. Without this, policies don't apply.
Common Error #1:
```
PolicyError: new row violates row-level security policy "" for table "posts"
```
This means you enabled RLS but haven't created a SELECT policy yet. Even table owners can't read rows without an explicit policy.
---
Step 2: Create Your First Policy
Let's say you have a posts table with id, title, user_id, and created_at. You want users to see only their own posts:
```sql CREATE POLICY "Users can view their own posts" ON public.posts FOR SELECT USING (auth.uid() = user_id); ```
Breakdown:
auth.uid() is the authenticated user's UUID from Supabase Authauth.uid() returns NULL if the user is unauthenticated. The policy fails for anonymous requests.
---
Step 3: Create Policies for Writes
Now let users insert their own posts:
```sql CREATE POLICY "Users can insert their own posts" ON public.posts FOR INSERT WITH CHECK (auth.uid() = user_id); ```
The WITH CHECK clause validates the row being inserted. If a user tries to insert a post with someone else's user_id, PostgreSQL rejects it.
For updates (users can only modify their own posts):
```sql CREATE POLICY "Users can update their own posts" ON public.posts FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); ```
For deletes:
```sql CREATE POLICY "Users can delete their own posts" ON public.posts FOR DELETE USING (auth.uid() = user_id); ```
---
Step 4: Test Your Policies
In your application, test with authenticated and unauthenticated clients:
```javascript // JavaScript/TypeScript (Supabase Client v2.43+, verify current version in docs) import { createClient } from '@supabase/supabase-js';
const supabase = createClient(URL, ANON_KEY);
// Authenticated request (after user logs in) const { data, error } = await supabase .from('posts') .select('*') .eq('id', somePostId);
if (error) console.error(error.message); // RLS violation caught here ```
Common Error #2: ``` PostgresError: permission denied for schema public ``` You may need to grant schema access. From the SQL Editor:
```sql GRANT USAGE ON SCHEMA public TO authenticated, anon; GRANT SELECT ON public.posts TO authenticated, anon; ```
Then apply RLS on top.
---
Advanced: Using Custom Claims
For role-based access, use custom claims in the JWT:
```sql CREATE POLICY "Admins can see all posts" ON public.posts FOR SELECT USING ( (auth.jwt() ->> 'role') = 'admin' OR auth.uid() = user_id ); ```
Set custom claims in your auth system or via Supabase's admin API. Verify exact claim structure in [official JWT docs](https://supabase.com/docs/guides/auth/jwt).
---
Common Mistakes
1. Forgetting to enable RLS
Policies won't apply if ALTER TABLE ... ENABLE ROW LEVEL SECURITY is missing.
2. No policy for a required operation ``` PostgresError: permission denied for table "posts" ``` You enabled RLS but created no INSERT policy? Users can't insert anything. Create it.
3. Common Error #3 - Incorrect auth context:
```
PostgresError: relation "auth" does not exist
```
You're using an old Supabase instance or the wrong schema. auth.uid() only works if Supabase Auth is enabled. Use the SQL Editor to test: SELECT auth.uid(); (should return a UUID if logged in).
4. RLS on related tables
If posts has RLS and comments references posts.id, add RLS to comments too—otherwise users might bypass post privacy by querying comments.
---
Production Checklist
ALTER TABLE ... ENABLE ROW LEVEL SECURITY)---
Related Resources
---
What am I missing?
Have you hit RLS issues not covered here? Spotted an error in the examples? Comment below with:
I'll update this guide with community feedback and edge cases. RLS patterns evolve as teams discover novel approaches—your insights matter.