Supabase Row Level Security: 2026 Beginner's Guide
Master RLS policies in Supabase to secure your database at the row level. Production patterns, real errors, and exact setup steps.
TL;DR
Row Level Security (RLS) in Supabase lets you enforce database-level access control automatically. Enable it on tables, create policies using PostgreSQL, and Supabase handles authentication context (auth.uid(), auth.jwt()) for you. This guide covers setup, common errors, and production patterns.
---
What is Row Level Security?
RLS is a PostgreSQL feature that restricts which rows users can access based on policies you define. Unlike application-level checks that can be bypassed, RLS enforces restrictions at the database level—even direct SQL queries respect it.
Why it matters:
---
Enable RLS on Your Table
First, enable RLS on the table:
```sql ALTER TABLE posts ENABLE ROW LEVEL SECURITY; ```
Critical: Once enabled, *no one* can access rows until you create policies. You'll see:
``` ERROR: new row violates row-level security policy for table "posts" ```
This is intentional—default deny.
---
Authentication Context
Supabase injects authentication data into every query via PostgreSQL's current_setting() function:
```sql -- Get current user ID auth.uid() -- Returns UUID of logged-in user auth.jwt() -- Returns full JWT payload as JSON auth.email() -- Returns email address (verify in official docs for your version) ```
These work *only* when: 1. User is authenticated via Supabase Auth 2. Request includes valid session token or JWT 3. You're using Supabase client libraries (they auto-inject tokens)
Direct PostgreSQL connections (psql CLI, legacy drivers) don't get this context—use service role key with caution.
---
Basic Pattern: Users Own Their Data
Most common use case: users can only see/edit their own rows.
```sql CREATE POLICY "Users can read own posts" ON posts FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own posts" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own posts" ON posts FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete own posts" ON posts FOR DELETE USING (auth.uid() = user_id); ```
Key concepts:
USING clause: controls which rows are *visible* (SELECT, UPDATE, DELETE)WITH CHECK clause: controls which rows can be *modified* (INSERT, UPDATE)FOR SELECT/INSERT/UPDATE/DELETE specifies the operation---
Real Error Messages You'll Encounter
Error 1: No Policies Defined
``` ERROR: new row violates row-level security policy for table "posts" DETAIL: Policy with check expression violated. ``` Cause: RLS enabled but no policies exist. Fix: Create at least one policy.Error 2: Missing auth.uid() in Unauthenticated Request
``` ERROR: new row violates row-level security policy for table "posts" HINT: Check the new row violates row-level security policy. ``` Cause:auth.uid() returns NULL for unauthenticated users, so NULL = user_id fails.
Fix: Add explicit unauthenticated policies or require authentication first.Error 3: Service Role Bypasses RLS
``` -- This works even if RLS would normally block it const { data } = await supabase.from('posts') .select('*') .rpc('admin_function', {}) ``` Cause: Service role key (backend only!) bypasses RLS entirely. Fix: Never expose service role to frontend. Use anon/authenticated keys only.---
Production Pattern: Public + Authenticated Access
Allow public read access, but only authenticated users can modify:
```sql -- Enable RLS ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Public can read published posts CREATE POLICY "Public posts are readable" ON posts FOR SELECT USING (published = true);
-- Authenticated users see their own draft posts CREATE POLICY "Users can read own drafts" ON posts FOR SELECT USING (published = false AND auth.uid() = user_id);
-- Only authors can insert/update/delete CREATE POLICY "Authors can manage posts" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Authors can edit posts" ON posts FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); ```
---
Advanced: Role-Based Access Control (RBAC)
Store user roles in a separate table and check membership:
```sql CREATE TABLE team_members ( id uuid PRIMARY KEY, team_id uuid NOT NULL, user_id uuid NOT NULL REFERENCES auth.users(id), role text NOT NULL CHECK (role IN ('owner', 'editor', 'viewer')) );
ALTER TABLE team_members ENABLE ROW LEVEL SECURITY;
-- Users can only read posts from teams they're members of CREATE POLICY "Team members can read posts" ON posts FOR SELECT USING ( EXISTS ( SELECT 1 FROM team_members WHERE team_members.team_id = posts.team_id AND team_members.user_id = auth.uid() ) );
-- Only editors and owners can modify CREATE POLICY "Editors can update posts" ON posts FOR UPDATE USING ( EXISTS ( SELECT 1 FROM team_members WHERE team_members.team_id = posts.team_id AND team_members.user_id = auth.uid() AND team_members.role IN ('owner', 'editor') ) ) WITH CHECK ( EXISTS ( SELECT 1 FROM team_members WHERE team_members.team_id = posts.team_id AND team_members.user_id = auth.uid() AND team_members.role IN ('owner', 'editor') ) ); ```
---
Testing RLS Policies
Supabase Dashboard includes a policy tester (verify in official docs for current UI):
1. Go to SQL Editor → your table 2. Click RLS toggle to test as specific user 3. Run SELECT queries to verify visibility
Or test via code:
```javascript const { data, error } = await supabase .from('posts') .select('*');
if (error) console.log('RLS blocked:', error.message); ```
---
Common Gotchas
1. RLS doesn't prevent COUNT queries from revealing data exists—consider returning null instead
2. Joins still respect RLS on all tables involved
3. current_user is NOT available—use auth.uid() instead
4. Updates via foreign keys still need explicit policies
5. Performance: Complex policies with subqueries can slow down queries—use EXPLAIN to check
---
Resources
---
What am I missing?
Have you hit RLS issues in production? Missing a specific use case? Drop corrections and additions in the comments—especially around performance tuning, tricky auth scenarios, or version-specific behavior (verify in official docs for your Supabase version).