Skip to main content

Overview

Supabase Row-Level Security (RLS) ensures users only access data they’re authorized to see. Every table has RLS enabled.

Policy Patterns

1. Organization Isolation

Users can only see data within their organization:
CREATE POLICY "Users can view own org data"
ON users
FOR SELECT
USING (
  organization_id = (
    SELECT organization_id 
    FROM users 
    WHERE id = auth.uid()
  )
);

2. User-Owned Data

Users can only access their own records:
CREATE POLICY "Users can view own profile"
ON creator_profiles
FOR SELECT
USING (user_id = auth.uid());

CREATE POLICY "Users can update own profile"
ON creator_profiles
FOR UPDATE
USING (user_id = auth.uid());

3. Service Role Bypass

Internal services (scraping, sync) use service role to bypass RLS:
-- Service role has full access
-- Use SUPABASE_SERVICE_ROLE_KEY for server operations

4. Public Read Access

Some tables are publicly readable:
CREATE POLICY "Public can view published posts"
ON publisher_posts
FOR SELECT
USING (status = 'published');

CREATE POLICY "Public can view products"
ON shopify_products
FOR SELECT
USING (true);

Policies by Table

users

PolicyOperationRule
View own dataSELECTid = auth.uid()
Update own dataUPDATEid = auth.uid()
Admin view allSELECTrole = 'admin'

organizations

PolicyOperationRule
Members viewSELECTUser belongs to org
Admin modifyUPDATEUser is org admin

publisher_posts

PolicyOperationRule
Public read publishedSELECTstatus = 'published'
Authors read own draftsSELECTauthor_id = auth.uid()
Authors update ownUPDATEauthor_id = auth.uid()

creator_profiles

PolicyOperationRule
Public view verifiedSELECTverified = true
Owner full accessALLuser_id = auth.uid()

creator_licenses

PolicyOperationRule
Public view activeSELECTstatus = 'active'
Creator manage ownALLcreator_id matches user’s profile

agency_clients

PolicyOperationRule
Staff view assignedSELECTUser assigned to client
Admin full accessALLUser is admin

jarvis_conversations

PolicyOperationRule
User own onlyALLuser_id = auth.uid()

jarvis_messages

PolicyOperationRule
User own conversationsALLConversation belongs to user

Implementation Example

Adding RLS to New Table

-- 1. Enable RLS
ALTER TABLE new_table ENABLE ROW LEVEL SECURITY;

-- 2. Create policies
CREATE POLICY "Users can view own data"
ON new_table
FOR SELECT
USING (user_id = auth.uid());

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

CREATE POLICY "Users can update own data"
ON new_table
FOR UPDATE
USING (user_id = auth.uid());

CREATE POLICY "Users can delete own data"
ON new_table
FOR DELETE
USING (user_id = auth.uid());

Testing Policies

-- Test as specific user
SET LOCAL ROLE authenticated;
SET LOCAL request.jwt.claim.sub = 'user-uuid-here';

-- Try query
SELECT * FROM creator_profiles;
-- Should only return that user's profile

-- Reset
RESET ROLE;

Service Role Usage

For server-side operations that need full access:
import { createClient } from '@supabase/supabase-js'

// Client with service role - bypasses RLS
const supabaseAdmin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY! // Full access
)

// Client with anon key - respects RLS
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // RLS enforced
)
When to use service role:
  • Scraping pipelines
  • Webhook handlers
  • Background jobs
  • Admin operations
When to use anon/user key:
  • User-facing queries
  • API routes with user context
  • Client-side operations

Security Checklist

RLS enabled on every table
No tables with public write access
Service role key never exposed to client
All policies tested with sample users
Admin bypass requires explicit role check

Common Gotchas

1. Forgetting RLS on New Tables

Problem: New tables default to public access. Solution: Always enable RLS immediately:
ALTER TABLE new_table ENABLE ROW LEVEL SECURITY;

2. Missing INSERT Policies

Problem: Users can SELECT but can’t INSERT. Solution: Use WITH CHECK for INSERT/UPDATE:
CREATE POLICY "insert_own"
ON table_name
FOR INSERT
WITH CHECK (user_id = auth.uid());

3. Circular Dependencies

Problem: Policy references table that references original table. Solution: Use security definer functions:
CREATE FUNCTION get_user_org_id()
RETURNS uuid
SECURITY DEFINER
AS $$
  SELECT organization_id FROM users WHERE id = auth.uid();
$$ LANGUAGE sql STABLE;

Supabase RLS Docs

Official Supabase RLS documentation