<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ahmed Hassan Yaseen</title>
    <description>The latest articles on DEV Community by Ahmed Hassan Yaseen (@ahmed_hassanyaseen_1b7ab).</description>
    <link>https://dev.to/ahmed_hassanyaseen_1b7ab</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4191596%2Fd71d6466-2f95-48e8-a258-753e7da9e9da.png</url>
      <title>DEV Community: Ahmed Hassan Yaseen</title>
      <link>https://dev.to/ahmed_hassanyaseen_1b7ab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahmed_hassanyaseen_1b7ab"/>
    <language>en</language>
    <item>
      <title>Fixing "new row violates row-level security policy" in Supabase</title>
      <dc:creator>Ahmed Hassan Yaseen</dc:creator>
      <pubDate>Sun, 18 Oct 2026 06:35:18 +0000</pubDate>
      <link>https://dev.to/ahmed_hassanyaseen_1b7ab/fixing-new-row-violates-row-level-security-policy-in-supabase-3n5f</link>
      <guid>https://dev.to/ahmed_hassanyaseen_1b7ab/fixing-new-row-violates-row-level-security-policy-in-supabase-3n5f</guid>
      <description>&lt;p&gt;If your app shows &lt;strong&gt;"new row violates row-level security policy for table"&lt;/strong&gt; (Postgres code &lt;code&gt;42501&lt;/code&gt;), the short answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Postgres refused the row because no policy's WITH CHECK allows it for the role making the request. Add an INSERT policy (and UPDATE plus SELECT policies for upserts) that matches how your app writes, for the authenticated role, and make sure the user is actually signed in.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This comes up constantly in apps built with AI builders like Lovable and Bolt, because they often create a read policy and forget the write side. Below: how to tell which case you are in, the policies that fix it, and how to prove the fix with a test, following the Supabase row-level security guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;There is no INSERT policy.&lt;/strong&gt; With row-level security on and no INSERT policy, every insert fails. AI builders often create a SELECT policy and forget the write side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The policy compares the wrong column.&lt;/strong&gt; The check reads (select auth.uid()) = user_id, but the app inserts the row without user_id, or with a different column such as owner_id or created_by.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The request is not signed in.&lt;/strong&gt; For a request without a session, auth.uid() is null, so auth.uid() = user_id is never true. A page that loads before the session is restored writes as anon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An upsert needs more than INSERT.&lt;/strong&gt; An upsert may update an existing row, which needs an UPDATE policy with both USING and WITH CHECK, and a SELECT policy so the row can be found.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The role is missing a grant.&lt;/strong&gt; A missing table grant raises the same code 42501 before any policy runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Find the cause in five minutes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open the browser console, run the action again and copy the full error, including the table name.&lt;/li&gt;
&lt;li&gt;In the Supabase dashboard, open Authentication &amp;gt; Policies and list the policies on that table, per operation.&lt;/li&gt;
&lt;li&gt;Log the session just before the write: if supabase.auth.getSession() returns no session, the request runs as anon.&lt;/li&gt;
&lt;li&gt;Compare the columns the app sends with the columns the policy reads. A missing user_id is the most common mismatch.&lt;/li&gt;
&lt;li&gt;If the app uses upsert, check for UPDATE and SELECT policies as well as INSERT.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Add an INSERT policy for signed-in users that owns the row
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Make sure the table has a user_id column that references auth.users.&lt;/li&gt;
&lt;li&gt;Create the policy for the authenticated role only.&lt;/li&gt;
&lt;li&gt;Send user_id with every insert, or give the column a default of auth.uid().
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can create their own projects"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Optional: fill user_id on the server so the app cannot forget it&lt;/span&gt;
&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cover upserts and updates
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Add a SELECT policy so the existing row can be found.&lt;/li&gt;
&lt;li&gt;Add an UPDATE policy with USING (which rows may change) and WITH CHECK (what they may become).
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can read their own projects"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can update their own projects"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Wait for the session before writing
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Write only after the auth state is known; disable the save button until a user exists.&lt;/li&gt;
&lt;li&gt;Never fix this by switching the write to a secret or service_role key in the browser: that bypasses every policy for everyone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Test the policies in SQL before you ship
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the SQL editor (or a pgTAP test), switch to the role and the user inside a transaction, as the Supabase guide recommends.&lt;/li&gt;
&lt;li&gt;Try the write as the owner, as another user and as anon, and expect 42501 for the last two.&lt;/li&gt;
&lt;li&gt;Keep it as a test: supabase test new projects_rls.test, then run supabase test db before every release. Prove an allowed write by checking the row exists afterwards, not only that no error was raised.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Owner: must succeed&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="k"&gt;local&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="k"&gt;local&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'11111111-1111-1111-1111-111111111111'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'My project'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'11111111-1111-1111-1111-111111111111'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Same user writing a row for someone else: must fail with 42501&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="k"&gt;local&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="k"&gt;local&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'11111111-1111-1111-1111-111111111111'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Not mine'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'22222222-2222-2222-2222-222222222222'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Check that it is really fixed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Signed in as user A, insert a row: it succeeds.&lt;/li&gt;
&lt;li&gt;Signed in as user A, try to insert a row with user B's user_id: it must fail with 42501.&lt;/li&gt;
&lt;li&gt;Signed out, try the same insert: it must fail.&lt;/li&gt;
&lt;li&gt;Run the Supabase security advisor (Database &amp;gt; Advisors) and confirm the table shows no warnings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Keep it from coming back
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Write one policy per operation and name the role on each (to authenticated).&lt;/li&gt;
&lt;li&gt;Keep a short test per table: owner can write, other user cannot, anonymous cannot.&lt;/li&gt;
&lt;li&gt;Index the column your policies filter on (user_id) so policies stay fast as data grows.&lt;/li&gt;
&lt;li&gt;Write (select auth.uid()) rather than auth.uid(): Postgres then runs it once per statement instead of once per row, which matters on large tables.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published in the &lt;a href="https://stacksmesh.com/fix/supabase-new-row-violates-row-level-security-policy" rel="noopener noreferrer"&gt;StacksMesh fix library&lt;/a&gt;, which covers the errors AI-built apps hit most, each checked against the official docs. To check a live app for exposed keys and missing headers, the &lt;a href="https://stacksmesh.com/tools/launch-check" rel="noopener noreferrer"&gt;Launch Check&lt;/a&gt; is free and needs no sign-up. I work at StacksMesh.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase: Row Level Security&lt;/a&gt;, &lt;a href="https://supabase.com/docs/guides/api/api-keys" rel="noopener noreferrer"&gt;Supabase: API keys&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>security</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
