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
Policy Operation Rule View own data SELECT id = auth.uid()Update own data UPDATE id = auth.uid()Admin view all SELECT role = 'admin'
organizations
Policy Operation Rule Members view SELECT User belongs to org Admin modify UPDATE User is org admin
publisher_posts
Policy Operation Rule Public read published SELECT status = 'published'Authors read own drafts SELECT author_id = auth.uid()Authors update own UPDATE author_id = auth.uid()
creator_profiles
Policy Operation Rule Public view verified SELECT verified = trueOwner full access ALL user_id = auth.uid()
creator_licenses
Policy Operation Rule Public view active SELECT status = 'active'Creator manage own ALL creator_id matches user’s profile
agency_clients
Policy Operation Rule Staff view assigned SELECT User assigned to client Admin full access ALL User is admin
jarvis_conversations
Policy Operation Rule User own only ALL user_id = auth.uid()
jarvis_messages
Policy Operation Rule User own conversations ALL Conversation 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