Supabase Row Level Security: Beginner's Guide 2026

Master RLS policies in Supabase with real code examples. Learn authentication patterns, avoid common pitfalls, and secure your PostgreSQL data.

TL;DR

Row Level Security (RLS) in Supabase enforces data access at the PostgreSQL level using policies tied to authenticated users. Enable RLS on tables, write policies matching your auth scheme, and test thoroughly—misconfigured policies leak data or block legitimate queries.

---

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. Instead of securing data at your application layer, RLS secures it *inside the database*.

Why this matters: Even if someone bypasses your API, they can't access rows they shouldn't see. Your database becomes the source of truth for access control.

Supabase (v1.67+, verify in [official docs](https://supabase.com/docs/guides/auth/row-level-security)) makes RLS approachable with:

  • Built-in PostgreSQL policies
  • JWT-based user identification
  • Dashboard policy editor (though SQL is more reliable)
  • ---

    Enable RLS on Your Table

    First, turn on RLS. Without it, policies don't matter:

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

    Critical: Once RLS is enabled on a table, *all queries require a matching policy*. If you enable RLS without policies, you'll see:

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

    Or on SELECT:

    ``` Error: no rows produced by implicit set operation without ALL ```

    Always create your first ALLOW SELECT policy before enabling RLS in production.

    ---

    Core Pattern: User-Owned Data

    Most indie hackers start here—each user sees only their own data:

    ```sql -- Table structure CREATE TABLE posts ( id BIGSERIAL PRIMARY KEY, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, title TEXT NOT NULL, content TEXT, created_at TIMESTAMPTZ DEFAULT NOW() );

    ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

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

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

    -- Policy 3: Users can UPDATE their own posts CREATE POLICY "Users can update their own posts" ON posts FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);

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

    Key function: auth.uid() returns the currently authenticated user's UUID from the JWT token. If no token, it returns NULL (denies access).

    ---

    Testing Policies Locally

    This is where most developers stumble. Use the Authorization header with a valid JWT:

    ```bash

    Get your anon key from Supabase dashboard

    curl -X GET http://localhost:54321/rest/v1/posts \ -H "apikey: $SUPABASE_ANON_KEY" \ -H "Authorization: Bearer $USER_JWT_TOKEN" ```

    Or test directly in SQL editor with:

    ```sql -- Simulate auth context for current session SET request.jwt.claims = jsonb_build_object( 'sub', '12345678-1234-1234-1234-123456789012' );

    -- Now queries use this user_id SELECT * FROM posts; -- Only shows posts where user_id matches ```

    Common mistake: Testing without setting auth context. You'll always see empty results because auth.uid() returns NULL.

    ---

    Advanced: Shared/Public Data

    Not everything is private. Share data with a status flag:

    ```sql CREATE TABLE articles ( id BIGSERIAL PRIMARY KEY, user_id UUID NOT NULL REFERENCES auth.users(id), title TEXT NOT NULL, is_published BOOLEAN DEFAULT FALSE, created_at TIMESTAMPTZ DEFAULT NOW() );

    ALTER TABLE articles ENABLE ROW LEVEL SECURITY;

    -- Anyone can read published articles CREATE POLICY "Public articles are readable" ON articles FOR SELECT USING (is_published = true);

    -- Authors can read their own (published or draft) CREATE POLICY "Authors can read own articles" ON articles FOR SELECT USING (auth.uid() = user_id);

    -- Only author can publish CREATE POLICY "Authors can update own articles" ON articles FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); ```

    ---

    Debugging Failed Queries

    When queries silently fail (return 0 rows), check Postgres logs:

    ``` Error: permission denied for table "users" ```

    This means RLS exists but no policy matches. Verify:

    1. Is RLS enabled? SELECT tablename FROM pg_tables WHERE schemaname='public' AND tablename='posts'; 2. Does the policy exist? SELECT * FROM pg_policies WHERE tablename='posts'; 3. Is auth.uid() returning the right value? Add logging:

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

    ---

    Connection to Authentication

    RLS depends entirely on [JWT authentication setup](/?guide=supabase-auth-setup). Your anon client key must match your JWT secret. If using a custom auth system, you must inject the user_id into the JWT claims yourself—Supabase won't auto-generate it.

    [Read our guide on Supabase auth strategies](/?guide=auth-patterns) for deeper integration patterns.

    ---

    Production Checklist

  • ✅ Enable RLS on all user-facing tables
  • ✅ Test policies with anon key *and* authenticated tokens separately
  • ✅ Never rely on RLS alone for highly sensitive data—also validate in application code
  • ✅ Use EXPLAIN ANALYZE to ensure policies don't cause N+1 queries
  • ✅ Document your policies in code comments
  • ✅ Verify in [official docs](https://supabase.com/docs/guides/auth/row-level-security) for version-specific syntax (versions change)
  • ---

    What am I missing?

    This guide covers the foundation, but RLS gets complex with:

  • Team/organization policies (multiple owners)
  • Time-based access (read-only after 30 days)
  • Cross-table policies (cascading permissions)
  • Performance optimization for large datasets
  • What patterns are *you* using in production? Share exact policy scenarios in comments—real-world examples help everyone level up.

    🔥 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