Supabase Row Level Security: Beginner Guide 2026

Learn to implement RLS policies in Supabase with production-ready patterns. Avoid common pitfalls and secure your PostgreSQL database properly.

TL;DR

Supabase Row Level Security (RLS) enforces database-level access control using PostgreSQL policies. Enable RLS on tables, create policies matching your auth logic, and test thoroughly before production. Common mistakes: forgetting USING clauses, not handling null auth tokens, and testing without SET role context.

---

What is Row Level Security?

Row Level Security is a PostgreSQL feature that restricts which rows users can access based on policies you define. In Supabase, RLS becomes your source-of-truth security layer—even if your frontend is compromised, unauthorized users can't read or modify protected data.

Think of it as middleware at the database level, not the application level. When a user runs a query, PostgreSQL automatically filters results based on policies attached to their role.

Current stable version: Supabase v2.x uses PostgreSQL 15.x (verify in official docs for latest patch)

---

Enable RLS on Your Table

First, enable RLS on tables that need protection:

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

Critical: Once RLS is enabled with no policies, ALL queries are blocked (even SELECT). This is intentional—you must explicitly grant access.

---

Create Your First Policy

Let's secure a posts table so users see only their own posts:

```sql -- Policy: Users can SELECT their own posts CREATE POLICY "Users can view own posts" ON posts FOR SELECT USING (auth.uid() = user_id);

-- Policy: Users can INSERT posts (they own) CREATE POLICY "Users can insert own posts" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id);

-- Policy: 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);

-- Policy: Users can DELETE their own posts CREATE POLICY "Users can delete own posts" ON posts FOR DELETE USING (auth.uid() = user_id); ```

Key components:

  • USING clause: Applied to SELECT/DELETE; determines which rows user sees
  • WITH CHECK clause: Applied to INSERT/UPDATE; validates the new data
  • auth.uid(): Supabase function returning authenticated user's UUID
  • ---

    Common Error Messages & Solutions

    Error 1: Policy Prevents All Access

    ``` PostgresError: new row violates row-level security policy for table "posts" ```

    Cause: Your WITH CHECK condition failed during INSERT/UPDATE.

    Fix: Ensure the condition matches your actual data: ```sql -- Bad: tries to set user_id to someone else const { data, error } = await supabase .from('posts') .insert([{ title: 'Hello', user_id: 'other-uuid' }]);

    -- Good: user_id from auth context const { data, error } = await supabase .from('posts') .insert([{ title: 'Hello', user_id: (await supabase.auth.getUser()).data.user.id }]); ```

    Error 2: NULL Auth Token

    ``` PostgresError: row level security policy "Users can view own posts" is violated ```

    Cause: auth.uid() returned NULL because request lacked auth header.

    Fix: Check client initialization: ```typescript // Ensure Supabase client reads auth token import { createClient } from '@supabase/supabase-js';

    const supabase = createClient( process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY );

    // Verify token exists const { data: { session } } = await supabase.auth.getSession(); console.log(session?.access_token); // Should not be null ```

    Error 3: Public Read Policy Blocks Anonymous Access

    ``` PostgresError: permission denied for schema public ```

    Cause: No policy allows anonymous users to read public data.

    Fix: Add a public read policy: ```sql CREATE POLICY "Public posts are readable" ON posts FOR SELECT USING (is_public = true); ```

    ---

    Production-Ready Patterns

    Pattern 1: Role-Based Access

    For admin-only operations:

    ```sql -- Create custom claims in auth.users -- Verify in official docs: [Supabase Auth](https://supabase.com/docs/guides/auth/managing-user-data)

    CREATE POLICY "Admins can delete any post" ON posts FOR DELETE USING ( (SELECT raw_user_meta_data->>'role' FROM auth.users WHERE id = auth.uid()) = 'admin' ); ```

    Pattern 2: Shared Access via Foreign Keys

    Allow access to resources shared with the user:

    ```sql CREATE TABLE projects ( id uuid PRIMARY KEY, name text, owner_id uuid REFERENCES auth.users(id) );

    CREATE TABLE project_members ( project_id uuid REFERENCES projects(id), member_id uuid REFERENCES auth.users(id), PRIMARY KEY (project_id, member_id) );

    CREATE POLICY "Users see projects they own or are members of" ON projects FOR SELECT USING ( owner_id = auth.uid() OR EXISTS ( SELECT 1 FROM project_members WHERE project_members.project_id = projects.id AND project_members.member_id = auth.uid() ) ); ```

    Pattern 3: Testing RLS Policies

    Always test with proper role context:

    ```sql -- In psql, simulate authenticated user SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claims" = '{"sub":"user-uuid-here"}'; SELECT * FROM posts; -- Should respect policy

    -- Test as anon (no auth) SET LOCAL ROLE anon; SELECT * FROM posts; -- Should be blocked ```

    ---

    Testing in Your Application

    ```typescript // Test that user can't bypass RLS via raw SQL const { data, error } = await supabase .rpc('check_post_access', { post_id: 'some-id' }); // RLS still applies to RPC functions

    // Use service role ONLY in trusted backend // Never expose service_role_key to frontend const adminClient = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY // Backend only! ); ```

    ---

    Debugging Checklist

  • [ ] RLS enabled on table: SELECT tablename FROM pg_tables WHERE schemaname = 'public';
  • [ ] At least one policy exists: SELECT * FROM pg_policies WHERE tablename = 'posts';
  • [ ] Auth token included in request headers
  • [ ] auth.uid() matches actual user UUID column
  • [ ] No typos in role/claim names
  • [ ] Tested with correct SET ROLE context
  • Verify in [official docs](https://supabase.com/docs/guides/database/postgres/row-level-security) for latest syntax and gotchas.

    ---

    Related Resources

  • [RLS Performance Optimization](/?guide=supabase-rls-performance)
  • [Securing Realtime Subscriptions](/?guide=supabase-realtime-security)
  • ---

    What am I missing?

    Have you hit edge cases with RLS? Found better patterns for team-based access? Spotted inaccuracies in this guide? Drop a comment below—experienced indie hackers reading this will appreciate real-world gotchas and solutions.

    🔥 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