Supabase Row Level Security 2026: Beginner Guide

Master RLS policies in Supabase. Real code examples, common errors, and production patterns for securing your database.

TL;DR

Row Level Security (RLS) in Supabase lets you enforce database-level access control at the row level. Write SQL policies that automatically filter queries based on the authenticated user. This guide covers setup, real errors, and patterns you'll actually use.

What is Row Level Security?

Row Level Security is a PostgreSQL feature that Supabase exposes through its dashboard. Instead of trusting your application layer to filter data, RLS ensures the *database itself* only returns rows a user is allowed to see.

Without RLS: ```javascript // Vulnerable: relies on app logic const { data } = await supabase .from('posts') .select('*') .eq('user_id', currentUser.id); // Easy to bypass ```

With RLS: ```javascript // Secure: database enforces access const { data } = await supabase .from('posts') .select('*'); // Database filters automatically ```

The database will only return posts where user_id matches the authenticated user's ID.

Enabling RLS on a Table

In the Supabase dashboard (v1.0+, [verify in official docs](https://supabase.com/docs/guides/auth/row-level-security)):

1. Go to Authentication → Policies 2. Select your table (e.g., posts) 3. Click Enable RLS

Once enabled, all access is denied by default until you write policies.

Your First Policy: SELECT

Let's assume you have a posts table with columns: id, user_id, title, content.

Goal: Users can only see their own posts.

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

Breakdown:

  • FOR SELECT: This policy applies to read operations
  • USING: The condition that must be true
  • auth.uid(): Supabase function returning the authenticated user's UUID
  • user_id: The column we're checking
  • Test it (Supabase JS v2.39.0+): ```javascript const { data, error } = await supabase .from('posts') .select('*');

    if (error) { console.log(error); // Error: "new row violates row-level security policy for table \"posts\"" } ```

    INSERT Policy: Let Users Create Posts

    ```sql CREATE POLICY "Users can insert their own posts" ON posts FOR INSERT WITH CHECK (auth.uid() = user_id); ```

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

    Common Error #1: ``` error: new row violates row-level security policy for table "posts" ```

    This happens when: ```javascript // ❌ Trying to insert with wrong user_id const { error } = await supabase .from('posts') .insert([{ title: 'My Post', user_id: 'different-uuid', content: '...' }]);

    // ✅ Always use the authenticated user's ID const { data: { user } } = await supabase.auth.getUser(); const { error } = await supabase .from('posts') .insert([{ title: 'My Post', user_id: user.id, content: '...' }]); ```

    UPDATE and DELETE Policies

    ```sql CREATE POLICY "Users can update their own posts" ON posts FOR UPDATE WITH CHECK (auth.uid() = user_id);

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

    Sharing Data: Public Read, Private Write

    Common pattern: public can read posts, only authors can edit.

    ```sql -- Everyone can read CREATE POLICY "Posts are public for select" ON posts FOR SELECT USING (true);

    -- Only authors can update/delete CREATE POLICY "Authors can update their posts" ON posts FOR UPDATE WITH CHECK (auth.uid() = user_id);

    CREATE POLICY "Authors can delete their posts" ON posts FOR DELETE USING (auth.uid() = user_id); ```

    Working with Joins and Related Tables

    Assuming comments table with post_id and author_id:

    ```sql CREATE POLICY "Users can read comments on public posts" ON comments FOR SELECT USING ( EXISTS ( SELECT 1 FROM posts WHERE posts.id = comments.post_id ) ); ```

    This allows users to see comments on any post they can see.

    Service Role and Bypass

    Your backend should use the service role key (keep this secret!) to bypass RLS:

    ```javascript // Client - respects RLS const client = supabase.createClient(url, anonKey);

    // Server - bypasses RLS const admin = supabase.createClient(url, serviceRoleKey, { auth: { persistSession: false } });

    const { data } = await admin .from('posts') .select('*'); // Returns ALL posts, RLS ignored ```

    Common Error #2: ``` error: Failed to fetch data Details: Auth session missing! ```

    You're using the anon key for operations that need authentication:

    ```javascript // ❌ Without auth, RLS blocks everything const { error } = await supabase .from('posts') .select('*');

    // ✅ Ensure user is signed in first const { data: { session } } = await supabase.auth.getSession(); if (session) { const { data } = await supabase.from('posts').select('*'); } ```

    Common Error #3: Policy Logic Bugs

    ``` Unexpected error: prepared statement \"\" does not exist ```

    Often caused by typos in column names or auth function calls:

    ```sql -- ❌ Wrong function name CREATE POLICY "broken" ON posts FOR SELECT USING (auth.userid() = user_id); -- typo: should be auth.uid()

    -- ✅ Correct CREATE POLICY "working" ON posts FOR SELECT USING (auth.uid() = user_id); ```

    Testing Policies

    Use the Supabase dashboard SQL Editor to test:

    ```sql SET request.jwt.claims = '{"sub": "your-test-uuid"}'; SELECT * FROM posts; -- Simulates a request from that user ```

    Or use different user sessions in your app.

    Performance Considerations

  • RLS policies are evaluated for *every* query
  • Complex nested queries can be slow
  • Always index columns used in USING clauses:
  • ```sql CREATE INDEX idx_posts_user_id ON posts(user_id); ```

    What About Anonymous Users?

    By default, unauthenticated users have auth.uid() = NULL. Your policies must explicitly allow them:

    ```sql CREATE POLICY "Public read access" ON posts FOR SELECT USING ( published = true OR auth.uid() = user_id ); ```

    Further Reading

  • [Supabase RLS Official Guide](https://supabase.com/docs/guides/auth/row-level-security)
  • [PostgreSQL RLS Documentation](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
  • [Supabase Auth Documentation](https://supabase.com/docs/guides/auth)
  • [Authentication Setup Guide](/?guide=supabase-auth-setup)
  • [Database Security Best Practices](/?guide=postgres-security)
  • What am I missing?

    Have you run into RLS issues not covered here? Specific patterns you use in production? Share your experiences, edge cases, and corrections in the comments—this guide improves with reader input.

    🔥 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