<?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: TIC</title>
    <description>The latest articles on DEV Community by 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: 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>Fix StorageApiError: new row violates RLS on upload</title>
      <dc:creator>TIC</dc:creator>
      <pubDate>Wed, 28 Oct 2026 13:30:03 +0000</pubDate>
      <link>https://dev.to/toritic/fix-storageapierror-new-row-violates-rls-on-upload-e0e</link>
      <guid>https://dev.to/toritic/fix-storageapierror-new-row-violates-rls-on-upload-e0e</guid>
      <description>&lt;p&gt;Your upload was refused by a policy on the &lt;code&gt;storage.objects&lt;/code&gt; table, not by the bucket, and making the bucket public does not change that. Four causes cover nearly every case, and the file path you pass to the upload call usually tells you which one you hit.&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 plaintext"&gt;&lt;code&gt;StorageApiError: new row violates row-level security policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What is this error? It is PostgreSQL refusing an insert into &lt;code&gt;storage.objects&lt;/code&gt;: every file in Supabase Storage is a row in that table, and an upload is an insert into it. Row level security checks it exactly as it checks an insert into one of your own tables, which is why the message is the same one PostgreSQL raises for a table (that case is on &lt;a href="https://ticassociation.com/supabase-42501-row-level-security" rel="noopener noreferrer"&gt;the 42501 page&lt;/a&gt;). In &lt;a href="https://supabase.com/docs/guides/storage/security/access-control" rel="noopener noreferrer"&gt;Supabase's words&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."&lt;/p&gt;

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

&lt;h3&gt;
  
  
  There is no insert policy on storage.objects for your role
&lt;/h3&gt;

&lt;p&gt;The bucket exists, the file is fine, and nothing lets a signed-in user insert the row. &lt;a href="https://supabase.com/docs/guides/storage/security/access-control" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;: "the only RLS policy required for uploading objects is to grant the INSERT permission to the storage.objects table." This policy lets each signed-in user upload into a folder named after their own user id inside one bucket. Proven on a real PostgreSQL: with it, a user stores a file under their own id and is refused under anyone else's.&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;"users upload into their own folder"&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;'avatars'&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&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;foldername&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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="nb"&gt;text&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The path you upload to does not match the policy
&lt;/h3&gt;

&lt;p&gt;The policy above compares the first folder of the object's name with the signed-in user's id. An upload to &lt;code&gt;avatar.png&lt;/code&gt; with no folder, or to a folder named after a username, an email, or a different id, fails with the same message. Log the path you pass to the upload call: its first segment must be the user id of the session making the request, and the bucket in the call must be the bucket in the policy. You can read how the helper splits a path in your SQL editor:&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="p"&gt;(&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;foldername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'11111111-1111-1111-1111-111111111111/avatar.png'&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  You are overwriting a file (upsert)
&lt;/h3&gt;

&lt;p&gt;Uploading with &lt;code&gt;upsert: true&lt;/code&gt; onto a path that already exists turns the insert into an update of the existing row, and that row is checked against your update policies. &lt;a href="https://supabase.com/docs/guides/storage/security/access-control" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;: "To allow overwriting files using the upsert functionality you will need to additionally grant SELECT and UPDATE permissions." &lt;a href="https://www.postgresql.org/docs/current/sql-createpolicy.html" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt; says why the error is a refusal and not a silent skip: "unlike a standalone UPDATE command, if the existing row does not pass the USING expressions, an error will be thrown." Proven: the same overwrite is refused while only the insert policy exists and succeeds once these two are added, and the other user's folder stays closed.&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;"users read their own folder"&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;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="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'avatars'&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&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;foldername&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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="nb"&gt;text&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 overwrite their own files"&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;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="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'avatars'&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&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;foldername&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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="nb"&gt;text&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="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'avatars'&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&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;foldername&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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="nb"&gt;text&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;The policies say &lt;code&gt;to authenticated&lt;/code&gt;, and a request with no session reaches the database as the anonymous role, which they do not cover. Proven: the anonymous role is refused by the same policy that lets the signed-in user through. In the browser, make sure a session exists right before the upload. On the server, the client has to be built from the request's cookies; &lt;a href="https://supabase.com/docs/guides/auth/server-side/creating-a-client" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;: "Use the browser client in code that runs on the browser, and the server client in code that runs on the server." If &lt;code&gt;auth.uid()&lt;/code&gt; is null on the request, no folder policy can match; &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;h2&gt;
  
  
  Two shortcuts that &lt;em&gt;open the bucket to everyone&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not allow uploads with &lt;code&gt;with check (true)&lt;/code&gt; for every role.&lt;/strong&gt; It makes the error disappear because it stops checking anything. Proven on a real PostgreSQL: with the policy below in place, a request with no session at all stores an object in the bucket, so anyone holding your public key can fill it.&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;"anyone 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;anon&lt;/span&gt;&lt;span class="p"&gt;,&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;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Do not upload with the service key from the browser.&lt;/strong&gt; The service role bypasses every policy, which the suite confirms by storing an object with no policy that allows it. &lt;a href="https://supabase.com/docs/guides/storage/security/access-control" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;: "Service keys entirely bypass RLS policies, granting you unrestricted access to all Storage APIs. Remember you should not share the service key publicly."&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Does making the bucket public fix the upload?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. A public bucket changes who can read, not who can write. Supabase: "When a bucket is designated as 'Public,' it effectively bypasses access controls for both retrieving and serving files within the bucket." And in the same passage: "Access control is still enforced for other types of operations including uploading, deleting, moving, and copying." Uploads still need an insert policy on storage.objects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which policies does a profile photo upload need?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One insert policy on storage.objects for the authenticated role, scoped to the bucket and to a folder named after the user's id. If the app overwrites the photo with upsert, add a select policy and an update policy with the same scope. Nothing else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The error names a table called objects, and I never created one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is Supabase's own table, storage.objects, where every uploaded file is a row. Your policies for uploads go on that table, not on the bucket and not on any table of yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  One missing 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-42501-row-level-security" rel="noopener noreferrer"&gt;42501 on one of your own tables&lt;/a&gt;, or &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;Stuck on an upload that still fails after this? The &lt;a href="https://ticassociation.com/supabase-rls-audit" rel="noopener noreferrer"&gt;free RLS Auditor&lt;/a&gt; checks your policies read-only, and the &lt;a href="https://shop.ticassociation.com/l/rls-security-pack/LAUNCH?utm_source=hub" rel="noopener noreferrer"&gt;RLS Security Pack&lt;/a&gt; Launch price USD 11.40 (normal USD 19) for the first 25 buyers or until 2026-10-31, whichever comes first. The code is already in the link. (Gumroad, USD 19) has a tested recipe for each finding.&lt;/p&gt;

&lt;p&gt;© 2026 Tori &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-storage-upload-row-level-security" rel="noopener noreferrer"&gt;https://ticassociation.com/supabase-storage-upload-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>Five questions to ask before you use an AI answer</title>
      <dc:creator>TIC</dc:creator>
      <pubDate>Thu, 22 Oct 2026 00:15:00 +0000</pubDate>
      <link>https://dev.to/toritic/five-questions-to-ask-before-you-use-an-ai-answer-29d3</link>
      <guid>https://dev.to/toritic/five-questions-to-ask-before-you-use-an-ai-answer-29d3</guid>
      <description>&lt;p&gt;AI tools write answers that sound sure, including the wrong ones. You do not need to be technical to catch many of them. Ask these five questions, in this order, in the same chat.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"What would have to be true for this to be wrong?"&lt;/strong&gt; A good answer names real conditions. A vague one tells you the first answer was weaker than it sounded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Which parts are you sure about, and which parts are you guessing?"&lt;/strong&gt; Mark the guesses and check only those.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What did you assume about my situation?"&lt;/strong&gt; Many bad answers are good answers to a question you did not ask. Your country, your budget and your skill level are the usual missing pieces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Give me one source I can open myself, and tell me if you are not certain it exists."&lt;/strong&gt; Then open it. If the link or the title is not real, treat the rest of the answer with care.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How could I check this in five minutes without you?"&lt;/strong&gt; A search, a calculator, a phone call, a look at the official page. Do that one thing before you act on the answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this makes the tool perfect. You stay the one who decides.&lt;/p&gt;




&lt;p&gt;We are TIC, a small studio that makes plain-language AI guides (ticassociation.com). The list above is free to copy and share.&lt;/p&gt;

&lt;p&gt;Drafted with AI help and approved by a person before posting.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>beginners</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to read the free limit of an AI tool before you rely on it</title>
      <dc:creator>TIC</dc:creator>
      <pubDate>Wed, 21 Oct 2026 00:14:58 +0000</pubDate>
      <link>https://dev.to/toritic/how-to-read-the-free-limit-of-an-ai-tool-before-you-rely-on-it-25bb</link>
      <guid>https://dev.to/toritic/how-to-read-the-free-limit-of-an-ai-tool-before-you-rely-on-it-25bb</guid>
      <description>&lt;p&gt;A tool that is free today can stop working for you halfway through a task. Most of the time the limit was written down. Here is the five-minute check we run before building anything on a free plan.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Go to the pricing page, not a review.&lt;/strong&gt; Reviews often copy old numbers. The tool's own page is the one to trust, and the one you can point to later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find the unit.&lt;/strong&gt; Is the limit in messages, credits, minutes, images or files? Two tools that both say "free" can mean very different amounts of work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find the reset period.&lt;/strong&gt; Per day, per month, or once and never again? A one-time allowance is a trial, not a free plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look for what is left out.&lt;/strong&gt; The best model, longer files, exports, or commercial use are often excluded from the free tier. Read the line about commercial use before you use the output for a client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find out what happens at the limit.&lt;/strong&gt; It may slow down, switch to a weaker model, stop until the reset, or ask for a card. Know which one before you are in the middle of a task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write down the date you checked.&lt;/strong&gt; Limits change without notice. A note like "checked on 15 Oct: this plan allows N messages a day" is worth more than your memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep one fallback.&lt;/strong&gt; Pick a second tool for the same job so a limit becomes a switch, not a stop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you do only one thing from this list, do number 6. It turns a guess into something you can recheck in a minute.&lt;/p&gt;




&lt;p&gt;We are TIC, a small studio that makes plain-language AI guides (ticassociation.com). The list above is free to copy and share.&lt;/p&gt;

&lt;p&gt;Drafted with AI help and approved by a person before posting.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>beginners</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Supabase works locally, not in production: 5 checks</title>
      <dc:creator>TIC</dc:creator>
      <pubDate>Sun, 18 Oct 2026 00:13:22 +0000</pubDate>
      <link>https://dev.to/toritic/supabase-works-locally-not-in-production-5-checks-4mnj</link>
      <guid>https://dev.to/toritic/supabase-works-locally-not-in-production-5-checks-4mnj</guid>
      <description>&lt;p&gt;Your local run and your production deployment differ in five places: the public variables the browser bundle was built with, the URL sign-in returns to, the cookies a server route reads, which key runs where, and whether the hosted database got your migrations. Check them in this order and the cause is usually the first one that fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same code, &lt;em&gt;two different environments&lt;/em&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run dev, localhost:   works
vercel production:        500, a blank page, "Invalid API key", or sign-in that lands on localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What is different in production? Not your code. A production build freezes the public variables into the bundle, the deployment runs with the variables that existed when it was built, sign-in returns to whatever URL your project allows, and a server route only knows the user if the request's cookies reach it. Each of those is a setting, and each has a primary source below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five checks. &lt;em&gt;In this order&lt;/em&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The public variables were not there when the bundle was built
&lt;/h3&gt;

&lt;p&gt;Anything the browser needs, such as your Supabase URL and publishable key, is prefixed &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt;, and &lt;a href="https://nextjs.org/docs/app/guides/environment-variables" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; says what that does: "it will be inlined into any JavaScript sent to the browser." And the consequence: "variables will be frozen with the value evaluated at build time, so these values need to be set appropriately when the project is built." So a variable added to the dashboard after the build is not in the bundle. &lt;a href="https://vercel.com/docs/environment-variables" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt;: "Any change you make to Environment Variables are not applied to previous deployments, they only apply to new deployments." Check: the same names as in your &lt;code&gt;.env.local&lt;/code&gt; exist for the Production environment, then redeploy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Vercel project &amp;gt; Settings &amp;gt; Environment Variables &amp;gt; Production
NEXT_PUBLIC_SUPABASE_URL              set
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY  set       then: Deployments &amp;gt; Redeploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Sign-in comes back to localhost
&lt;/h3&gt;

&lt;p&gt;Magic links and OAuth return the user to the URL the project allows, and a project that only ever knew &lt;code&gt;http://localhost:3000&lt;/code&gt; sends production users there. &lt;a href="https://supabase.com/docs/guides/auth/redirect-urls" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;: "we recommend setting the exact redirect URL path for your site URL in production." Check: the Site URL is your production domain and the production callback path is in the redirect allow list.&lt;/p&gt;

&lt;h3&gt;
  
  
  A server route has no session
&lt;/h3&gt;

&lt;p&gt;Locally you were signed in through the browser; in production a server component, route handler or middleware built a client with no cookies, and the database saw nobody. &lt;a href="https://supabase.com/docs/guides/auth/server-side/creating-a-client" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;: "Server-Side Rendering (SSR) with Supabase requires cookie-based session storage." Then every policy that compares &lt;code&gt;auth.uid()&lt;/code&gt; fails, inserts get 42501 and selects come back empty. Proven on a real PostgreSQL and explained on &lt;a href="https://ticassociation.com/supabase-auth-uid-null-rls" rel="noopener noreferrer"&gt;the auth.uid() page&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The wrong key in the wrong place
&lt;/h3&gt;

&lt;p&gt;The publishable key belongs in the browser and runs under row level security. The secret key belongs on the server only, and even there it does not always bypass policies. &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;: "A secret key bypasses RLS only when the request carries no user access token." Check: no secret key in any &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; variable, and the server code that needs to bypass policies is not forwarding the user's token.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hosted database never got the migration
&lt;/h3&gt;

&lt;p&gt;If local development runs against a local database, the table, column or policy you added there does not exist on the hosted project until your migration is applied to it. Check: open the hosted project's SQL editor and confirm the table and its policies are there; if row level security is on with no policy, every request is refused, which is &lt;a href="https://ticassociation.com/supabase-42501-row-level-security" rel="noopener noreferrer"&gt;the 42501 page&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Do I have to redeploy after adding an environment variable on Vercel?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Vercel's documentation says changes to environment variables are not applied to previous deployments and only apply to new deployments, and Next.js inlines public variables into the bundle at build time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does sign-in send production users to localhost?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because the project's Site URL or redirect allow list still points at localhost. Supabase recommends setting the exact redirect URL path for your site URL in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which Supabase key goes in the browser?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The publishable key, which runs under row level security. The secret key stays on the server, and it bypasses row level security only when the request carries no user access token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Still broken after the five checks? &lt;em&gt;Audit the policies&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Most "works locally, fails in production" cases are a policy or a grant. The auditor is a read-only SQL file you run in your own SQL editor, and it lists the holes worst first.&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;Want a tested recipe for each finding? The &lt;a href="https://shop.ticassociation.com/l/rls-security-pack/LAUNCH?utm_source=hub" rel="noopener noreferrer"&gt;RLS Security Pack&lt;/a&gt; Launch price USD 11.40 (normal USD 19) for the first 25 buyers or until 2026-10-31, whichever comes first. The code is already in the link. (Gumroad, USD 19) has them.&lt;/p&gt;

&lt;p&gt;© 2026 Tori &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-app-works-locally-not-in-production" rel="noopener noreferrer"&gt;https://ticassociation.com/supabase-app-works-locally-not-in-production&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>webdev</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
    <item>
      <title>3 Production Security Pitfalls in Next.js App Router (And How to Fix Them)</title>
      <dc:creator>TIC</dc:creator>
      <pubDate>Tue, 13 Oct 2026 13:09:58 +0000</pubDate>
      <link>https://dev.to/toritic/3-production-security-pitfalls-in-nextjs-app-router-and-how-to-fix-them-5g34</link>
      <guid>https://dev.to/toritic/3-production-security-pitfalls-in-nextjs-app-router-and-how-to-fix-them-5g34</guid>
      <description>&lt;p&gt;Building fast with modern AI coding assistants makes shipping fullstack web apps effortless. However, code generators and tutorials often produce code that passes local tests but suffers from critical architectural security vulnerabilities in production.&lt;/p&gt;

&lt;p&gt;When building multi-tenant SaaS applications with Next.js App Router and Supabase, developers frequently encounter subtle security flaws that allow unauthorized users to view private database records.&lt;/p&gt;

&lt;p&gt;In this guide, we walk through three common vulnerability scenarios, provide reproducible code examples, demonstrate secure implementation patterns, and cite official framework specifications.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Code Implementation Disclaimer:&lt;/strong&gt; The reference implementation patterns and code snippets in this guide have been statically reviewed against official framework specifications (@supabase/ssr 0.5.x and Next.js 14/15 App Router) rather than executed as live automated test suites. Always verify integration behavior within your project's specific dependency versions and deployment environment.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Vulnerability Scenario 1: The "auth.uid() IS NOT NULL" Trap
&lt;/h3&gt;

&lt;p&gt;When developers first enable PostgreSQL Row-Level Security (RLS), tutorials frequently suggest a policy like this:&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="c1"&gt;-- VULNERABLE RLS POLICY&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;"Allow authenticated users to read records"&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;customer_records&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="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="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Why It Fails:
&lt;/h4&gt;

&lt;p&gt;This policy only checks whether the client has presented a valid JWT token. It does NOT verify that the authenticated user owns the specific row being requested.&lt;/p&gt;

&lt;p&gt;If User A logs in and runs &lt;code&gt;supabase.from('customer_records').select('*')&lt;/code&gt;, PostgreSQL evaluates &lt;code&gt;auth.uid() IS NOT NULL&lt;/code&gt; to true for every single row in the table. User A can now read all customer records belonging to User B, User C, and every other tenant.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Secure Implementation Pattern:
&lt;/h4&gt;

&lt;p&gt;A secure RLS policy must explicitly check that the record belongs to the requester:&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="c1"&gt;-- SECURE RLS POLICY: Direct Tenant Ownership&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 only read own records"&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;customer_records&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="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;p&gt;For team or organization-based workspaces, join through a validated membership 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="c1"&gt;-- SECURE RLS POLICY: Workspace Membership&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;"Members can view workspace records"&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;workspace_documents&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;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&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;workspace_memberships&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;workspace_memberships&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;workspace_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;workspace_documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;workspace_id&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;workspace_memberships&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&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="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Vulnerability Scenario 2: Shared Cache &amp;amp; Set-Cookie Leaks in Next.js App Router
&lt;/h3&gt;

&lt;p&gt;In Next.js 14 and 15, route handlers and Server Components cache aggressively by default. If your middleware or route handler refreshes Supabase session tokens, an intermediate proxy or CDN might cache the response headers, including &lt;code&gt;Set-Cookie&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why It Fails:
&lt;/h4&gt;

&lt;p&gt;When User A visits &lt;code&gt;/dashboard&lt;/code&gt;, the server generates a new session token and returns a &lt;code&gt;Set-Cookie: sb-&amp;lt;project_ref&amp;gt;-auth-token=...&lt;/code&gt; header. If the response headers lack explicit cache privacy directives, a shared edge cache (e.g., Cloudflare, CloudFront, or Vercel Edge Cache) may cache that response.&lt;/p&gt;

&lt;p&gt;When User B visits the same URL, the edge cache serves the cached response along with User A's session cookie. User B is now authenticated as User A.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Secure Implementation Pattern:
&lt;/h4&gt;

&lt;p&gt;Any endpoint handling authentication credentials or user-specific data must declare strict private caching directives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/auth/callback/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Authentication logic...&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="c1"&gt;// Enforce private cache boundary&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;private, no-cache, no-store, must-revalidate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pragma&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Expires&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Vulnerability Scenario 3: Stripe Webhook HMAC Mutation
&lt;/h3&gt;

&lt;p&gt;When integrating payment webhooks, developers frequently parse incoming JSON requests using standard body parsers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// VULNERABLE WEBHOOK HANDLER&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// BUG: Mutates signed byte order&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Fails or behaves inconsistently:&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Why It Fails:
&lt;/h4&gt;

&lt;p&gt;Stripe signs the raw request payload bytes. Calling &lt;code&gt;request.json()&lt;/code&gt; and re-stringifying it mutates whitespace, property key ordering, and floating-point number representations. This causes HMAC verification to fail intermittently.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Secure Implementation Pattern:
&lt;/h4&gt;

&lt;p&gt;Always read the unparsed string buffer before HMAC verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SECURE WEBHOOK HANDLER&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2023-10-16&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// PRESERVES EXACT SIGNED BYTES&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Missing Stripe signature header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Process verified event idempotently...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook Error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Automated Verification Script
&lt;/h3&gt;

&lt;p&gt;Run this quick audit in your project root to check for common security configuration issues:&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;# 1. Detect leaked secrets in public environment variables&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"NEXT_PUBLIC_.*(SECRET|PRIVATE_KEY|SERVICE_ROLE)"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.env*"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.ts"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.tsx"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.js"&lt;/span&gt;

&lt;span class="c"&gt;# 2. Check for raw JSON parsing inside Stripe webhook handlers&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"request.json()"&lt;/span&gt; app/api/webhooks/ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"WARNING: Stripe webhooks must read request.text(), not request.json()"&lt;/span&gt;

&lt;span class="c"&gt;# 3. Detect broad RLS policies that check auth without tenant filtering&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"USING&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\(\s&lt;/span&gt;&lt;span class="s2"&gt;*auth&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;uid&lt;/span&gt;&lt;span class="se"&gt;\(\)\s&lt;/span&gt;&lt;span class="s2"&gt;+IS&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;+NOT&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;+NULL&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.sql"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"WARNING: Policy checks login, but not tenant row ownership"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Complete 18-Point Production Checklist &amp;amp; Open Runbook
&lt;/h3&gt;

&lt;p&gt;These three scenarios are part of an 18-point production invariant checklist developed to audit multi-tenant web applications under the REST Personal Economic Operating System.&lt;/p&gt;

&lt;p&gt;The complete 18-point runbook and automated bash audit script are available as an open, free resource:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rest-auth-guard.vercel.app/?utm_source=devto&amp;amp;utm_campaign=phase5_pilot&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;Download the Free 18-Point Production Security Runbook&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;100% free open developer resource (EUR 0.00). No email signup or credit card required.&lt;/p&gt;

&lt;p&gt;Published by Tori under the REST Personal Economic Operating System.&lt;br&gt;&lt;br&gt;
Questions or feedback: &lt;a href="mailto:hello@ticassociation.com"&gt;hello@ticassociation.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>supabase</category>
      <category>postgres</category>
      <category>security</category>
    </item>
    <item>
      <title>We checked 15 'make money with AI' schemes against their sources</title>
      <dc:creator>TIC</dc:creator>
      <pubDate>Fri, 09 Oct 2026 00:17:20 +0000</pubDate>
      <link>https://dev.to/toritic/we-checked-15-make-money-with-ai-schemes-against-their-sources-1imb</link>
      <guid>https://dev.to/toritic/we-checked-15-make-money-with-ai-schemes-against-their-sources-1imb</guid>
      <description>&lt;p&gt;Every week there is a new "make money with AI" playbook. We took 15 of the most common ones, opened the sources behind them, and labelled each scheme by what we could check. Date of the check: 2026-10-08.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VERIFIED&lt;/strong&gt;: the figures come from a platform, a company report, a data service or a regulator we could open. It does not mean the scheme pays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLAIM&lt;/strong&gt;: someone states a figure or a rule we could not trace, or could only trace in part.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MARKETING&lt;/strong&gt;: the figures come from a company that sells a tool, platform or course in the same area, with no outside check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: 7 verified, 3 claims, 5 marketing. In the sources we opened, none of the 15 came with a dated record of what a newcomer earned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven with traceable figures
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Micro-SaaS.&lt;/strong&gt; &lt;a href="https://trustmrr.com/category/ai" rel="noopener noreferrer"&gt;TrustMRR&lt;/a&gt; verifies revenue through Stripe and lists 4,152 AI startups. Of the first 30 shown, the highest has $864k in monthly recurring revenue. Two listings we opened show &lt;a href="https://trustmrr.com/startup/viasibility?metric=mrr" rel="noopener noreferrer"&gt;$70 all-time&lt;/a&gt; and &lt;a href="https://trustmrr.com/startup/indiemerger?metric=mrr" rel="noopener noreferrer"&gt;$4,549 all-time&lt;/a&gt;, both with no active subscription. The site does not show how revenue is spread across the 4,152, so a top exists and a tiny bottom exists, and the typical result is unknown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solo AI-native companies.&lt;/strong&gt; &lt;a href="https://stripe.com/blog/top-solo-founder-traits" rel="noopener noreferrer"&gt;Stripe&lt;/a&gt; (2026-05-28) found AI-native solo startups earned almost twice the revenue of other solo-founded startups by month 24, and the median solo B2B founder earned more than four times the median solo B2C founder. It also found median first-six-month revenue in 2025 down 23% year over year while the top decile was up 19%. Atlas companies are a self-selected group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Freelance marketplaces.&lt;/strong&gt; &lt;a href="https://www.selfemployed.com/news/fiverr-q2-2026-earnings-ai-freelance/" rel="noopener noreferrer"&gt;Fiverr's Q2 2026&lt;/a&gt;: revenue $97.8M, down 10%, marketplace revenue down 15.5%, with the decline tied to AI eroding low-value gigs. &lt;a href="https://www.staffingindustry.com/news/global-daily-news/upwork-q2-revenue-falls-17-though-ai-related-work-provides-bright-spot" rel="noopener noreferrer"&gt;Upwork's Q2 2026&lt;/a&gt;: total volume down 3.6%, AI-related volume up more than 22% to about $330M. Both are news summaries of company results, and market totals do not tell you what a new account wins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;x402 agent payments.&lt;/strong&gt; &lt;a href="https://regional-front.cointelegraph.com/news/ai-agent-payment-volume-closer-to-1-6m-says-a16z" rel="noopener noreferrer"&gt;a16z, using Allium data&lt;/a&gt; (2026-03-12) put 30 days of agent transactions at about $3M, and about $1.6M after removing wash trades. An earlier reported figure was $24M.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two "AI income" business opportunities, per the FTC.&lt;/strong&gt; An &lt;a href="https://www.ftc.gov/news-events/news/press-releases/2025/06/ftc-case-leads-order-banning-ascend-ecom-its-owners-business-opportunity-marketing" rel="noopener noreferrer"&gt;FTC release dated 2025-06-23&lt;/a&gt; says an e-commerce coaching company charged tens of thousands of dollars for online storefronts and marketed AI-powered tools; the FTC alleges it falsely claimed buyers would quickly earn thousands of dollars a month. The proposed order is a $25M judgment, partly suspended, and a permanent ban on selling business opportunities. An &lt;a href="https://www.ftc.gov/news-events/news/press-releases/2026/03/air-ai-its-owners-will-be-banned-marketing-business-opportunities-settle-ftc-charges-company-misled" rel="noopener noreferrer"&gt;FTC release dated 2026-03-24&lt;/a&gt; reports a proposed settlement with an AI sales-agent company over alleged false earnings and refund claims: an $18M judgment, largely suspended, and a ban on selling business opportunities. These are allegations and proposed orders, not court findings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three claims we could not confirm
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AI content and programmatic SEO.&lt;/strong&gt; Google's &lt;a href="https://developers.google.com/search/docs/essentials/spam-policies" rel="noopener noreferrer"&gt;spam policy&lt;/a&gt; defines scaled content abuse as pages made mainly to manipulate rankings and says it does not matter how the content is created. A &lt;a href="https://alexcloudstar.com/blog/programmatic-seo-2026-indie-hackers/" rel="noopener noreferrer"&gt;September 2026 blog&lt;/a&gt; says enforcement removed 50 to 80 percent of traffic from thin sites, and that AI Overviews cost the top result about 58% of clicks. It names no source for either number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Faceless YouTube channels.&lt;/strong&gt; &lt;a href="https://support.google.com/youtube/answer/1311392" rel="noopener noreferrer"&gt;YouTube's help page&lt;/a&gt; (notice dated 2025-07-15) says repetitive or mass-produced content "has always been ineligible for monetization." A &lt;a href="https://air.io/en/monetization/youtube-monetization-policy-changes-2026-a-complete-dated-timeline" rel="noopener noreferrer"&gt;services firm&lt;/a&gt; says channels with about 35 million subscribers were removed in January 2026; it cites no source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI-written books and digital products.&lt;/strong&gt; &lt;a href="https://kdp.amazon.com/en_US/help/topic/G200672390" rel="noopener noreferrer"&gt;Amazon KDP&lt;/a&gt; requires you to disclose AI-generated content but not AI-assisted content. We found rules, and no earnings source at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five where the numbers come from a seller
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automation agencies.&lt;/strong&gt; &lt;a href="https://betonai.net/?p=386" rel="noopener noreferrer"&gt;The page that ranks&lt;/a&gt; says its figures are "estimates from public rate pages, not a survey."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI voice agencies.&lt;/strong&gt; The &lt;a href="https://dev.to/alfredo_romero_fc4afdcb68/state-of-ai-voice-for-agencies-2026-market-data-margins-and-platform-shifts-1fb4"&gt;margin figures&lt;/a&gt; come from a survey of 47 applicants to the author's own platform beta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GEO audits.&lt;/strong&gt; &lt;a href="https://ciela.ai/blogs/geo-services-pricing-what-to-charge-clients" rel="noopener noreferrer"&gt;Price ranges&lt;/a&gt; from a company that sells a demo platform to agencies, with no source for the ranges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid MCP servers and Apify Actors.&lt;/strong&gt; &lt;a href="https://apify.com/mcp/developers" rel="noopener noreferrer"&gt;Apify's page&lt;/a&gt; quotes "$500k+ payouts to developers every month" with no date. One &lt;a href="https://godberrystudios.com/posts/how-to-monetize-mcp-servers-2026/" rel="noopener noreferrer"&gt;independent author&lt;/a&gt; reported zero paying users after two weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI avatar videos and TikTok Shop.&lt;/strong&gt; A &lt;a href="https://www.shortformnation.com/blog/ai-generated-ugc-for-tiktok-shop-what-actually-works-and-what-gets-you-demonetized-2026-guide" rel="noopener noreferrer"&gt;guide&lt;/a&gt; from a company that sells an analytics platform, with no creator earnings in it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to read the next one
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Who is selling something?&lt;/li&gt;
&lt;li&gt;Is it a price list or a sales record?&lt;/li&gt;
&lt;li&gt;How many tried?&lt;/li&gt;
&lt;li&gt;Revenue or profit?&lt;/li&gt;
&lt;li&gt;Can a third party check it?&lt;/li&gt;
&lt;li&gt;Is there a date?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What we could not verify
&lt;/h2&gt;

&lt;p&gt;A dated record of what any newcomer earned from any of the 15. How revenue is spread across TrustMRR's listings. Fiverr's and Upwork's own releases, which we read only as news summaries. Revenue screenshots on Reddit and X, which we could not open. If you have a source we missed, tell us in the comments and we will check it and note the date.&lt;/p&gt;

&lt;p&gt;Tori&lt;/p&gt;

</description>
      <category>ai</category>
      <category>business</category>
      <category>startup</category>
      <category>freelance</category>
    </item>
    <item>
      <title>auth.uid() is null: why Supabase RLS denies your app</title>
      <dc:creator>TIC</dc:creator>
      <pubDate>Thu, 08 Oct 2026 16:30:03 +0000</pubDate>
      <link>https://dev.to/toritic/authuid-is-null-why-supabase-rls-denies-your-app-17ah</link>
      <guid>https://dev.to/toritic/authuid-is-null-why-supabase-rls-denies-your-app-17ah</guid>
      <description>&lt;p&gt;Row level security compares &lt;code&gt;auth.uid()&lt;/code&gt; with the row's owner. When the request carries no JWT, &lt;code&gt;auth.uid()&lt;/code&gt; is null, every comparison is false, inserts fail with 42501 and selects come back empty with no error at all. The SQL editor hides it too: on its default role it runs as postgres, which row security lets through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three faces of &lt;em&gt;one missing session&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, insert:  42501 new row violates row-level security policy for table "profiles"
from the app, select:  []   no error, no rows
in the SQL editor:     works on its default role
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What is &lt;code&gt;auth.uid()&lt;/code&gt;? It is the Supabase helper that reads the subject of the JWT that came with the request. In &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase's words&lt;/a&gt;: "When a request is made without an authenticated user (e.g., no access token is provided or the session has expired), auth.uid() returns null". A policy that compares it with the owner column then fails on every row, because, as the same guide puts it, "null = user_id is always false in SQL". Proven on a real PostgreSQL: with no subject on the request the insert is refused with 42501, the select returns 0 rows and no error, and both succeed the moment the subject is present. You can see the value your editor session carries:&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;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;p&gt;On the editor's default role it returns null too. There is no JWT there either; that role is just not stopped by it, for the reason in the last section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three places. &lt;em&gt;Check them in this order&lt;/em&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The browser client has no session yet
&lt;/h3&gt;

&lt;p&gt;The query ran before sign-in finished, or after the session expired. Right before the query, ask the client who the user is and log the answer; if it is empty, nothing you do in the policy will help. Do not widen the policy to the anonymous role to make the error go away: that hands the table to anyone holding your public key.&lt;/p&gt;

&lt;h3&gt;
  
  
  A server route built its client without the user's cookies
&lt;/h3&gt;

&lt;p&gt;A server component, route handler or middleware that creates a plain client has no session, so the database sees no user. &lt;a href="https://supabase.com/docs/guides/auth/server-side/creating-a-client" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;: "Server-Side Rendering (SSR) with Supabase requires cookie-based session storage." And: "Use the browser client in code that runs on the browser, and the server client in code that runs on the server." The server client is built from the request's cookies, and that is what carries the JWT to the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  A secret-key client on the server still runs as the user
&lt;/h3&gt;

&lt;p&gt;This one surprises people in the other direction: the request was denied even though the server used the secret key. &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;: "A secret key bypasses RLS only when the request carries no user access token. If the request carries one, it runs under the RLS policies of that signed-in user, even when the client library was initialized with a secret key." So a secret-key client that also forwards the user's token is checked like the user, and a secret-key client that forwards nothing bypasses every policy. Keep the second kind on the server only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Say what you mean: &lt;em&gt;signed in, and the owner&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;(select auth.uid()) = user_id&lt;/code&gt; already refuses a request with no session, because null never equals anything. The &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;guide&lt;/a&gt; still asks for the check to be explicit: "we recommend explicitly checking for authentication". This policy does that for every operation on the table. Proven: with no subject it refuses the insert (42501) and returns 0 rows; with the subject present the same user inserts and reads their own row.&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;"own rows only"&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;all&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="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;and&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="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;and&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;h2&gt;
  
  
  The editor is &lt;em&gt;the owner, by default&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;By default, queries in the dashboard run as postgres. &lt;a href="https://supabase.com/docs/guides/database/postgres/roles" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;: "Queries you run in the Dashboard execute as postgres". That role usually owns your tables, and &lt;a href="https://www.postgresql.org/docs/current/ddl-rowsecurity.html" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt; lets owners through: "Table owners normally bypass row security as well". Proven: the table owner inserts a row with no JWT at all, while the signed-in role with no JWT is refused. So a query that works in the editor on its default role tells you nothing about the policy. To test the policy itself, switch the editor's role dropdown to Authenticated and pick the user. &lt;a href="https://supabase.com/blog/studio-introducing-assistant" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;: "You can use the new Role dropdown to select a different Postgres role for your queries in Studio."&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Why does select auth.uid() return null in the SQL editor?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because on its default role the editor sends no JWT: auth.uid() reads the subject of the request's JWT, and there is none. It still works on your tables because it runs as postgres, which usually owns them, and row security lets the owner through. Switch the role dropdown to Authenticated and pick a user to test as them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do I get no error, just no rows?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A select is filtered, not refused: rows that fail the policy are simply left out. With auth.uid() null every row fails, so the result is empty and nothing raises. Proven on a real PostgreSQL: 0 rows, no error, and the same select returns the user's row once the JWT subject is present.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is auth.uid() = user_id wrong?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No, it refuses a request with no session as well, because null equals nothing. Supabase recommends adding the explicit null check so the intention is clear to whoever reads the policy next. The suite runs the explicit form on this page and confirms it refuses and allows exactly as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  One policy is rarely the only one. &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-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-storage-upload-row-level-security" rel="noopener noreferrer"&gt;the same error on a file upload&lt;/a&gt;, or &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;Still denied after the session is fixed? The &lt;a href="https://ticassociation.com/supabase-rls-audit" rel="noopener noreferrer"&gt;free RLS Auditor&lt;/a&gt; checks the rest of your policies read-only, and the &lt;a href="https://shop.ticassociation.com/l/rls-security-pack/LAUNCH?utm_source=hub" rel="noopener noreferrer"&gt;RLS Security Pack&lt;/a&gt; Launch price USD 11.40 (normal USD 19) for the first 25 buyers or until 2026-10-31, whichever comes first. The code is already in the link. (Gumroad, USD 19) has a tested recipe for each finding.&lt;/p&gt;

&lt;p&gt;© 2026 Tori &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-auth-uid-null-rls" rel="noopener noreferrer"&gt;https://ticassociation.com/supabase-auth-uid-null-rls&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Supabase: permission denied for table (42501)</title>
      <dc:creator>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? The free &lt;a href="https://ticassociation.com/supabase-rls-audit" rel="noopener noreferrer"&gt;RLS Auditor&lt;/a&gt; checks the rest of your tables read-only, and the &lt;a href="https://shop.ticassociation.com/l/rls-security-pack/LAUNCH?utm_source=hub" rel="noopener noreferrer"&gt;RLS Security Pack&lt;/a&gt; Launch price USD 11.40 (normal USD 19) for the first 25 buyers or until 2026-10-31, whichever comes first. The code is already in the link. (Gumroad, USD 19) has a tested recipe for each finding.&lt;/p&gt;

&lt;p&gt;© 2026 Tori &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>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>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>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>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>
  </channel>
</rss>
