Supabase Row Level Security 2026: Beginner's Complete Guide

Master Supabase RLS policies with production-ready patterns, real error messages, and step-by-step setup for indie hackers securing user data.

TL;DR

Supabase Row Level Security (RLS) is PostgreSQL's native permission system that prevents unauthorized data access at the database level. Enable RLS on tables, write policies using auth.uid() and custom claims, test thoroughly, and avoid common pitfalls like forgetting to enable RLS or writing overly permissive policies. We'll cover exact setup, real errors you'll encounter, and battle-tested patterns.

---

What is Row Level Security?

Row Level Security (RLS) is a PostgreSQL feature that acts as a gatekeeper—it evaluates a SQL policy before returning any row to a user. Think of it as middleware living inside your database. Without RLS, your API must handle all authorization logic, creating security gaps. With RLS, PostgreSQL itself enforces who can see what.

Supabase exposes RLS configuration through the Dashboard and SQL editor, making it accessible even if you haven't written raw PostgreSQL policies before.

Version note: These patterns work with Supabase (verify in official docs for latest pricing and feature availability at https://supabase.com/docs/guides/auth/row-level-security).

---

Enable RLS on a Table

First, activate RLS. This is non-negotiable for security.

```sql -- Enable RLS on the 'posts' table ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Verify it's enabled SELECT tablename, rowsecurity FROM pg_tables WHERE tablename = 'posts'; -- Should return: rowsecurity = true ```

Critical: When you enable RLS without policies, all reads/writes are blocked by default. You'll see:

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

This is intentional—now write policies to allow access.

---

Core Pattern: SELECT Policy

Let users see only their own data:

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

Breakdown:

  • FOR SELECT: This policy applies to read operations
  • USING (auth.uid() = user_id): PostgreSQL evaluates this condition. Only rows where user_id matches the authenticated user's UID are returned
  • auth.uid(): Supabase function returning the current user's UUID from JWT
  • ---

    Core Pattern: INSERT Policy

    Prevent users from inserting data for other users:

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

    Key difference: WITH CHECK instead of USING. The condition validates before insertion.

    Without this, you'd get:

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

    ---

    Core Pattern: UPDATE & DELETE

    ```sql -- Policy: Users can UPDATE their own posts CREATE POLICY "Users can update own posts" ON posts FOR UPDATE USING (auth.uid() = user_id) -- Check current row WITH CHECK (auth.uid() = user_id); -- Check updated row

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

    For UPDATE, both USING and WITH CHECK are checked—before and after the update.

    ---

    Advanced Pattern: Public Reads, Authenticated Writes

    Common pattern for blogs or forums:

    ```sql -- Anyone can see published posts CREATE POLICY "Public posts are visible" ON posts FOR SELECT USING (published = true);

    -- Only authenticated users can insert CREATE POLICY "Authenticated users can create posts" ON posts FOR INSERT WITH CHECK ( auth.role() = 'authenticated' AND auth.uid() = user_id ); ```

    auth.role() returns 'authenticated', 'anon', or 'service_role'. Service role bypasses RLS (use sparingly on backend).

    ---

    Real Error #3: Missing Service Role Header

    When testing from your backend:

    ``` Error: JWT invalid or expired ```

    If you're using Supabase client with service role, include the header:

    ```javascript const { data, error } = await supabase .from('posts') .select('*') .headers({ 'Authorization': Bearer ${SERVICE_ROLE_KEY} }); ```

    Or use the server client:

    ```javascript const adminClient = createClient(SUPABASE_URL, SERVICE_ROLE_KEY); const { data } = await adminClient .from('posts') .select('*') .eq('published', true); ```

    ---

    Testing Your Policies

    Test in the Supabase SQL editor with SET context:

    ```sql -- Simulate user with UUID 'abc-123' SET request.jwt.claim.sub = 'abc-123';

    SELECT * FROM posts; -- Returns only posts where user_id = 'abc-123'

    RESET request.jwt.claim.sub; ```

    ---

    Production Checklist

  • [ ] RLS enabled on all sensitive tables (auth, profiles, private_data)
  • [ ] Test policies cover: SELECT, INSERT, UPDATE, DELETE
  • [ ] No overly permissive conditions (e.g., USING (true) defeats RLS)
  • [ ] auth.uid() verified in policies (not hardcoded UUIDs)
  • [ ] Service role key stored only on backend, never in frontend
  • [ ] Test unauthenticated access returns empty (not error)
  • [ ] Verify auth.role() for role-based rules
  • ---

    Common Mistakes

    1. Forgetting WITH CHECK on INSERT: Allows inserting any user_id 2. Not enabling RLS: Data is world-readable 3. Using JWT claims without verification: Always validate auth.uid() matches request context 4. Recursive policies: A policy that joins another RLS-protected table can cause performance issues 5. Over-complex conditions: Keep policies readable; logic belongs in application code when possible

    ---

    Related Resources

    For deeper dives, explore [authentication best practices](/guide=auth-security) and [database design patterns](/guide=postgres-design).

    Official Supabase RLS Docs: https://supabase.com/docs/guides/auth/row-level-security

    PostgreSQL RLS Reference: https://www.postgresql.org/docs/current/ddl-rowsecurity.html

    ---

    What am I missing?

    RLS is nuanced—policies interact with your schema design, JWT claims, and business logic in ways that break in production. If you've hit RLS errors not covered here, or found patterns that work better for multi-tenant setups, team-based access, or complex hierarchies, drop them in the comments. This guide will evolve with real-world examples.

    🔥 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