Expert guidance for writing Supabase PostgreSQL row-level security (RLS) policies. Use when creating, modifying, or troubleshooting RLS policies for Supabase databases, implementing access control patterns, or setting up table-level security rules.
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Generate production-ready row-level security policies for Supabase PostgreSQL databases following best practices and Supabase-specific conventions.
All policies follow this structure:
CREATE POLICY "Policy description" ON table_name
FOR operation
TO role
USING (condition)
WITH CHECK (condition);
Never use FOR ALL - always create separate policies for each operation (SELECT, INSERT, UPDATE, DELETE).
Supabase maps requests to two built-in roles:
anon: Unauthenticated users (not logged in)authenticated: Authenticated users (logged in)Apply roles with the TO clause, which must come after the operation:
-- CORRECT
CREATE POLICY "policy name" ON profiles
FOR select
TO authenticated
USING (true);
-- INCORRECT - TO must come after FOR
CREATE POLICY "policy name" ON profiles
TO authenticated
FOR select
USING (true);
auth.uid() - Returns the ID of the authenticated user making the request
auth.jwt() - Returns the JWT with access to user metadata:
raw_user_meta_data: User-updatable, not secure for authorizationraw_app_meta_data: Cannot be updated by user, use for authorizationExample using JWT for team membership:
CREATE POLICY "User is in team" ON my_table
TO authenticated
USING (team_id IN (
SELECT auth.jwt() -> 'app_metadata' -> 'teams'
));
Check for multi-factor authentication using AAL (Assurance Level):
CREATE POLICY "Restrict updates" ON profiles
AS restrictive
FOR update
TO authenticated
USING ((SELECT auth.jwt()->>'aal') = 'aal2');
CREATE INDEX userid ON test_table USING btree (user_id);
-- OPTIMIZED - uses initPlan caching
CREATE POLICY "policy" ON test_table
TO authenticated
USING ((SELECT auth.uid()) = user_id);
-- SLOWER - calls function on every row
CREATE POLICY "policy" ON test_table
TO authenticated
USING (auth.uid() = user_id);
-- SLOW - joins on each row
CREATE POLICY "Team access" ON test_table
TO authenticated
USING (
(SELECT auth.uid()) IN (
SELECT user_id FROM team_user
WHERE team_user.team_id = team_id -- JOIN
)
);
-- FAST - no join
CREATE POLICY "Team access" ON test_table
TO authenticated
USING (
team_id IN (
SELECT team_id FROM team_user
WHERE user_id = (SELECT auth.uid()) -- no join
)
);
TO clause:-- OPTIMIZED
CREATE POLICY "policy" ON rls_test
TO authenticated
USING ((SELECT auth.uid()) = user_id);
Always use double apostrophes in SQL strings:
-- CORRECT
name = 'Night''s watch'
-- INCORRECT
name = 'Night\'s watch'
Create separate policies for each operation - PostgreSQL doesn't support multiple operations per policy:
-- INCORRECT
CREATE POLICY "policy" ON profiles
FOR insert, delete -- NOT SUPPORTED
TO authenticated
WITH CHECK (true)
USING (true);
-- CORRECT
CREATE POLICY "Can create profiles" ON profiles
FOR insert
TO authenticated
WITH CHECK (true);
CREATE POLICY "Can delete profiles" ON profiles
FOR delete
TO authenticated
USING (true);
-- Users can view their own records
CREATE POLICY "Users view own records" ON test_table
FOR select
TO authenticated
USING ((SELECT auth.uid()) = user_id);
-- Users can update their own records
CREATE POLICY "Users update own records" ON test_table
FOR update
TO authenticated
USING ((SELECT auth.uid()) = user_id)
WITH CHECK ((SELECT auth.uid()) = user_id);
-- Users can access team records
CREATE POLICY "Team member access" ON test_table
FOR select
TO authenticated
USING (
team_id IN (
SELECT team_id FROM team_user
WHERE user_id = (SELECT auth.uid())
)
);
-- Anyone can read
CREATE POLICY "Public read" ON profiles
FOR select
TO anon, authenticated
USING (true);
-- Only authenticated can insert
CREATE POLICY "Authenticated insert" ON profiles
FOR insert
TO authenticated
WITH CHECK (true);
Policies are combined with OR - if any policy grants access, it's allowed. Always prefer PERMISSIVE unless you have a specific need for RESTRICTIVE.
All RESTRICTIVE policies must pass (AND logic). Use only for additional security layers like MFA requirements. Discourage use because:
Always wrap SQL in markdown code blocks with language tag:
CREATE POLICY "Descriptive policy name" ON books
FOR insert
TO authenticated
WITH CHECK ((SELECT auth.uid()) = author_id);
Policy naming: Use descriptive sentences in double quotes explaining what the policy does.
Explanations: Provide as separate text, never inline SQL comments.
Before finalizing policies, verify:
auth.uid() instead of current_userSELECT for performanceTO clauseFOR ALL)If user requests anything not related to RLS policies, explain that this skill only assists with Supabase row-level security policy creation and suggest they rephrase their request or use other tools.