Supabase Row Level Security: Beginner's Guide 2026
Master Supabase RLS with production-ready patterns, real error messages, and step-by-step setup. Protect user data at the database level.
TL;DR
Supabase Row Level Security (RLS) enforces data access rules at the PostgreSQL level, not your application. Enable RLS on tables, define policies matching your auth schema, test thoroughly, and always verify official docs for current versions.
---
What is Row Level Security?
Row Level Security is a PostgreSQL feature that determines which rows a user can access based on database policies. Unlike application-level checks (which can be bypassed), RLS operates at the database engine itself.
Why it matters:
---
Setup: Enabling RLS
Step 1: Enable RLS on Your Table
```sql -- Enable RLS on the table ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Verify it's enabled SELECT tablename, rowsecurity FROM pg_tables WHERE tablename = 'posts'; -- Output: posts | t (t = true, enabled) ```
Critical: Once RLS is enabled, ALL queries are blocked until you create policies. You'll see:
``` ERROR: new row violates row-level security policy for table "posts" DETAIL: Policy (user_posts_policy): 1 row affected. ```
Step 2: Create Your First Policy
Assuming you have an auth.users table (standard in Supabase) and your posts table has a user_id column:
```sql -- Users can only SELECT their own posts CREATE POLICY "Users can select own posts" ON posts FOR SELECT USING (auth.uid() = user_id);
-- Users can INSERT their own posts CREATE POLICY "Users can insert own posts" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Users can UPDATE their own posts CREATE POLICY "Users can update own posts" ON posts FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
-- Users can DELETE their own posts CREATE POLICY "Users can delete own posts" ON posts FOR DELETE USING (auth.uid() = user_id); ```
---
Understanding Policy Clauses
Every RLS policy has this structure:
```sql CREATE POLICY "policy_name" ON table_name FOR operation -- SELECT, INSERT, UPDATE, DELETE, or ALL USING (expression) -- For SELECT/UPDATE/DELETE WITH CHECK (expression); -- For INSERT/UPDATE ```
USING vs WITH CHECK:
Real example - UPDATE needs both:
```sql CREATE POLICY "update_own_profile" ON profiles FOR UPDATE USING (auth.uid() = id) -- Can only update your own row WITH CHECK (auth.uid() = id); -- Can't change user_id to someone else's ```
---
Common Real-World Errors
Error 1: Authentication Context Missing
``` ERROR: row-level security policy "users_own_data" requires authentication DETAIL: Cannot check expression at statement level. ```
Cause: Called auth.uid() without active session. Fix: Always authenticate before queries:
```javascript const { data, error } = await supabase .from('posts') .select('*') .eq('user_id', user.id); // user must be authenticated ```
Error 2: Permission Denied on SELECT
``` ERROR: permission denied for schema public DETAIL: Default deny on schema. ```
Cause: Table exists but no SELECT policy created. Fix:
```sql CREATE POLICY "anyone_can_read" ON public_posts FOR SELECT USING (true); ```
Error 3: Policy Blocking Insert
``` ERROR: new row violates row-level security policy "insert_own_posts" DETAIL: Policy (insert_own_posts) WITH CHECK expression returned false. ```
Cause: WITH CHECK clause failed. Verify the condition matches your data:
```sql -- Debug: Check what auth.uid() returns SELECT auth.uid(); -- Then verify your WITH CHECK references correct column ```
---
Production-Ready Patterns
Pattern 1: Public + Authenticated Tiers
```sql -- Anyone reads published articles CREATE POLICY "public_articles_readable" ON articles FOR SELECT USING (published = true);
-- Authors manage their drafts CREATE POLICY "author_draft_access" ON articles FOR SELECT USING (published = false AND auth.uid() = author_id);
CREATE POLICY "author_can_publish" ON articles FOR UPDATE USING (auth.uid() = author_id) WITH CHECK (auth.uid() = author_id); ```
Pattern 2: Role-Based Access
Assuming user_metadata stores role:
```sql -- Admin can do anything CREATE POLICY "admin_all_access" ON posts FOR ALL USING ( auth.jwt()->>'role' = 'admin' );
-- Moderators see flagged content CREATE POLICY "mod_flagged_posts" ON posts FOR SELECT USING ( auth.jwt()->>'role' = 'moderator' AND flagged = true ); ```
Pattern 3: Shared Resources
```sql -- Teams accessing shared documents CREATE POLICY "team_members_read" ON documents FOR SELECT USING ( team_id IN ( SELECT team_id FROM team_members WHERE user_id = auth.uid() ) ); ```
---
Testing Your Policies
Never trust policies without testing. Use the Supabase dashboard or:
```sql -- Impersonate a user (for testing only) SET request.jwt.claim.sub = 'user-uuid-here'; SELECT * FROM posts; -- Should show only their posts
RESET request.jwt.claim.sub; ```
Verify in official docs for [RLS testing strategies](https://supabase.com/docs/guides/auth/row-level-security).
---
Related Guides
---
Version Notes
This guide applies to Supabase v2.x (verify in official [Supabase release notes](https://github.com/supabase/supabase/releases)). Policy syntax is stable, but JWT claim access patterns may changeβcheck docs before production deployment.
Pricing: RLS has no additional cost. [Verify current Supabase pricing](https://supabase.com/pricing) for storage/bandwidth implications.
---
What am I missing?
Did I oversimplify a scenario? Miss a common gotcha? Found an outdated pattern? Comment below with:
This guide improves with your corrections.