Supabase Row Level Security: Beginner's Guide 2026

Master RLS policies in Supabase. Learn JWT claims, production patterns, and avoid common policy mistakes that break apps.

TL;DR

Supabase Row Level Security (RLS) uses PostgreSQL policies to control data access at the database level. Enable RLS on tables, create policies using auth.uid() and JWT claims, test with authenticated sessions, and always set a deny-by-default policy first. Common mistake: forgetting FOR SELECT returns no rows even if other policies exist.

---

What is Row Level Security?

Row Level Security (RLS) in Supabase is PostgreSQL's native mechanism to restrict database rows based on the user making the request. Instead of handling authorization in your application code, RLS enforces it at the database level—meaning malicious queries or direct API calls can't bypass your rules.

Supabase wraps PostgreSQL's RLS with JWT token authentication. When a user logs in via Supabase Auth, they receive a JWT containing their uid (user ID) and custom claims. This token is sent with every request, and Supabase extracts claims as PostgreSQL variables accessible in your policies.

Key benefit: RLS is enforced whether queries come from your frontend, backend, or direct SQL—no exceptions.

---

Prerequisites

  • Supabase project created (verify current pricing at [official pricing](https://supabase.com/pricing))
  • Basic PostgreSQL knowledge
  • Supabase CLI v1.150.0+ (verify in official docs for latest)
  • A table ready for RLS (we'll use posts in examples)
  • ---

    Enable RLS on a Table

    RLS is disabled by default. Toggle it via the dashboard or SQL:

    ```sql -- Enable RLS ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

    -- Disable (dangerous—all users can access all rows) ALTER TABLE posts DISABLE ROW LEVEL SECURITY; ```

    Once enabled, no policies = no access (not even admins). This is intentional. Always create policies immediately after enabling RLS.

    ---

    Understanding JWT Claims

    When a user authenticates, Supabase Auth creates a JWT. The payload contains:

    ```json { "sub": "12345-67890-abcdef", "aud": "authenticated", "role": "authenticated", "email": "user@example.com", "user_metadata": { "subscription_tier": "pro" } } ```

    Inside PostgreSQL policies, access these via:

  • auth.uid() → user's UUID (sub field)
  • auth.jwt() → entire JWT object
  • auth.jwt()->>'email' → extract email from JWT
  • (auth.jwt()->'user_metadata'->>'subscription_tier') → custom metadata
  • ---

    Production-Ready Policy Patterns

    Pattern 1: Users See Only Their Own Data

    ```sql -- Assume 'posts' table has user_id column CREATE POLICY "Users view own posts" ON posts FOR SELECT USING (auth.uid() = user_id);

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

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

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

    Why two clauses on UPDATE?

  • USING: who can modify this row
  • WITH CHECK: what the row can become after update
  • Pattern 2: Public Read, Authenticated Write

    ```sql CREATE POLICY "Anyone read published posts" ON posts FOR SELECT USING (is_published = true);

    CREATE POLICY "Authors edit published posts" ON posts FOR UPDATE USING (auth.uid() = user_id AND is_published = true) WITH CHECK (auth.uid() = user_id); ```

    Pattern 3: Role-Based Access

    ```sql CREATE POLICY "Admins full access" ON posts FOR ALL USING (auth.jwt()->>'role' = 'admin');

    CREATE POLICY "Moderators delete spam" ON posts FOR DELETE USING (auth.jwt()->>'role' = 'moderator'); ```

    Verify custom claims in official docs before using—custom claims setup requires auth configuration.

    ---

    Common Console Errors

    Error 1: "new row violates row-level security policy"

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

    Cause: Your INSERT values fail the WITH CHECK clause. Usually: trying to set user_id to a different user ID.

    Fix: ```javascript // ❌ Wrong const { data, error } = await supabase .from('posts') .insert({ title: 'Hello', user_id: 'other-user-id' });

    // ✅ Correct const { data, error } = await supabase .from('posts') .insert({ title: 'Hello', user_id: session.user.id }); ```

    Error 2: "permission denied for schema public"

    ``` Error: permission denied for schema public ```

    Cause: Table has RLS enabled but no SELECT policy exists. The role trying to query has zero permissions.

    Fix: Create at least one SELECT policy before testing.

    Error 3: "column auth.uid() does not exist"

    ``` Error: column auth.uid() does not exist ```

    Cause: Attempting to use auth functions in a non-authenticated context (e.g., service role without proper token). Or typo in policy.

    Fix: Ensure request includes valid JWT token. In tests, use supabase.auth.signInWithPassword() first.

    ---

    Testing RLS Policies

    Via Supabase Dashboard

    1. Navigate to SQL Editor 2. Run SELECT current_user_id(); to verify auth context 3. Query your table—should respect policies

    Via Code (JavaScript)

    ```javascript const supabase = createClient(url, anonKey);

    // Unauthenticated request—fails if SELECT policy requires auth const { data, error } = await supabase.from('posts').select();

    // Authenticated request const { data: user } = await supabase.auth.signInWithPassword({ email: 'user@example.com', password: 'password' });

    const { data: posts, error } = await supabase .from('posts') .select(); // Returns only rows matching RLS policies for this user ```

    ---

    Debugging RLS Issues

    Check enabled tables: ```sql SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public'; ```

    View all policies on a table: ```sql SELECT * FROM pg_policies WHERE tablename = 'posts'; ```

    Test a specific policy: ```sql SET ROLE authenticated; SET request.jwt.claim.sub = '12345-67890-abcdef'; SELECT * FROM posts; -- Now runs as that user ```

    ---

    Best Practices

    1. Always start with deny-first: Create a restrictive base policy, then add permissive ones 2. Index filtered columns: RLS often filters on user_id—ensure it's indexed 3. Test unauthenticated access: Verify anon role can't access private data 4. Document policies: Use clear naming ("Users view own posts", not "policy1") 5. Use service_role sparingly: It bypasses RLS—only for admin operations 6. Audit JWT claims: Log policy failures to catch abuse

    ---

    Related Guides

  • [Supabase Authentication Setup](/?guide=supabase-auth)
  • [PostgreSQL Security Best Practices](/?guide=postgres-security)
  • ---

    What am I missing?

    RLS policies are contextual—different setups have different needs. What edge cases have bitten you? Comments on:

  • Team-based access patterns
  • Multi-tenant RLS strategies
  • Performance tuning with complex policies
  • Migration paths from app-level authorization
  • Corrections or additions welcome below. For official details, check [Supabase RLS Documentation](https://supabase.com/docs/guides/auth/row-level-security).

    🔥 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