Supabase Row Level Security Guide 2026

Master RLS policies with production-ready patterns. Real errors, exact configs, and beginner-friendly examples for indie hackers.

TL;DR

Row Level Security (RLS) in Supabase lets you control which database rows users can access at the database level—not your application code. Enable it per table, write policies using PostgreSQL, and test thoroughly. Most bugs come from policy logic errors and missing auth.uid() context.

---

What is Row Level Security?

Row Level Security (RLS) is a PostgreSQL feature that Supabase exposes to let you enforce access control directly in the database. Instead of filtering results in your application (vulnerable to bugs), RLS prevents unauthorized rows from being returned by the database itself.

Example: A user should only see their own notes. With RLS, even if your frontend code fails to filter, the database won't return other users' notes.

Enabling RLS on a Table

RLS is disabled by default in Supabase. You must explicitly enable it:

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

Once enabled, all queries are blocked until you create policies. This is intentional—fail-secure by default.

Real Error #1: Missing Policies

``` ERROR: new row violates row-level security policy for table "notes" DETAIL: A SELECT policy does not exist (or has been dropped) for table "notes" [code: 42501] ```

Fix: Create a SELECT policy before querying.

Writing Your First Policy

Policies are SQL expressions evaluated in the database context. The auth.uid() function returns the logged-in user's UUID.

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

Breakdown:

  • FOR SELECT: This policy applies to SELECT queries
  • USING: The condition that must be TRUE for a row to be returned
  • auth.uid(): Supabase-provided function returning current user's UUID
  • user_id: Column in your table storing the note's owner
  • CRUD Policy Patterns

    SELECT: Users see only their own data

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

    INSERT: Users can create notes for themselves

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

    Note: WITH CHECK is used for INSERT/UPDATE instead of USING.

    UPDATE: Users can only modify their own notes

    ```sql CREATE POLICY "update_own_notes" ON notes FOR UPDATE USING (auth.uid() = user_id) -- Check before update WITH CHECK (auth.uid() = user_id); -- Validate new row state ```

    DELETE: Users can delete only their own notes

    ```sql CREATE POLICY "delete_own_notes" ON notes FOR DELETE USING (auth.uid() = user_id); ```

    Real Error #2: Forgetting WITH CHECK on INSERT

    ``` ERROR: new row violates row-level security policy for table "notes" DETAIL: A WITH CHECK option is not defined for UPDATE/INSERT on table "notes" [code: 42501] ```

    Fix: Add WITH CHECK (auth.uid() = user_id) to your INSERT/UPDATE policies.

    Advanced: Shared Data & Roles

    Public readable, user-editable data

    ```sql -- Anyone can read published articles CREATE POLICY "public_read_articles" ON articles FOR SELECT USING (published = true);

    -- Only authors can update their articles CREATE POLICY "author_update_articles" ON articles FOR UPDATE USING (auth.uid() = author_id) WITH CHECK (auth.uid() = author_id); ```

    Team-based access

    ```sql CREATE POLICY "team_access" ON projects FOR SELECT USING ( auth.uid() IN ( SELECT user_id FROM team_members WHERE team_id = projects.team_id ) ); ```

    Real Error #3: NULL auth.uid() in Anonymous Mode

    ``` ERROR: column "user_id" must appear in the GROUP BY clause or be subject to an aggregate function [code: 42803] ```

    This often masks the real issue: auth.uid() returns NULL for unauthenticated users. If your policy uses auth.uid() = user_id, it silently blocks anonymous access (correct behavior, but confusing error message).

    Fix: Explicitly handle anonymous users:

    ```sql CREATE POLICY "select_public_or_own" ON notes FOR SELECT USING ( published = true OR auth.uid() = user_id ); ```

    Testing RLS Policies

    In Supabase Studio

    1. Go to SQL Editor 2. Run: SELECT * FROM notes; (as authenticated user) 3. Your policies are automatically applied

    In JavaScript (Supabase Client v2.39.0+, verify in [official docs](https://supabase.com/docs/guides/auth/row-level-security?version=latest))

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

    const supabase = createClient(url, key);

    // Sign in user await supabase.auth.signInWithPassword({ email, password });

    // This SELECT respects RLS policies const { data, error } = await supabase .from('notes') .select('*');

    // Only rows matching the policy are returned console.log(data); // Shows only notes where user_id = current user's uuid ```

    Viewing Existing Policies

    ```sql -- List all policies for a table SELECT * FROM pg_policies WHERE tablename = 'notes';

    -- Detailed policy definition SELECT polname, poldef FROM pg_policies WHERE tablename = 'notes'; ```

    Common Gotchas

    1. RLS doesn't apply to service role: When using supabase-js with the service role key (server-only), RLS is bypassed. This is intentional for admin operations, but dangerous if exposed.

    2. Recursive policies: Policies can't reference rows in the same table (infinite recursion). Use subqueries on *other* tables instead.

    3. Performance: Complex policies with subqueries can slow queries. Use EXPLAIN ANALYZE to debug:

    ```sql EXPLAIN ANALYZE SELECT * FROM projects WHERE team_id = 1; ```

    4. Auth context: RLS reads from auth.jwt(). If you're calling Supabase from a backend using the service role, auth.uid() is NULL.

    Debugging Checklist

  • [ ] Is RLS enabled? Run SELECT relrowsecurity FROM pg_class WHERE relname = 'notes'; (should return true)
  • [ ] Do policies exist? Check pg_policies
  • [ ] Is user authenticated? Check auth.uid() in policy context
  • [ ] Are you using the right policy type (SELECT, INSERT, UPDATE)?
  • [ ] Did you use USING for SELECT/DELETE and WITH CHECK for INSERT/UPDATE?
  • Further Reading

  • [Supabase RLS Documentation](https://supabase.com/docs/guides/auth/row-level-security?version=latest)
  • [PostgreSQL RLS Official](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
  • [Supabase Security Best Practices](https://supabase.com/docs/guides/database/postgres/security-overview)
  • Related guides: [Supabase Authentication Setup](/?guide=supabase-auth) and [Database Triggers](/?guide=database-triggers).

    ---

    What am I missing?

    RLS is powerful but nuanced. Did I gloss over a frustrating edge case? Are there patterns I missed? Drop your RLS war stories and corrections in the comments—this guide should evolve with your 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