<?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: Tori TIC</title>
    <description>The latest articles on DEV Community by Tori TIC (@toritic).</description>
    <link>https://dev.to/toritic</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%2F4026651%2F9ed75e82-c112-4675-a912-f4bb2827d250.png</url>
      <title>DEV Community: Tori TIC</title>
      <link>https://dev.to/toritic</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/toritic"/>
    <language>en</language>
    <item>
      <title>Supabase: permission denied for table (42501)</title>
      <dc:creator>Tori TIC</dc:creator>
      <pubDate>Sun, 04 Oct 2026 16:30:04 +0000</pubDate>
      <link>https://dev.to/toritic/supabase-permission-denied-for-table-42501-371e</link>
      <guid>https://dev.to/toritic/supabase-permission-denied-for-table-42501-371e</guid>
      <description>&lt;p&gt;This is not row level security. The database role your app uses (&lt;code&gt;anon&lt;/code&gt; or &lt;code&gt;authenticated&lt;/code&gt;) has no privilege on the table at all, so Postgres refuses before any policy is read. A correct policy cannot fix it; a grant can. Supabase is also changing a default that makes this error easier to hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same code, &lt;em&gt;different error&lt;/em&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from the app:  42501 permission denied for table notes
not this one:  42501 new row violates row-level security policy for table "notes"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What is permission denied for table? It is Postgres refusing a statement because the role running it holds no privilege (select, insert, update or delete) on that table. Supabase's API surfaces it as error 42501.&lt;/p&gt;

&lt;p&gt;Both errors carry the code 42501, which is why they get mixed up. The text tells them apart. &lt;a href="https://supabase.com/docs/guides/troubleshooting/database-api-42501-errors" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;: "If you see an error like permission denied for table your_table, the querying role may not have the required privilege for the operation." A row level security violation names a policy. This one names only the table. Proven on a real PostgreSQL: with a correct select policy in place, a role that had its privileges revoked got &lt;code&gt;permission denied for table&lt;/code&gt; on a select and on an insert; the policy never ran. A row that fails the insert policy of a role that does have the privilege got the other message, &lt;code&gt;new row violates row-level security policy&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  New tables &lt;em&gt;stop being granted for you&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Until now a table you created in the &lt;code&gt;public&lt;/code&gt; schema was usable by the API roles straight away. &lt;a href="https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically" rel="noopener noreferrer"&gt;Supabase's changelog&lt;/a&gt; (April 28, 2026): "New tables in the public schema will no longer be exposed to the Data API automatically." The dates it gives: from May 30, 2026 the setting starts to become the default for new projects (a gradual rollout), and on October 30, 2026 it is applied to existing projects. Existing tables are not affected; only tables created after the change reaches your project need the grant. And the advice: "For new tables you want to expose via the Data API, make explicit grants part of your table-creation flow." A table created by a migration that has no grant for the API roles gets exactly this error on the first query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask the database &lt;em&gt;what the role may do&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Run this in the SQL editor, with your table name in place of &lt;code&gt;your_table&lt;/code&gt;. If it returns &lt;code&gt;f&lt;/code&gt;, the role has no select privilege and a grant is the fix. Proven: it returned &lt;code&gt;f&lt;/code&gt; after the privilege was revoked and &lt;code&gt;t&lt;/code&gt; after it was granted back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;has_table_privilege&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'authenticated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'public.your_table'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'select'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see every privilege the API roles hold on the table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;grantee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;privilege_type&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role_table_grants&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your_table'&lt;/span&gt;
&lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;grantee&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'anon'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'authenticated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'service_role'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Grant the privilege, &lt;em&gt;keep row security on&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;A grant decides whether the role may touch the table. Row level security still decides which rows. &lt;a href="https://www.postgresql.org/docs/current/ddl-rowsecurity.html" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt;: "In addition to the SQL-standard privilege system available through GRANT, tables can have row security policies that restrict, on a per-user basis, which rows can be returned by normal queries or inserted, updated, or deleted by data modification commands." So grant, and keep the policies. Proven: after the grant below, the owner reads their row, another signed-in user gets 0 rows and no error, and a row that fails the insert policy is refused with the policy message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;delete&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;your_table&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grant only what the app needs: for a read-only table, &lt;code&gt;select&lt;/code&gt; alone. Never grant to a role to make an error go away on a table that has row level security switched off, because then every row is open to anyone holding your public key. Our &lt;a href="https://ticassociation.com/supabase-rls-audit" rel="noopener noreferrer"&gt;free auditor&lt;/a&gt; lists tables in that state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three look-alikes, &lt;em&gt;each with its own message&lt;/em&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A table another role created
&lt;/h3&gt;

&lt;p&gt;Default grants belong to the role that set them up. &lt;a href="https://www.postgresql.org/docs/current/ddl-priv.html" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt;: "For most kinds of objects, the initial state is that only the owner (or a superuser) can do anything with the object." Proven: a table created by a different role, with a correct select policy, gave &lt;code&gt;permission denied for table&lt;/code&gt; to the signed-in role until it was granted. Run the grant above.&lt;/p&gt;

&lt;h3&gt;
  
  
  A schema other than public
&lt;/h3&gt;

&lt;p&gt;The message changes to &lt;code&gt;permission denied for schema&lt;/code&gt;. PostgreSQL: usage on a schema "Essentially this allows the grantee to "look up" objects within the schema." Proven: with select granted on a table in a custom schema but no usage on the schema, the error named the schema; granting usage fixed it. Use your schema's name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;usage&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A serial column and its sequence
&lt;/h3&gt;

&lt;p&gt;An insert into a table with a &lt;code&gt;serial&lt;/code&gt; id also uses a sequence, and the message names it: &lt;code&gt;permission denied for sequence&lt;/code&gt;. PostgreSQL: "For sequences, allows use of the currval and nextval functions." Proven: with insert granted on the table but no usage on the sequence, the insert failed naming the sequence, and succeeded after this grant (the sequence is named after the table and column):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;usage&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;sequence&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;your_table_id_seq&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Straight answers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is permission denied for table the same as the row level security error?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Both carry the code 42501, but permission denied for table means the role has no privilege on the table, and no policy can change that. The row level security error says new row violates row-level security policy and names a policy. Proven on a real PostgreSQL: a correct select policy did not help a role whose privileges were revoked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the service role need grants too?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The service role skips row level security but not privileges. Proven on a real PostgreSQL: after its privileges on a table were revoked, the service role also got permission denied for table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did my new table work before and fail now?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Supabase's changelog says new tables in the public schema will no longer be exposed to the Data API automatically, from May 30, 2026 as the default for new projects (gradual rollout) and on October 30, 2026 for existing projects, and only for tables created after the change reaches your project. Add the grant to the migration that creates the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  One missing grant is easy. &lt;em&gt;The next one may be a hole&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Our free auditor names every table whose policies never run because a grant is missing, and prints the GRANT for each, alongside the row level security holes, worst first. It runs in your own SQL editor in about a minute and never touches your rows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ticassociation.com/supabase-rls-audit" rel="noopener noreferrer"&gt;Get the free RLS auditor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also seeing &lt;a href="https://ticassociation.com/supabase-42501-row-level-security" rel="noopener noreferrer"&gt;42501, new row violates row-level security policy&lt;/a&gt;, &lt;a href="https://ticassociation.com/supabase-auth-uid-null-rls" rel="noopener noreferrer"&gt;auth.uid() returning null&lt;/a&gt;, or &lt;a href="https://ticassociation.com/supabase-app-works-locally-not-in-production" rel="noopener noreferrer"&gt;an app that works locally and not in production&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;Still denied after the grant, or would rather hand it off? Paste the error into the free diagnosis at &lt;a href="https://rescue.ticassociation.com" rel="noopener noreferrer"&gt;rescue.ticassociation.com&lt;/a&gt;, or send us the project: you pay only when the fix works.&lt;/p&gt;

&lt;p&gt;© 2026 TIC Association &lt;a href="https://ticassociation.com/privacy" rel="noopener noreferrer"&gt;Privacy&lt;/a&gt; · &lt;a href="https://ticassociation.com/terms" rel="noopener noreferrer"&gt;Terms&lt;/a&gt; &lt;a href="mailto:hello@ticassociation.com"&gt;hello@ticassociation.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Originally published with the full SQL and sources at &lt;a href="https://ticassociation.com/supabase-permission-denied-for-table" rel="noopener noreferrer"&gt;https://ticassociation.com/supabase-permission-denied-for-table&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Supabase 42501: new row violates row-level security</title>
      <dc:creator>Tori TIC</dc:creator>
      <pubDate>Thu, 01 Oct 2026 12:30:03 +0000</pubDate>
      <link>https://dev.to/toritic/supabase-42501-new-row-violates-row-level-security-3g66</link>
      <guid>https://dev.to/toritic/supabase-42501-new-row-violates-row-level-security-3g66</guid>
      <description>&lt;p&gt;PostgreSQL refused the write because no policy let this user write this row. In a Supabase app that comes down to one of six causes, and three things tell you which: the table named in the error, the role making the request, and whether you asked for the row back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are looking at
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"42501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"new row violates row-level security policy for table &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;profiles&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What is error 42501? &lt;code&gt;42501&lt;/code&gt; is the PostgreSQL error code &lt;a href="https://www.postgresql.org/docs/current/errcodes-appendix.html" rel="noopener noreferrer"&gt;&lt;code&gt;insufficient_privilege&lt;/code&gt;&lt;/a&gt;. PostgreSQL raises exactly this message, with exactly this code, when a new row fails a row level security check; you can see it in &lt;a href="https://github.com/postgres/postgres/blob/master/src/backend/executor/execMain.c" rel="noopener noreferrer"&gt;its executor's source&lt;/a&gt;. The same code also covers &lt;code&gt;permission denied for table&lt;/code&gt;, which is a missing grant rather than a policy. That case is further down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six causes. &lt;em&gt;Check them in this order&lt;/em&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  There is no insert policy for your role
&lt;/h3&gt;

&lt;p&gt;With row level security on, PostgreSQL refuses anything no policy allows. In &lt;a href="https://www.postgresql.org/docs/current/ddl-rowsecurity.html" rel="noopener noreferrer"&gt;its own words&lt;/a&gt;: "If no policy exists for the table, a default-deny policy is used, meaning that no rows are visible or can be modified." Check the table's policies in your dashboard: is there an INSERT (or ALL) policy for the &lt;code&gt;authenticated&lt;/code&gt; role? If not, add one:&lt;br&gt;
&lt;/p&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;"insert own rows"&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;profiles&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="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;
  
  
  The row you send fails the check
&lt;/h3&gt;

&lt;p&gt;The policy exists, but the new row does not pass its &lt;code&gt;WITH CHECK&lt;/code&gt;. The usual reason is that the app never sends &lt;code&gt;user_id&lt;/code&gt;, or sends a different one. &lt;a href="https://www.postgresql.org/docs/current/sql-createpolicy.html" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt;: "Rows being inserted that do not pass this policy will result in a policy violation error, and the entire INSERT command will be aborted." Send the signed-in user's id, or let the database fill it in so the app cannot get it wrong:&lt;br&gt;
&lt;/p&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;profiles&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;
  
  
  The request is not signed in
&lt;/h3&gt;

&lt;p&gt;Your policy says &lt;code&gt;to authenticated&lt;/code&gt;, but the request reached the database as the anonymous role: the client had no session yet, or a server route built its client without the user's session. &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;: "Once RLS is enabled, no data is accessible through the API when using a publishable key, until you create policies." Check that a session exists right before the insert, and on the server build the client from the request's cookies. Do not widen the policy to &lt;code&gt;anon&lt;/code&gt; to make the error go away.&lt;/p&gt;

&lt;h3&gt;
  
  
  You chained &lt;code&gt;.select()&lt;/code&gt; after &lt;code&gt;.insert()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://supabase.com/docs/reference/javascript/insert" rel="noopener noreferrer"&gt;Supabase's client&lt;/a&gt; returns nothing from an insert by default: "By default, inserted rows are not returned. To return it, chain the call with .select()." Asking for the row back adds a &lt;code&gt;RETURNING&lt;/code&gt; clause, and then &lt;a href="https://www.postgresql.org/docs/current/sql-createpolicy.html" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt; also requires a SELECT policy the new row passes: "If a newly inserted or updated row does not satisfy the relation's SELECT policies, an error will be thrown." That is why the same insert works without &lt;code&gt;.select()&lt;/code&gt; and fails with it. Add a select policy, or drop &lt;code&gt;.select()&lt;/code&gt;:&lt;br&gt;
&lt;/p&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;"select own rows"&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;profiles&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="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;
  
  
  It is an upsert
&lt;/h3&gt;

&lt;p&gt;An upsert is an insert that may turn into an update, so it needs an update policy as well as an insert policy. When the row already exists, PostgreSQL checks it against your update policies and, as its &lt;a href="https://www.postgresql.org/docs/current/sql-createpolicy.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; says, "unlike a standalone UPDATE command, if the existing row does not pass the USING expressions, an error will be thrown." It needs a select policy too: the same page says "the rows proposed for insertion are checked using the relation's SELECT policies", and on a real PostgreSQL an upsert of a new row with no select policy was refused with 42501. Add the update policy:&lt;br&gt;
&lt;/p&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;"update own rows"&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;profiles&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="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="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;
  
  
  It is a file upload
&lt;/h3&gt;

&lt;p&gt;If the table in the error is &lt;code&gt;objects&lt;/code&gt;, the refusal came from Supabase Storage, not your own table. &lt;a href="https://supabase.com/docs/guides/storage/security/access-control" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;: "By default Storage does not allow any uploads to buckets without RLS policies. You selectively allow certain operations by creating RLS policies on the storage.objects table." This lets signed-in users upload into one bucket; overwriting with upsert also needs SELECT and UPDATE policies. The four upload causes, a folder-scoped policy and the upsert policies are on &lt;a href="https://ticassociation.com/supabase-storage-upload-row-level-security" rel="noopener noreferrer"&gt;the Storage upload page&lt;/a&gt;:&lt;br&gt;
&lt;/p&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;"signed-in users can upload"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&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="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your-bucket'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Two fixes that &lt;em&gt;trade the error for a leak&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not switch row level security off.&lt;/strong&gt; The error goes away because nothing is checked any more. Supabase gives its API roles access to tables in the public schema by default, so without RLS anyone holding your public key can read and write the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not move the insert to the secret key in the browser.&lt;/strong&gt; &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;: "A secret key authorizes access through the service_role Postgres role, which has the bypassrls attribute. Never use a secret key in the browser or expose it to customers."&lt;/p&gt;

&lt;h2&gt;
  
  
  Same code, &lt;em&gt;different cause&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;If the message is &lt;code&gt;permission denied for table&lt;/code&gt;, no policy was ever consulted: the role has no grant on the table at all. Supabase's own &lt;a href="https://supabase.com/docs/guides/troubleshooting/database-api-42501-errors" rel="noopener noreferrer"&gt;troubleshooting page&lt;/a&gt; explains that by default "tables in the public schema are granted SELECT, INSERT, UPDATE, and DELETE to the anon and authenticated roles". A table in a custom schema, or one whose grants were revoked, has to be granted again. The &lt;code&gt;auth&lt;/code&gt; and &lt;code&gt;vault&lt;/code&gt; schemas are closed to the API on purpose, so reach them through a security definer function instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;on&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;profiles&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Straight answers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why does the same insert work in the SQL editor?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because the dashboard runs your queries as the &lt;code&gt;postgres&lt;/code&gt; role (Supabase: "Queries you run in the Dashboard execute as postgres"), the role that created, and so owns, your tables. PostgreSQL lets table owners skip row security: "Table owners normally bypass row security as well." Your users are not table owners, so the same insert is checked for them. When the app's request carries no session at all, &lt;code&gt;auth.uid()&lt;/code&gt; is null and every ownership check fails; &lt;a href="https://ticassociation.com/supabase-auth-uid-null-rls" rel="noopener noreferrer"&gt;that case has its own page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does error 42501 mean in Supabase?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;42501 is PostgreSQL's code for insufficient_privilege. With the message "new row violates row-level security policy", a row you tried to write failed a row level security check. With "permission denied for table", the role has no grant on the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it safe to just turn RLS off?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Supabase gives its API roles access to tables in the public schema by default, so a table without row level security is readable and writable by anyone holding your public key. Fix the policy instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  One broken policy is rarely alone. &lt;em&gt;Check every table&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Our free auditor reads your policies and lists the holes worst first, including the quiet ones that never raise an error. It runs in your own SQL editor in about a minute and never touches your rows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ticassociation.com/supabase-rls-audit" rel="noopener noreferrer"&gt;Get the free RLS auditor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also seeing &lt;a href="https://ticassociation.com/supabase-42p17-infinite-recursion" rel="noopener noreferrer"&gt;42P17, infinite recursion detected in policy&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;© 2026 TIC Association &lt;a href="https://ticassociation.com/privacy" rel="noopener noreferrer"&gt;Privacy&lt;/a&gt; · &lt;a href="https://ticassociation.com/terms" rel="noopener noreferrer"&gt;Terms&lt;/a&gt; &lt;a href="mailto:hello@ticassociation.com"&gt;hello@ticassociation.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Originally published with the full SQL and sources at &lt;a href="https://ticassociation.com/supabase-42501-row-level-security" rel="noopener noreferrer"&gt;https://ticassociation.com/supabase-42501-row-level-security&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How We Replaced 50/mo Social Growth Subscriptions with a Local Desktop Tool</title>
      <dc:creator>Tori TIC</dc:creator>
      <pubDate>Sun, 26 Jul 2026 22:29:08 +0000</pubDate>
      <link>https://dev.to/toritic/how-we-replaced-50mo-social-growth-subscriptions-with-a-local-desktop-tool-1836</link>
      <guid>https://dev.to/toritic/how-we-replaced-50mo-social-growth-subscriptions-with-a-local-desktop-tool-1836</guid>
      <description>&lt;p&gt;If you are a solo developer or indie hacker, you know the monthly subscription stack adds up fast.&lt;/p&gt;

&lt;p&gt;Every tool wants $30 to $60 every single month just to run basic repetitive loops like audience targeting, account auditing, and following/unfollowing non-responders.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Cloud SaaS for Personal Workflows
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Monthly Subscription Tax&lt;/strong&gt;: Paying $400 to $600 per year for simple automation loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security &amp;amp; Credential Risk&lt;/strong&gt;: Giving third-party servers your session cookies or password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overkill Complexity&lt;/strong&gt;: Massive web dashboards when all you need is a reliable local script or app.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Local Desktop Alternative
&lt;/h2&gt;

&lt;p&gt;We built &lt;strong&gt;XDB&lt;/strong&gt; (for X/Twitter) and &lt;strong&gt;IDB&lt;/strong&gt; (for Instagram) as standalone, local-first Windows desktop tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Benefits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero Cloud Dependence&lt;/strong&gt;: Everything runs on your own machine. Your credentials never touch a external server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety Governor&lt;/strong&gt;: Built-in human pacing delays prevent account blocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-Time Purchase&lt;/strong&gt;: Pay once ($29), keep it forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can inspect the tools and local documentation here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://xdb.ticassociation.com" rel="noopener noreferrer"&gt;XDB - X Audience Toolkit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://idb.ticassociation.com" rel="noopener noreferrer"&gt;IDB - Instagram Drive-By&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built by the team at TIC Association.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>webdev</category>
      <category>tools</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Next.js App Shows a White Screen After Vercel Deploy: 5 Real Causes</title>
      <dc:creator>Tori TIC</dc:creator>
      <pubDate>Mon, 20 Jul 2026 19:48:58 +0000</pubDate>
      <link>https://dev.to/toritic/nextjs-app-shows-a-white-screen-after-vercel-deploy-5-real-causes-4fh6</link>
      <guid>https://dev.to/toritic/nextjs-app-shows-a-white-screen-after-vercel-deploy-5-real-causes-4fh6</guid>
      <description>&lt;p&gt;A Next.js app that works perfectly on your machine and shows a blank white page the moment Vercel deploys it is one of the most common ways an AI-built app breaks in production. It is also one of the most misleading, because "white screen" is not a cause. It is what five or six different causes all look like from the outside.&lt;/p&gt;

&lt;p&gt;Here is how to actually narrow it down, in the order that catches the most cases fastest.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Open the browser console before anything else
&lt;/h2&gt;

&lt;p&gt;A white screen almost always means the client-side JavaScript threw an error before it could render anything, and that error is sitting in the browser console even when nothing shows up in the Vercel deploy log. This single step splits the problem in half: if there is an error here, you are debugging client-side code. If the console is silent, the failure is happening before the client ever runs, which points at the next two checks instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Check for a missing environment variable
&lt;/h2&gt;

&lt;p&gt;This is the single most common cause. Your &lt;code&gt;.env.local&lt;/code&gt; file exists on your machine and never gets deployed, by design. If your code reads &lt;code&gt;process.env.SOMETHING&lt;/code&gt; and that variable was never added in the Vercel project's Environment Variables settings, the value is &lt;code&gt;undefined&lt;/code&gt; in production, and depending on how it is used, that can fail completely silently, especially if it is passed into a client SDK's constructor with no error handling around it. Check the Vercel deploy's Function Logs, not just the build log. A build can succeed while the actual request still fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Check whether the build genuinely succeeded, not just that it finished
&lt;/h2&gt;

&lt;p&gt;Vercel will sometimes complete a build and still ship something broken if a data-fetching call at build time (&lt;code&gt;generateStaticParams&lt;/code&gt;, a server component's own fetch) silently failed and got swallowed instead of throwing. The page "builds," but the HTML that gets served is empty or wrong. Look at the actual deployed HTML source (view source on the live URL, not the rendered page) and check whether meaningful content is even there before the JavaScript runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Check the output configuration if you changed frameworks or moved directories
&lt;/h2&gt;

&lt;p&gt;If the project was migrated, restructured, or built with an AI tool that changed the framework preset partway through, Vercel can end up looking in the wrong output directory, or running a build command that no longer matches the actual project structure. This usually shows up as a successful-looking build with nothing meaningful in the output, which again reads as a plain white screen once deployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Node version mismatches
&lt;/h2&gt;

&lt;p&gt;Code that relies on a newer JavaScript or Node feature than the one Vercel's build environment is using can fail in ways that do not always throw a clean, readable error, particularly inside a dependency rather than your own code. Pin the Node version explicitly in your project settings rather than leaving it on whatever the platform default happens to be, and confirm it matches what you built and tested locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern underneath all five
&lt;/h2&gt;

&lt;p&gt;Every one of these fails in a way that looks identical from the outside (a blank page) while being caused by something that only shows up if you look in a specific, different place: the browser console, the environment variables panel, the raw HTML source, the output directory, or the build environment's Node version. Checking the symptom again does not help. Checking each of those five places, in order, usually does.&lt;/p&gt;

&lt;p&gt;If you have already gone through this and the app still will not come back, we do free diagnosis on exactly this class of problem, then a fixed-price repair only if the fix is clear: &lt;a href="https://rescue.ticassociation.com" rel="noopener noreferrer"&gt;https://rescue.ticassociation.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>vercel</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to Check Supabase Row Level Security Holes (Free SQL Audit)</title>
      <dc:creator>Tori TIC</dc:creator>
      <pubDate>Mon, 20 Jul 2026 19:45:45 +0000</pubDate>
      <link>https://dev.to/toritic/how-to-check-supabase-row-level-security-holes-free-sql-audit-2b7k</link>
      <guid>https://dev.to/toritic/how-to-check-supabase-row-level-security-holes-free-sql-audit-2b7k</guid>
      <description>&lt;p&gt;Row level security in Supabase fails quietly. There is no crash, no red error banner, nothing in your logs that says "this table is wide open." The app keeps working in the demo. The first sign anything is wrong is usually someone else's data showing up where it shouldn't, and by then it already happened.&lt;/p&gt;

&lt;p&gt;Here is how to check for the five holes that cause almost all of it, using nothing but a read-only SQL query against your own database's catalog.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five holes, in order of how often they actually happen
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. RLS switched off entirely.&lt;/strong&gt; The single most common leak. It usually happens to a table created after row security was already turned on for the rest of the schema, and the new table never got the same treatment. Anyone with your anon key can read and write it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. RLS is on, but zero policies exist.&lt;/strong&gt; This is the most common cause of the &lt;code&gt;42501&lt;/code&gt; error ("new row violates row-level security policy"), and it goes both ways: depending on the default deny/allow behavior for the operation, the table either blocks everything (your own app breaks) or exposes everything (your data leaks), and it is easy to not notice which one happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A write policy with no &lt;code&gt;WITH CHECK&lt;/code&gt;.&lt;/strong&gt; A policy can correctly gate who is allowed to write, using &lt;code&gt;USING&lt;/code&gt;, while leaving the actual row values unchecked. That lets an authenticated user insert or update a row that claims to belong to someone else, because nothing verified what they wrote, only that they were allowed to write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A policy that resolves to &lt;code&gt;true&lt;/code&gt; for everyone.&lt;/strong&gt; This one is sneaky because it looks like security. There is a policy. It has a name. It shows up in your dashboard as "protected." But the condition inside it evaluates to true for every role, so it protects nothing while giving every appearance of doing so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The &lt;code&gt;anon&lt;/code&gt; role holds a direct grant.&lt;/strong&gt; Someone ran a &lt;code&gt;GRANT&lt;/code&gt; statement straight against the anonymous role at some point, usually while debugging, and it bypasses your row-level policies entirely regardless of how carefully those policies are written.&lt;/p&gt;

&lt;h2&gt;
  
  
  The query
&lt;/h2&gt;

&lt;p&gt;You can check for most of these directly from Supabase's own SQL editor, because the answers live in Postgres's own catalog tables (&lt;code&gt;pg_policies&lt;/code&gt;, &lt;code&gt;pg_class&lt;/code&gt;, &lt;code&gt;information_schema&lt;/code&gt;), not in anything Supabase adds. A useful starting query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt;
  &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nspname&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relrowsecurity&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rls_enabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;polname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;policy_count&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;pg_namespace&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relnamespace&lt;/span&gt;
&lt;span class="k"&gt;left&lt;/span&gt; &lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;pg_policies&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tablename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nspname&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nspname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relkind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'r'&lt;/span&gt;
&lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nspname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relrowsecurity&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;rls_enabled&lt;/span&gt; &lt;span class="k"&gt;asc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy_count&lt;/span&gt; &lt;span class="k"&gt;asc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any row with &lt;code&gt;rls_enabled = false&lt;/code&gt; is hole #1. Any row with &lt;code&gt;rls_enabled = true&lt;/code&gt; and &lt;code&gt;policy_count = 0&lt;/code&gt; is hole #2. Holes #3 through #5 need a look at the actual policy definitions (&lt;code&gt;pg_policies.qual&lt;/code&gt; and &lt;code&gt;.with_check&lt;/code&gt;) and at &lt;code&gt;information_schema.role_table_grants&lt;/code&gt; for direct grants to &lt;code&gt;anon&lt;/code&gt;, which is more than fits cleanly in one query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worth checking even if nothing looks broken
&lt;/h2&gt;

&lt;p&gt;The whole point of these five is that none of them produce an error under normal use. Your own testing, as the table's owner or as an authenticated user hitting your own rows, will not surface a policy that is technically present but too permissive, or a grant that quietly overrides your policies. The only way to know is to check the catalog directly, which is exactly what this kind of query is for.&lt;/p&gt;

&lt;p&gt;We built a free, one-file version of this check that runs the fuller version of the above (read-only, checks all five holes, plain-language output with the fix for each finding) and never asks for your project URL or your keys, because it runs inside your own SQL editor. It is a SQL file, so you can read exactly what it does before you run it: &lt;a href="https://ticassociation.com/supabase-rls-audit" rel="noopener noreferrer"&gt;https://ticassociation.com/supabase-rls-audit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nothing here is a substitute for reading your own policies. It is a fast way to find out where to start looking.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>We made our security auditor buyable by AI agents (x402, one serverless function)</title>
      <dc:creator>Tori TIC</dc:creator>
      <pubDate>Sat, 18 Jul 2026 00:07:34 +0000</pubDate>
      <link>https://dev.to/toritic/we-made-our-security-auditor-buyable-by-ai-agents-x402-one-serverless-function-44j</link>
      <guid>https://dev.to/toritic/we-made-our-security-auditor-buyable-by-ai-agents-x402-one-serverless-function-44j</guid>
      <description>&lt;p&gt;Last night we made our Supabase security auditor buyable by AI agents. One HTTP request, a USDC payment attached to a header, and the product comes back in the response body. No checkout page, no account, no human.&lt;/p&gt;

&lt;p&gt;Here is why we did it, how the whole thing is about 80 lines of code, and an honest accounting of what it will and will not do for us.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-second history of HTTP 402
&lt;/h2&gt;

&lt;p&gt;The HTTP spec reserved status code &lt;code&gt;402 Payment Required&lt;/code&gt; in 1997 and it sat unused for nearly three decades. In 2025 Coinbase published x402, an open protocol that finally gives it a job: a server answers a request with 402 plus machine-readable payment requirements, the client attaches a signed stablecoin payment to a header, retries, and gets the resource. Settlement happens on-chain (USDC on Base) in one round trip. Visa's Intelligent Commerce integrated it this spring. It is not a concept; it is running infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we shipped
&lt;/h2&gt;

&lt;p&gt;Our RLS Security Pack is a zip: a read-only SQL auditor that finds the five common row-level-security holes in AI-built Supabase apps, fix recipes for every finding class, and a Claude Code skill. Humans buy it on Gumroad. Now an agent can buy it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ask for the product&lt;/span&gt;
curl &lt;span class="nt"&gt;-i&lt;/span&gt; https://ticassociation.com/api/agent/rls-pack

&lt;span class="c"&gt;# the server answers 402 with the exact terms:&lt;/span&gt;
&lt;span class="c"&gt;# {"x402Version":1,"accepts":[{"scheme":"exact","network":"base",&lt;/span&gt;
&lt;span class="c"&gt;#   "asset":"...USDC...","payTo":"0x...","maxAmountRequired":"...", ...}]}&lt;/span&gt;

&lt;span class="c"&gt;# an x402-capable client attaches the signed payment and retries:&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-PAYMENT: &amp;lt;signed&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://ticassociation.com/api/agent/rls-pack &lt;span class="nt"&gt;-o&lt;/span&gt; pack.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server side is one serverless function: return 402 with the requirements when there is no payment header, verify and settle through the public facilitator when there is one, then stream the zip. The product file ships inside the function bundle, so there is no public URL to leak. The whole thing took an evening, and most of that was reading the spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a tiny company bothered
&lt;/h2&gt;

&lt;p&gt;Three honest reasons, in decreasing order of honesty:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Our brand line has always been "a collective of agents, human or AI." Selling to both makes the sentence literal, and we could not resist that.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Coding agents already do real work. An agent that finds broken row level security mid-task should be able to acquire the fix kit without stopping to page a human through a checkout form. The demand is small today. It was also small for HTTPS in 1995.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Answer engines reward being early and specific. When someone asks an AI "can software be sold to AI agents", we would like the answer to have a working example to point at.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The honest accounting
&lt;/h2&gt;

&lt;p&gt;Will agents autonomously buy our security pack this month? Almost certainly not in any volume. Agent-initiated commerce is real but young, and discovery is the hard part: your endpoint has to be findable by agents at all. We list ours in our llms.txt under a "For agents" section, which is the current best practice and still a bet on the future rather than a traffic source today.&lt;/p&gt;

&lt;p&gt;What it cost us: one evening, zero dollars of infrastructure (a serverless function and the public facilitator), and the risk of looking silly if the category stalls. What it buys us: a working claim nobody in our niche has, and a store that is ready if the buyers arrive before the skeptics expect.&lt;/p&gt;

&lt;p&gt;If you want to see it: the human-readable version is at &lt;a href="https://ticassociation.com/agent-store" rel="noopener noreferrer"&gt;ticassociation.com/agent-store&lt;/a&gt;, the free edition of the auditor (no payment, agent or human) is at &lt;a href="https://ticassociation.com/supabase-rls-audit" rel="noopener noreferrer"&gt;ticassociation.com/supabase-rls-audit&lt;/a&gt;, and if your AI-built app is broken in ways an auditor cannot fix, that is our day job: &lt;a href="https://rescue.ticassociation.com" rel="noopener noreferrer"&gt;rescue.ticassociation.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>payments</category>
      <category>webdev</category>
      <category>supabase</category>
    </item>
    <item>
      <title>Where to get a 3Blue1Brown style explainer video made, and how to judge one</title>
      <dc:creator>Tori TIC</dc:creator>
      <pubDate>Fri, 17 Jul 2026 23:42:14 +0000</pubDate>
      <link>https://dev.to/toritic/where-to-get-a-3blue1brown-style-explainer-video-made-and-how-to-judge-one-585b</link>
      <guid>https://dev.to/toritic/where-to-get-a-3blue1brown-style-explainer-video-made-and-how-to-judge-one-585b</guid>
      <description>&lt;p&gt;If you want an explainer video in the calm, diagram-driven 3Blue1Brown style, you have three real options: learn Manim yourself (free, steep curve), hire a motion studio (usually four figures), or use a small specialist service built on a Manim pipeline. This guide covers all three honestly, plus how to judge the result before you pay anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the style actually is
&lt;/h2&gt;

&lt;p&gt;3Blue1Brown's videos work because the animation IS the explanation. There is no stock footage, no talking head, no kinetic-text filler. A diagram builds on screen exactly as the idea builds in your head, one element at a time, while a calm voice narrates. The viewer never reads one thing while hearing another.&lt;/p&gt;

&lt;p&gt;That style comes from Manim, the animation engine Grant Sanderson wrote and open-sourced. Every derivative of the look traces back to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: make it yourself with Manim
&lt;/h2&gt;

&lt;p&gt;Manim is free and the community edition is well documented. If you are comfortable with Python, the honest cost is time: expect a few days to become productive and a few hours per finished minute of animation once you are. The parts nobody warns you about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timing animation to narration is the real work. The scenes are easy; the pacing is not.&lt;/li&gt;
&lt;li&gt;Audio mastering matters more than you think. Voice anchored first, music ducked underneath, or the whole thing feels amateur.&lt;/li&gt;
&lt;li&gt;Rendering is fast on any modern machine. This is not a GPU-hungry workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have the time and the Python, this is the best option. You will own the skill forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: hire a studio or freelancer
&lt;/h2&gt;

&lt;p&gt;Motion design studios quote explainer videos at roughly $1,000 to $10,000 per finished minute depending on market and polish. Freelancer marketplaces are cheaper but most listings there do whiteboard or kinetic-text styles, not the diagram-first Manim look. If you go this route, ask specifically what engine they use and ask for a sample in the target style before committing. A generic motion reel does not predict a good diagram-first explainer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: a specialist service
&lt;/h2&gt;

&lt;p&gt;Full disclosure: this is what we do, so weigh the bias accordingly. We run a fully local Manim pipeline (animation, narration, and music bed all licence-clean, produced on machines we own) and sell finished explainers you own outright, no watermark and no subscription. Because the pipeline is automated where it should be and hand-tuned where it matters, the price sits far below studio quotes.&lt;/p&gt;

&lt;p&gt;The honest pitch is the sample: we make a free 10 second sample of your actual idea so you can judge the style on your own material before paying anything. That offer, and a 36 second reel of what the engine produces, is at &lt;a href="https://ticassociation.com/explainer-videos" rel="noopener noreferrer"&gt;ticassociation.com/explainer-videos&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to judge any explainer, from anyone
&lt;/h2&gt;

&lt;p&gt;Use these five checks whether you build or buy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the diagram build in sync with the narration, or is a finished graphic just sitting there being described?&lt;/li&gt;
&lt;li&gt;Is there any stock footage or filler b-roll? In this style, that is a failure.&lt;/li&gt;
&lt;li&gt;Is the pacing calm? Rapid cuts mean the maker did not trust the explanation.&lt;/li&gt;
&lt;li&gt;Voice first in the mix, music under it, silence used on purpose.&lt;/li&gt;
&lt;li&gt;Could a viewer redraw the core diagram from memory afterward? That is the whole point of the style.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The honest limits
&lt;/h2&gt;

&lt;p&gt;This style is wrong for some jobs. Product demos that need real UI footage, emotional brand films, and anything where a human face builds the trust all want different treatments. Diagram-first explainers shine when the product or idea is abstract: infrastructure, security, finance, algorithms, anything where the buyer says "I do not quite get how this works."&lt;/p&gt;

&lt;p&gt;If that is your situation, learn Manim if you have the hours, or grab the free sample if you do not.&lt;/p&gt;

</description>
      <category>animation</category>
      <category>video</category>
      <category>manim</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Supabase Sign in with Apple keeps throwing invalid_client: the checklist that actually finds it</title>
      <dc:creator>Tori TIC</dc:creator>
      <pubDate>Fri, 17 Jul 2026 21:30:56 +0000</pubDate>
      <link>https://dev.to/toritic/supabase-sign-in-with-apple-keeps-throwing-invalidclient-the-checklist-that-actually-finds-it-5c7i</link>
      <guid>https://dev.to/toritic/supabase-sign-in-with-apple-keeps-throwing-invalidclient-the-checklist-that-actually-finds-it-5c7i</guid>
      <description>&lt;p&gt;You rotate a key for Sign in with Apple, and suddenly every login throws &lt;code&gt;invalid_client&lt;/code&gt;. It was supposed to be a five minute change. Three hours later you are still staring at the same error.&lt;/p&gt;

&lt;p&gt;This one bites hard because &lt;code&gt;invalid_client&lt;/code&gt; is Apple's answer to about six different mistakes, and the error text never tells you which one you made. Here is the checklist that finds it, in the order of most likely to least.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually being checked
&lt;/h2&gt;

&lt;p&gt;When Supabase (or any backend) talks to Apple, it presents a client secret that is not a static string: it is a JWT you generate, signed with your &lt;code&gt;.p8&lt;/code&gt; key. Apple validates five things about it, and a mismatch in any one of them returns &lt;code&gt;invalid_client&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the JWT is signed by the key whose Key ID is in the JWT header&lt;/li&gt;
&lt;li&gt;that key belongs to your Apple team&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;sub&lt;/code&gt; claim matches your Services ID (not your App ID)&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;iss&lt;/code&gt; claim is your Team ID&lt;/li&gt;
&lt;li&gt;the JWT is not expired&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Did you regenerate the client-secret JWT after rotating the .p8?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the classic trap. Rotating the key in the Apple Developer portal does nothing to the JWT you generated from the old key. That JWT still carries the old Key ID in its header, so Apple rejects it. After any key rotation you must generate a fresh client-secret JWT from the new &lt;code&gt;.p8&lt;/code&gt; and paste that into your provider config. The old secret does not "refresh".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Is the &lt;code&gt;sub&lt;/code&gt; claim your Services ID?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;sub&lt;/code&gt; in the client-secret JWT must be the &lt;strong&gt;Services ID&lt;/strong&gt; (the identifier you created for web auth, usually something like &lt;code&gt;com.yourapp.web&lt;/code&gt;), not the App ID and not the bundle id. If you copied the wrong identifier when generating the secret, Apple sees a client it does not know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Does the Key ID in the JWT header match the uploaded key?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Decode your client secret at any JWT debugger (it is not sensitive to decode, it is public claims plus a signature). The header's &lt;code&gt;kid&lt;/code&gt; must be exactly the Key ID shown next to your Sign in with Apple key in the developer portal. A stale &lt;code&gt;kid&lt;/code&gt; means you signed with a key Apple no longer associates with that configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Is the JWT expired?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Apple caps client-secret JWTs at 6 months (&lt;code&gt;exp&lt;/code&gt; at most 15777000 seconds after &lt;code&gt;iat&lt;/code&gt;). If you generated the secret long ago and it worked until today, this is your answer: it aged out, which people then misdiagnose as a rotation problem while rotating everything except the one thing that expired.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Is the ES256 algorithm actually used?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The secret must be signed with ES256 (the &lt;code&gt;.p8&lt;/code&gt; is an EC key). A generator defaulting to HS256 or RS256 produces a structurally valid JWT that Apple rejects. The decoded header should read &lt;code&gt;"alg": "ES256"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Are the domain and return URL registered on the Services ID?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the identifiers all match but auth still fails at the redirect step, check that the Services ID has your auth domain (for Supabase: &lt;code&gt;&amp;lt;project-ref&amp;gt;.supabase.co&lt;/code&gt;) and the exact callback URL registered under its Sign in with Apple configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five minute reset, when you would rather stop debugging
&lt;/h2&gt;

&lt;p&gt;When the state is too tangled, a clean rebuild is faster than archaeology:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the Apple portal: create a fresh key with Sign in with Apple enabled, download the new &lt;code&gt;.p8&lt;/code&gt;, note the Key ID.&lt;/li&gt;
&lt;li&gt;Confirm the Services ID, Team ID, and domain/return URL config.&lt;/li&gt;
&lt;li&gt;Generate a brand new client-secret JWT (ES256, &lt;code&gt;iss&lt;/code&gt; = Team ID, &lt;code&gt;sub&lt;/code&gt; = Services ID, &lt;code&gt;kid&lt;/code&gt; = the new Key ID, &lt;code&gt;exp&lt;/code&gt; under 6 months).&lt;/li&gt;
&lt;li&gt;Paste it into your provider config (in Supabase: Authentication, then Providers, then Apple) and save.&lt;/li&gt;
&lt;li&gt;Test in a private window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every value is freshly derived, so any stale-state mismatch is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you would rather hand it over
&lt;/h2&gt;

&lt;p&gt;We fix broken AI-built and Supabase apps for a living, auth is one of the three things we repair most, and the diagnosis is free: &lt;a href="https://rescue.ticassociation.com" rel="noopener noreferrer"&gt;rescue.ticassociation.com&lt;/a&gt;. You pay only after the fix is verified working.&lt;/p&gt;

&lt;p&gt;And if your app is Supabase and you have never audited its Row Level Security, our free read-only auditor finds the common holes in a minute: &lt;a href="https://ticassociation.com/supabase-rls-audit" rel="noopener noreferrer"&gt;ticassociation.com/supabase-rls-audit&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>authentication</category>
      <category>apple</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The 5 RLS holes that quietly leak data in AI-built Supabase apps</title>
      <dc:creator>Tori TIC</dc:creator>
      <pubDate>Fri, 17 Jul 2026 20:09:16 +0000</pubDate>
      <link>https://dev.to/toritic/the-5-rls-holes-that-quietly-leak-data-in-ai-built-supabase-apps-242c</link>
      <guid>https://dev.to/toritic/the-5-rls-holes-that-quietly-leak-data-in-ai-built-supabase-apps-242c</guid>
      <description>&lt;p&gt;Your app works. Sign-ups land, dashboards load, and Stripe pays out. Then one day a stranger reads another user's data, and you find out the door was never locked.&lt;/p&gt;

&lt;p&gt;That is what broken Row Level Security looks like in a Supabase app. It is not a crash. There is no error in the console. The app behaves perfectly for you while quietly serving other people's rows to anyone who asks the API directly. If your app was built with Lovable, Bolt, Cursor, or Replit, this is the single most common security hole it shipped with.&lt;/p&gt;

&lt;p&gt;Here are the five ways it happens, and a free read-only auditor that finds all five in about a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. RLS is switched off on a table
&lt;/h2&gt;

&lt;p&gt;The number one leak. Someone (often the AI assistant) created a table after row security was set up, and nobody remembered to lock it. With RLS off and the standard grants in place, that table is world-readable and writable through your public API.&lt;/p&gt;

&lt;p&gt;Check it yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;relname&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;pg_namespace&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relnamespace&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nspname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relkind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'r'&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relrowsecurity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything this returns is unprotected right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. RLS is on, but there are zero policies
&lt;/h2&gt;

&lt;p&gt;Row security without policies means Postgres denies everything for regular roles. This is the classic cause of the &lt;code&gt;42501 permission denied&lt;/code&gt; error that appears after a deploy. It fails closed, which is safer than open, but it also means the feature you shipped does not work, and the usual "fix" people paste from a forum is to switch RLS off. See hole number 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A write policy with no WITH CHECK
&lt;/h2&gt;

&lt;p&gt;A policy's &lt;code&gt;USING&lt;/code&gt; clause controls which rows you can see. The &lt;code&gt;WITH CHECK&lt;/code&gt; clause controls what you are allowed to write. A policy that sets only &lt;code&gt;USING&lt;/code&gt; on insert or update lets an authenticated user write rows as somebody else, for example inserting messages with another user's id. Reads look secure, writes are wide open.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A policy that is true for everyone
&lt;/h2&gt;

&lt;p&gt;A policy like &lt;code&gt;using (true)&lt;/code&gt; on a sensitive table passes every review that only checks "RLS is on and policies exist". It protects nothing. These usually appear as leftover debugging or as AI-generated scaffolding that was never tightened.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The anon role holds a direct write grant
&lt;/h2&gt;

&lt;p&gt;Even with sensible policies, a stray &lt;code&gt;grant insert on ... to anon&lt;/code&gt; gives the anonymous API role a door of its own. This one hides because nobody looks at grants after setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-minute audit
&lt;/h2&gt;

&lt;p&gt;I bundled all five checks into a single read-only SQL file. You paste it into your Supabase SQL editor, run it, and get a prioritized findings table: severity, table, what the hole is in plain language, and the copy-paste fix.&lt;/p&gt;

&lt;p&gt;It reads system catalogs only. It never touches your rows, changes nothing, and you can read every line before you run it. No install, no account access, no keys shared.&lt;/p&gt;

&lt;p&gt;Download it free here: &lt;a href="https://ticassociation.com/supabase-rls-audit" rel="noopener noreferrer"&gt;ticassociation.com/supabase-rls-audit&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;The auditor catches the common, repeated failure patterns fast. It is not a penetration test, and an app holding sensitive data still deserves a real security review before scale. Treat it as your pre-launch preflight: five minutes that rule out the five most likely leaks.&lt;/p&gt;

&lt;p&gt;If it flags something you cannot fix, that is literally what we do all day: we repair broken AI-built apps, RLS most of all. Free diagnosis at &lt;a href="https://rescue.ticassociation.com" rel="noopener noreferrer"&gt;rescue.ticassociation.com&lt;/a&gt;, and you pay only after the fix is verified working.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>security</category>
      <category>postgres</category>
      <category>webdev</category>
    </item>
    <item>
      <title>AI Builders Make Apps LOOK Finished. Here Is How To Tell If Yours Actually Works.</title>
      <dc:creator>Tori TIC</dc:creator>
      <pubDate>Thu, 16 Jul 2026 23:36:47 +0000</pubDate>
      <link>https://dev.to/toritic/ai-builders-make-apps-look-finished-here-is-how-to-tell-if-yours-actually-works-158g</link>
      <guid>https://dev.to/toritic/ai-builders-make-apps-look-finished-here-is-how-to-tell-if-yours-actually-works-158g</guid>
      <description>&lt;p&gt;You built something with an AI tool. It looked finished. You clicked around for a while, felt good about it, and sent the link to someone who actually needed it. Then they messaged back confused, because the thing they were looking at was not real.&lt;/p&gt;

&lt;p&gt;This happens constantly and it is not really a coding mistake. It is a looking mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looks connected is not the same as is connected
&lt;/h2&gt;

&lt;p&gt;AI builders are very good at one specific trick: making an empty screen look full. Sample names, sample dates, a calendar that already has bookings on it, a dashboard with numbers already in the boxes. None of that proves anything is wired up. It proves the builder did not want you to stare at a blank page.&lt;/p&gt;

&lt;p&gt;You cannot tell the difference by looking at it. That is the whole problem. A screen full of realistic-looking data and a screen full of real data render exactly the same to your eyes. The only way to know which one you have is to test it, not admire it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three checks that take about a minute, no code required
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The write test.&lt;/strong&gt; Add one real thing through your own app. A booking, a record, whatever the app is for. Then hard refresh the page. If what you added is gone, it never reached a database. It was living in the browser tab and nowhere else. Sample rows that were already there will survive a refresh too, which is why this check is not enough on its own, pair it with the next one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Unplug it on purpose.&lt;/strong&gt; Find the setting for your database connection or your API key and break it. Rename a table, revoke a key, whatever is easiest. Then load the page again. A real connection fails loudly and immediately, usually with an error or a blank spot. If the page still looks completely fine with the backend broken, nothing was ever actually talking to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Watch the network tab for ten seconds.&lt;/strong&gt; Open your browser's developer tools, click the Network tab, then load the page. Real data means a request goes out to fetch it, you will see it appear in the list. No request means those rows were baked into the page itself, not pulled from anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Empty is honest
&lt;/h2&gt;

&lt;p&gt;Here is the instinct worth building: a brand new app, one you have not entered anything into yet, should look bare. An empty calendar. Zero bookings. A dashboard with nothing on it. If a fresh app looks full before you have put anything into it, ask where that fullness is coming from. It did not come from you.&lt;/p&gt;

&lt;p&gt;This is also why the person who finds these bugs first is almost never the one who built the app. You have been staring at the demo, admiring what it can do. The first real user is not admiring anything, they are trying to actually use it, and they touch the one path that was never really connected within minutes. That is not bad luck. That is the difference between testing as the builder and testing as the customer, and only one of those two ever finds the real bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you already shipped something and are not sure
&lt;/h2&gt;

&lt;p&gt;Run the three checks above on it today, before anyone else does. It takes less time to run them than it took to read this.&lt;/p&gt;

&lt;p&gt;If you run them and something is wrong and you are not sure how to fix it, that is a normal place to end up, not a sign you did something wrong. We do free diagnoses for exactly this: send us the app, we look at what is actually happening under the surface, and tell you straight whether it is a five minute fix or something bigger. No pitch, no obligation. rescue.ticassociation.com&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>webdev</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Supabase login works locally but the session dies after you deploy to Vercel. Here is why.</title>
      <dc:creator>Tori TIC</dc:creator>
      <pubDate>Thu, 16 Jul 2026 09:57:07 +0000</pubDate>
      <link>https://dev.to/toritic/supabase-login-works-locally-but-the-session-dies-after-you-deploy-to-vercel-here-is-why-1p2d</link>
      <guid>https://dev.to/toritic/supabase-login-works-locally-but-the-session-dies-after-you-deploy-to-vercel-here-is-why-1p2d</guid>
      <description>&lt;p&gt;You build an app in Lovable, Bolt, v0, or Cursor. Login works perfectly on localhost. You deploy to Vercel, log in on the live site, and you are immediately bounced back to the login page. Or the page loads but every query returns nothing, as if you are a stranger to your own app.&lt;/p&gt;

&lt;p&gt;This is the single most common way an AI-built Supabase app breaks in production. It is almost never the login code itself. It is the session, and specifically where the session lives.&lt;/p&gt;

&lt;p&gt;Here is what is actually happening, and the order to check it in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The session lives in cookies, and cookies are the thing that breaks
&lt;/h2&gt;

&lt;p&gt;Supabase auth gives you a session and stores it in cookies. Every part of your app that talks to Supabase has to read those cookies correctly. On localhost, a lot of sloppiness is survivable: one origin, no HTTPS, no edge, no separate server context. In production, all of that becomes strict.&lt;/p&gt;

&lt;p&gt;So the question is never "is my login broken." The question is "which layer lost the cookie."&lt;/p&gt;

&lt;h2&gt;
  
  
  1. You are using the browser client on the server
&lt;/h2&gt;

&lt;p&gt;This is the number one cause in generated code.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@supabase/supabase-js&lt;/code&gt; createClient is a browser client. It keeps the session in browser storage. If you call it from a server component, a route handler, or middleware, there is no browser, so there is no session. Your query runs anonymous, RLS says no, and you get empty results or a redirect.&lt;/p&gt;

&lt;p&gt;In an App Router project you need two different clients from &lt;code&gt;@supabase/ssr&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a browser client for client components&lt;/li&gt;
&lt;li&gt;a server client, created per request, that reads and writes cookies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your project has a single &lt;code&gt;supabase.ts&lt;/code&gt; that everything imports, that is your bug. Generated code does this constantly because it works locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Middleware is not refreshing the session
&lt;/h2&gt;

&lt;p&gt;Supabase access tokens expire. &lt;code&gt;@supabase/ssr&lt;/code&gt; expects middleware to refresh the token and write the updated cookies back onto the response.&lt;/p&gt;

&lt;p&gt;If you have no middleware, or middleware that reads cookies but never sets them back, the session silently dies the moment the token expires. Classic symptom: login works, then a refresh five minutes later logs you out.&lt;/p&gt;

&lt;p&gt;Your middleware must both refresh the session and return the response carrying the updated cookies. If your middleware creates a response object and then returns a different one, you dropped the cookies.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. cookies() is not awaited
&lt;/h2&gt;

&lt;p&gt;On recent Next.js versions &lt;code&gt;cookies()&lt;/code&gt; is async. If your generated code calls it synchronously, you can get an app that builds fine and behaves strangely at runtime, because the cookie store is not what you think it is.&lt;/p&gt;

&lt;p&gt;Search your project for &lt;code&gt;cookies()&lt;/code&gt; and make sure every call is awaited, and that the server client is created inside the request scope rather than at module top level. A Supabase server client created at module scope is shared across requests, which is both a bug and a security problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Your production URL is not in Supabase's redirect allowlist
&lt;/h2&gt;

&lt;p&gt;Open your Supabase dashboard, Authentication, then URL Configuration. Site URL and Redirect URLs must include your real deployed domain, including the exact protocol and any preview domains you actually use.&lt;/p&gt;

&lt;p&gt;If the allowlist only has localhost, the auth callback on production is rejected. Supabase is doing the right thing. It just looks like your login is broken.&lt;/p&gt;

&lt;p&gt;Add the production domain. If you use Vercel preview deployments, be aware their URLs change per deploy, which is a common source of "it works on prod but not on preview."&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Cookie flags differ between localhost and HTTPS
&lt;/h2&gt;

&lt;p&gt;On localhost you are on http and one origin. In production you are on https, possibly with a custom domain. Cookies marked Secure will not be set over plain http, and SameSite rules bite the moment an OAuth provider redirects you back from another origin.&lt;/p&gt;

&lt;p&gt;If you hand-rolled cookie options in generated code, this is worth a look. If you let &lt;code&gt;@supabase/ssr&lt;/code&gt; manage them, it is usually correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check this in the right order
&lt;/h2&gt;

&lt;p&gt;Do not start changing code. Start by finding out who Supabase thinks you are.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On the deployed site, log in, then hit a server route that logs the result of &lt;code&gt;getUser()&lt;/code&gt;. If it returns null, your server has no session and the cause is in items 1 to 3.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;getUser()&lt;/code&gt; returns your user but queries are still empty, your session is fine and your problem is RLS, not auth. Different bug, different article.&lt;/li&gt;
&lt;li&gt;Check the Supabase dashboard redirect allowlist against the exact domain in your address bar.&lt;/li&gt;
&lt;li&gt;Log in and wait past token expiry, then refresh. If that is when it breaks, your middleware is not refreshing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sequence tells you which layer lost the cookie in about five minutes, without touching a line of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI builders produce this specific bug
&lt;/h2&gt;

&lt;p&gt;Generated code optimizes for the preview working. The preview is a single origin, no HTTPS boundary, no expiry, no separate server context. Every shortcut that fails in production is invisible there. The tool did not lie to you. It just never had to cross the boundary that production forces.&lt;/p&gt;

&lt;p&gt;This is why these apps break on deploy specifically, and why the fix is almost always at a boundary rather than in the feature logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to stop guessing
&lt;/h2&gt;

&lt;p&gt;If you have changed the same three files four times and the behavior keeps moving, stop. Auth, RLS, cookies, and deployment callbacks interact, and guessing changes more than one variable at a time. Preserve the exact error, find the failing boundary, then make the smallest change that crosses it.&lt;/p&gt;

&lt;p&gt;If you want a second set of eyes, we run a free diagnosis for broken AI-built apps: describe what is broken in plain words, or paste the error if you have one, and you get the likely cause and a fix path back. No card, no repo access to start. &lt;a href="https://rescue.ticassociation.com" rel="noopener noreferrer"&gt;https://rescue.ticassociation.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A TIC Association creation.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>vercel</category>
    </item>
  </channel>
</rss>
