Supabase Row Level Security Guide 2026

Master Supabase RLS policies to secure user data at the database level. Real examples, common errors, and production patterns.

TL;DR

Row Level Security (RLS) in Supabase enforces data access rules directly in PostgreSQL. Enable it per table, write policies using SQL expressions, and test thoroughly before production. Most mistakes involve missing auth claims or overly permissive policies.

---

What is Row Level Security?

Row Level Security is a PostgreSQL feature that restricts which rows a user can access based on policies you define. Supabase makes this accessible through its dashboard and API.

Instead of filtering data in your application code: ```typescript // ❌ Risky - filtering happens in app const { data } = await supabase .from('posts') .select('*') .eq('user_id', userId); // What if userId is spoofed? ```

You enforce rules at the database layer: ```sql -- ✅ Secure - enforced by PostgreSQL CREATE POLICY "users_can_read_own_posts" ON posts FOR SELECT USING (auth.uid() = user_id); ```

---

Enable RLS on a Table

First, enable RLS on the table:

```sql ALTER TABLE posts ENABLE ROW LEVEL SECURITY; ```

Important: Enabling RLS without policies blocks *all* access by default. You must create policies immediately or your app breaks.

Common Error #1:

``` PolicyError: row level security policy denied access ``` This means either: 1. No policy exists for your operation (SELECT, INSERT, UPDATE, DELETE) 2. Your auth context doesn't satisfy the policy condition 3. The policy's TO clause excludes your role

---

Basic Policy Patterns

Pattern 1: Users Read Their Own Data

```sql CREATE POLICY "users_read_own_posts" ON posts FOR SELECT USING (auth.uid() = user_id); ```

Breakdown:

  • FOR SELECT - applies only to reads
  • USING - the filter condition (what rows are visible)
  • auth.uid() - built-in Supabase function returning the JWT's sub claim
  • user_id - your table's foreign key column
  • Pattern 2: Authenticated Users Insert, Own Rows Only

    ```sql CREATE POLICY "users_insert_own_posts" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id); ```

    Key difference:

  • WITH CHECK for INSERT/UPDATE - validates data being written
  • Your app passes the user_id; the policy enforces it matches auth
  • Pattern 3: Public Read, Private Write

    ```sql CREATE POLICY "posts_public_read" ON posts FOR SELECT USING (true); -- Everyone can read

    CREATE POLICY "posts_owner_write" ON posts FOR UPDATE USING (auth.uid() = user_id); ```

    ---

    Advanced: Roles and Sharing

    For multi-user collaboration (team projects, shared documents):

    ```sql CREATE TABLE documents ( id uuid PRIMARY KEY, owner_id uuid NOT NULL, title text );

    CREATE TABLE document_access ( document_id uuid NOT NULL, user_id uuid NOT NULL, access_level text, -- 'read' or 'write' PRIMARY KEY (document_id, user_id) );

    ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

    CREATE POLICY "owner_full_access" ON documents FOR ALL USING (auth.uid() = owner_id);

    CREATE POLICY "shared_access" ON documents FOR SELECT USING ( EXISTS ( SELECT 1 FROM document_access WHERE document_access.document_id = documents.id AND document_access.user_id = auth.uid() ) ); ```

    This uses EXISTS to check if the user appears in your access control table.

    ---

    Testing RLS Policies

    In Supabase Dashboard

    Use the SQL editor with SET ROLE to simulate users:

    ```sql SET ROLE authenticated; -- Simulate logged-in user SET app.jwt_claims = jsonb_build_object('sub', '12345');

    SELECT * FROM posts; -- Respects RLS policies ```

    In Your Application (TypeScript/JavaScript)

    ```typescript import { createClient } from '@supabase/supabase-js';

    const supabase = createClient(url, anonKey);

    // Sign in user await supabase.auth.signInWithPassword({ email: 'user@example.com', password: 'password123' });

    // This request includes the auth token // PostgreSQL sees auth.uid() from the JWT const { data, error } = await supabase .from('posts') .select('*');

    if (error?.code === 'PGRST116') { console.error('RLS denied access:', error.message); } ```

    Common Error #2:

    ``` JWT claims missing or invalid ``` Ensure: 1. User is authenticated (check auth.currentUser) 2. Auth token is sent with the request (Supabase client does this automatically) 3. Your policy doesn't assume custom claims that don't exist

    ---

    Production Checklist

    ✅ Enable RLS on all user-facing tables ```sql SELECT tablename FROM pg_tables WHERE schemaname = 'public'; -- Then run ALTER TABLE X ENABLE ROW LEVEL SECURITY for each ```

    ✅ Test policies with real user tokens Never assume auth.uid() works without testing cross-origin requests.

    ✅ Verify policy performance Complex EXISTS subqueries can be slow. Check query plans: ```sql EXPLAIN ANALYZE SELECT * FROM documents; ```

    ✅ Audit policy changes Log who modified policies using triggers or Supabase audit logs.

    ✅ Handle policy bypass scenarios Service role keys (backend-only) bypass RLS. Use them only for admin operations: ```typescript // ✅ Backend only const admin = createClient(url, serviceRoleKey); await admin.from('users').update({ banned: true }).eq('id', userId); ```

    ---

    Common Error #3:

    ``` ForeignKeyViolation: insert or update on table violates foreign key ``` Often combined with RLS—the row your policy allows to insert references a row you can't see. Solution: ```sql CREATE POLICY "can_insert_if_owner_of_parent" ON posts FOR INSERT WITH CHECK ( auth.uid() = user_id AND EXISTS (SELECT 1 FROM projects WHERE id = project_id AND owner_id = auth.uid()) ); ```

    ---

    Helpful Resources

  • [Supabase RLS Official Documentation](https://supabase.com/docs/guides/auth/row-level-security)
  • [PostgreSQL RLS Docs](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
  • Related: [Authentication Setup](/?guide=supabase-auth)
  • Related: [PostgreSQL Basics for Supabase](/?guide=postgres-fundamentals)
  • ---

    What am I missing?

    Have you hit RLS issues I didn't cover? Comment below with:

  • Specific error messages you encountered
  • Policy patterns that solved your problem
  • Performance gotchas in production
  • Multi-tenant patterns you use
  • This guide improves with your real-world feedback.

    🔥 0d
    LIVE
    ↓ PlanetScale rage spiking •⚠ Vercel pricing complaints •↑ Railway gaining fast •↑ Supabase happiness rising •↑ Resend loved by devs •↓ PlanetScale rage spiking •⚠ Vercel pricing complaints •↑ Railway gaining fast •↑ Supabase happiness rising
    DEVELOPER PAIN RADAR // Loading...

    Developers complain.
    Opportunities appear.

    We track what developers are struggling with today — and what opportunities that creates.

    —
    guides today
    —
    avg happiness
    🔥 Pain
    📖 Guides
    🔭 Explore
    👤 Mine
    🔥 Pain Radar — rage scores today
    ↗ share
    💡 Opportunity Feed — pain = market gap
    📈 Tool Momentum
    all scores →
    📖 Latest Guide
    all guides →
    📖 All Guides
    📊 Tool Scores
    + Submit
    📰 Hacker News
    ➕ Submit a Tool
    ← back