How to Fix Supabase RLS violating policy or infinite recursion errors
Quick Diagnosis
- ✓ Are your
insertorupdatequeries throwing a42501exception indicating that the “new row violates row-level security policy”? - ✓ Does your console output display
42P17: infinite recursion detected in policywhen performing database operations? - ✓ Did you recently enable Row-Level Security (RLS) on a table without creating matching write and read permissions?
Environment
The Supabase database policies execute directly inside the PostgreSQL instance, affecting queries sent from the supabase-js Web SDK, Next.js Route Handlers, and Serverless Edge Functions.
| Database Write Strategy | Active Policies Configuration | SDK Invocation Syntax | Database Execution Outcome |
|---|---|---|---|
| Insert client-side payload | INSERT policy defined, SELECT policy missing |
supabase.from('tasks').insert(...) |
Failed (PostgREST attempts implicit SELECT returning new row, violates read privileges) |
| Insert client-side payload | INSERT policy defined, SELECT policy missing |
supabase.from('tasks').insert(...).select() |
Failed (Explicit select triggers RLS policy violation exception) |
| Insert client-side payload | INSERT policy defined, SELECT policy missing |
supabase.from('tasks').insert(...).select().single() |
Failed (Returns new row violates RLS error) |
| Insert client-side payload | INSERT policy defined, SELECT policy missing |
supabase.from('tasks').insert(...).select() with SELECT policy added |
Success (Satisfies read and write verification filters) |
Minimal Repro
Row-Level Security (RLS) acts as a firewall directly on the PostgreSQL storage engine level. When RLS is active (ALTER TABLE table_name ENABLE ROW LEVEL SECURITY), PostgreSQL denies all read, write, update, and delete access by default unless specific authorization policies allow them. When invoking an insert statement via the supabase-js client, PostgREST (the HTTP API layer for PostgreSQL) automatically compiles a query containing a SELECT returning clause to fetch and return the newly created records to the client wrapper. If your database has an INSERT policy active but lacks a matching SELECT policy for the authenticated user, PostgreSQL successfully writes the database row but aborts the entire transaction when compiling the returning record. The API client intercepts this read-permission failure and returns a 42501 database error with the new row violates row-level security policy exception. Separately, creating policy checks that query the current table within the check clause triggers circular dependency loops (42P17 infinite recursion), which can be resolved by deploying SECURITY DEFINER function wrappers.
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY);
export async function createNewTask(title, userId) {
// CRASH: PostgreSQL writes row but PostgREST fails to return it
const { data, error } = await supabase
.from('tasks')
.insert({ title, user_id: userId });
if (error) throw error;
return data;
}
PostgrestError: new row violates row-level security policy for table "tasks" (Status Code: 42501)
Resolution
When resolving Row-Level Security exceptions, developers can choose between two main structural options depending on whether they are solving read violations or infinite recursion loops.
Option A: Create Matching SELECT and INSERT Policies (For Write Violations)
If you configure client-side writes, ensuring that users have both write (INSERT) and read (SELECT) permissions is applicable. This configuration allows PostgREST returning clauses to resolve successfully.
- Log into your Supabase SQL editor or migration files.
- Enable RLS on the target table using
ALTER TABLE. - Define the
INSERTpolicy withWITH CHECKconditions matching user IDs. - Define the corresponding
SELECTpolicy to permit user reads.
-- 1. Enable RLS
ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY;
-- 2. Define insert privileges
CREATE POLICY "Allow authenticated inserts"
ON public.tasks
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
-- 3. Define select privileges to satisfy post-insert SELECT checks
CREATE POLICY "Allow users to view own tasks"
ON public.tasks
FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
Option B: Use SECURITY DEFINER Functions (For Infinite Recursion)
If you create checks that verify relationships by querying tables protected by the same policy rule, using a SECURITY DEFINER helper function is applicable. This prevents circular recursion deadlocks.
- Create a PostgreSQL function matching the check conditions.
- Declare the function with
SECURITY DEFINERso it executes with bypass privileges. - Call the function inside the policy
USINGexpression.
-- Create database helper function executing as creator role
CREATE OR REPLACE FUNCTION public.check_user_is_team_member(team_id uuid, user_id uuid)
RETURNS boolean AS $$
BEGIN
-- Bypasses Team table RLS checks during validation query run
RETURN EXISTS (
SELECT 1 FROM public.team_members
WHERE team_members.team_id = check_user_is_team_member.team_id
AND team_members.user_id = check_user_is_team_member.user_id
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Bind policy checking teams without recursion
CREATE POLICY "Allow members view teams"
ON public.teams
FOR SELECT
TO authenticated
USING (check_user_is_team_member(id, auth.uid()));
When This Fix Won’t Work
If you utilize supabase-js service keys (service_role) inside backend APIs, RLS policies are bypassed entirely. Ensure your client handles credentials contexts correctly before applying RLS rules.
Operational Runbook
Case 1: Write Violations
- Verify
SELECTpolicy exists on the table. - Confirm the target user’s UUID matches the token
auth.uid().
Case 2: Circular Dependency Deadlocks
- Wrap relationship check queries inside helper SQL functions.
- Apply
SECURITY DEFINERattributes.
Rollback Strategy
To roll back these changes, execute sql commands to disable Row-Level Security on the target tables using the ALTER TABLE table_name DISABLE ROW LEVEL SECURITY declaration, drop the database policies using DROP POLICY statements, and remove custom check functions from the database schema footprint.
Verification
- Client application database queries execute successfully returning HTTP status codes of 201 Created.
- Client-side API payload response lists contain the newly populated data records.
- PostgreSQL runtime console logs contain zero 42501 policy violations or 42P17 recursion deadlock aborts.
Error Trigger Point Lifecycle
Initialize client context ➔ Execute insert or update query ➔ Evaluate RLS policy check filters [ERROR OCCURS HERE] ➔ PostgreSQL verification scan ➔ Resolve query returning parameters ➔ Return client promise results
References
- Supabase Row-Level Security Reference: https://supabase.com/docs/guides/auth/row-level-security
- Evidence Tier: Official
- Contribution: Verified the Supabase RLS row-level security mechanisms, PostgREST SELECT return behaviors, and PostgreSQL SECURITY DEFINER functions.
- PostgreSQL Security Definer Documentation: https://www.postgresql.org/docs/current/sql-createfunction.html
- Evidence Tier: Official
- Contribution: Verified the SECURITY DEFINER execution scopes, schema privileges bindings, and circular dependency checks.
- Supabase Core Repo GitHub Issue #8754: https://github.com/supabase/supabase/issues/8754
- Evidence Tier: Community
- Contribution: Captured the real-world execution symptoms and reproduction parameters of the RLS recursion and write violation exceptions.