Supabase Row Level Security Guide 2026

Master RLS policies for PostgreSQL databases. Real errors, production patterns, and exact syntax for indie hackers building secure apps.

TL;DR

Row Level Security (RLS) in Supabase lets PostgreSQL enforce data access rules at the database layer. Enable it on tables, write policies matching your auth model, and test thoroughly before production. Real developers catch permission errors early.

What is Row Level Security?

Row Level Security (RLS) is a PostgreSQL feature that Supabase surfaces through its dashboard and API. When enabled on a table, every query—regardless of source—must pass through RLS policies you define. Without RLS, your application layer is the only gatekeeper. One bug and data leaks.

Think of it as a second lock: 1. Application logic says "user can see this" 2. Database policy says "user can actually see this"

If either says no, the user sees nothing.

Enable RLS (The First Step)

In the Supabase dashboard, navigate to your table and toggle "Enable RLS." This is non-reversible for production—once enabled, ALL queries require matching policies or they fail.

You can also enable via SQL:

```sql ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY; ```

Before enabling RLS on existing tables with data, write your policies first. A table with RLS enabled but zero policies is completely locked down.

Write Your First Policy

Here's the canonical pattern for user-owned data:

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

Breakdown:

  • Policy name: "Users can view their own documents" (for your sanity)
  • ON table: public.documents
  • FOR SELECT: This policy applies to reads only
  • USING clause: The condition—auth.uid() returns the authenticated user's UUID
  • The USING clause runs for every row. If true, the row is included in results. If false, it's filtered out silently.

    Real Error Message #1: "No rows returned"

    You write a query that should return data, but get an empty result set. No error—just nothing. This usually means: 1. Your RLS policy doesn't match the query context 2. You're querying as a different user than the policy allows 3. The auth.uid() is NULL (common in service role queries)

    Always test with select auth.uid() in your policy context to confirm the authenticated user is who you think it is.

    CRUD Policies: The Production Pattern

    Most tables need four policies—one per operation:

    ```sql -- SELECT (read) CREATE POLICY "select_own_documents" ON public.documents FOR SELECT USING (auth.uid() = user_id);

    -- INSERT (create) CREATE POLICY "insert_own_documents" ON public.documents FOR INSERT WITH CHECK (auth.uid() = user_id);

    -- UPDATE (modify) CREATE POLICY "update_own_documents" ON public.documents FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);

    -- DELETE (remove) CREATE POLICY "delete_own_documents" ON public.documents FOR DELETE USING (auth.uid() = user_id); ```

    Key difference: INSERT and UPDATE use WITH CHECK, which validates the *new* data. SELECT and DELETE use USING, which validates existing rows.

    Real Error Message #2: "new row violates row level security policy"

    You're trying to insert or update, but the WITH CHECK clause fails:

    ``` Postgres Error: new row violates row level security policy "insert_own_documents" for table "documents" ```

    This means your WITH CHECK condition returned false. Common causes:

  • Trying to set user_id to someone else's UUID
  • Missing user_id field entirely in the insert payload
  • Timestamp or status field validation failing
  • Shared Data: Team Policies

    Not all data is user-owned. Teams, workspaces, and shared resources need different logic:

    ```sql CREATE POLICY "team_members_can_read" ON public.projects FOR SELECT USING ( EXISTS ( SELECT 1 FROM public.team_members tm WHERE tm.team_id = projects.team_id AND tm.user_id = auth.uid() AND tm.deleted_at IS NULL ) ); ```

    This policy checks: "Can the current user read this project? Only if they're an active team member."

    The EXISTS subquery runs for every row. It's slightly slower than direct comparisons but accurate for complex relationships.

    Real Error Message #3: "Permission denied for schema public"

    You've enabled RLS but haven't created any policies yet, OR you're using service role without explicit grants:

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

    Service role bypasses RLS by design (it's meant for admin operations). If you see this, you likely: 1. Disabled RLS by accident 2. Deleted all policies 3. Used service role credentials in client code (security risk—use anon key)

    Test RLS Before Production

    Create test users and verify policies:

    ```sql -- As admin, create test user data INSERT INTO public.documents (id, user_id, title, content) VALUES ( gen_random_uuid(), '550e8400-e29b-41d4-a716-446655440000', 'Secret Doc', 'Only user 550e8400... should see this' );

    -- Then authenticate as that user and query -- If you see the row, RLS works -- If you see nothing, your policy is wrong ```

    In production, use [Supabase's policy simulator](https://supabase.com/docs/guides/auth/row-level-security) to verify complex policies before deployment.

    Common Gotchas

    Anonymous users: auth.uid() returns NULL for unauthenticated requests. Policies checking auth.uid() = user_id silently reject them. This is usually desired.

    Performance: RLS policies add query overhead. Index columns used in USING/WITH CHECK clauses (like user_id). See [query optimization](/?guide=postgres-performance).

    Cascade deletes: RLS doesn't prevent foreign key cascades, but it prevents the *user* from triggering them if they can't access the parent row.

    Service role: Queries with service role credentials bypass RLS entirely. Never expose service role keys in client code. Use anon key + RLS + [realtime subscriptions](/?guide=supabase-realtime) instead.

    Verify Official Docs

    For version-specific details, [verify in official Supabase RLS docs](https://supabase.com/docs/guides/auth/row-level-security). Policy syntax may evolve; this guide reflects patterns current as of early 2026.

    What am I missing?

    Did this guide miss your RLS headache? Comment below:

  • Policies for multi-tenant SaaS architectures?
  • RLS performance tuning on million-row tables?
  • Migration strategies from application-level auth?
  • Debugging complex EXISTS subqueries?
  • Share your production patterns and we'll update this guide.

    🔥 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