<?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: Victor Caña</title>
    <description>The latest articles on DEV Community by Victor Caña (@victor_caa_ab4153b4bcf6e).</description>
    <link>https://dev.to/victor_caa_ab4153b4bcf6e</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3635403%2F3600a84a-6987-4b34-a2e5-0f76ceebce82.jpg</url>
      <title>DEV Community: Victor Caña</title>
      <link>https://dev.to/victor_caa_ab4153b4bcf6e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/victor_caa_ab4153b4bcf6e"/>
    <language>en</language>
    <item>
      <title>How I turned a single Supabase query into 19GB of egress</title>
      <dc:creator>Victor Caña</dc:creator>
      <pubDate>Tue, 14 Apr 2026 16:12:48 +0000</pubDate>
      <link>https://dev.to/victor_caa_ab4153b4bcf6e/how-i-turned-a-single-supabase-query-into-19gb-of-egress-7ob</link>
      <guid>https://dev.to/victor_caa_ab4153b4bcf6e/how-i-turned-a-single-supabase-query-into-19gb-of-egress-7ob</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;I caught it in Supabase billing metrics: one dashboard query was responsible for a massive jump in egress, way out of proportion to the actual traffic.&lt;/p&gt;

&lt;p&gt;At first, it looked like a normal internal dashboard issue. In reality, a single &lt;code&gt;.select('*')&lt;/code&gt; on a large table was quietly pulling far more data than the UI ever needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical context
&lt;/h2&gt;

&lt;p&gt;The app is &lt;strong&gt;ReadyToRelease&lt;/strong&gt;, built with &lt;strong&gt;Next.js 14&lt;/strong&gt; and &lt;strong&gt;Supabase&lt;/strong&gt;, and this query lived inside an internal dashboard. It was not a Stripe bug, not a Groq issue, and not a frontend rendering problem. The real issue was simpler and more dangerous: the dashboard was asking Supabase for every column in every matching row.&lt;/p&gt;

&lt;p&gt;That kind of query is easy to miss during development because it works fine on small datasets. But once the table grows and the dashboard starts loading often, the cost profile changes fast. In my case, the result was &lt;strong&gt;19GB of egress&lt;/strong&gt; before I noticed what was happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problematic code
&lt;/h2&gt;

&lt;p&gt;Here was the original query:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reports&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&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="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks harmless at first glance. The problem is that &lt;code&gt;*&lt;/code&gt; is a convenience shortcut, not a performance strategy. It returns every column, including fields the dashboard never displayed, which increases payload size and therefore egress.&lt;/p&gt;

&lt;p&gt;The second issue was that the query did not limit the number of rows. Even if each row is moderate in size, a few hundred or a few thousand rows can become expensive when the query runs often.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The solution was to make the query explicit and bounded:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reports&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id, created_at, status, title&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="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This changed two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It fetched only the columns the dashboard actually rendered.&lt;/li&gt;
&lt;li&gt;It limited the result set to 25 rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That reduced payload size immediately, which brought egress down to almost nothing and made the dashboard feel faster at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this worked
&lt;/h2&gt;

&lt;p&gt;The lesson is that egress problems are often query design problems in disguise. When you fetch more fields than you render, you pay for data transfer you do not use. When you fetch more rows than the interface needs, you multiply that waste across every load.&lt;/p&gt;

&lt;p&gt;In this case, the fix was not a bigger cache, a new edge layer, or a database rewrite. It was simply making the query match the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  General rule
&lt;/h2&gt;

&lt;p&gt;If a screen only shows five fields and 25 rows, the query should probably ask for five fields and 25 rows.&lt;/p&gt;

&lt;p&gt;That rule sounds obvious, but it is easy to ignore when you are moving quickly. Defaults like &lt;code&gt;.select('*')&lt;/code&gt; are convenient during prototyping, yet they can become expensive once real users and real tables enter the picture.&lt;/p&gt;

&lt;p&gt;One useful habit is to treat every dashboard query like a public API response: return only what the client needs, nothing more. That keeps payloads smaller, latency lower, and billing surprises less likely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing note
&lt;/h2&gt;

&lt;p&gt;A single query can be cheap in development and expensive in production. In my case, that difference showed up as 19GB of Supabase egress and a very avoidable bill.&lt;/p&gt;

&lt;p&gt;The fix was small, but the lesson was not: data transfer costs are often hidden in plain sight, inside code that “works” but returns far too much.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related reading:&lt;/strong&gt; you can find more details about Supabase cost traps (including how &lt;code&gt;.maybeSingle()&lt;/code&gt; can silently fail when multiple rows exist) in my post &lt;a href="https://readytorelease.online/blog/supabase-maybesingle-multiple-rows-returns-null" rel="noopener noreferrer"&gt;Supabase: When &lt;code&gt;.maybeSingle()&lt;/code&gt; silently fails with multiple rows&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>supabase</category>
    </item>
    <item>
      <title>Next.js 14 www non-www redirects: GSC errors that won't die (and how I fixed them)</title>
      <dc:creator>Victor Caña</dc:creator>
      <pubDate>Tue, 07 Apr 2026 12:55:22 +0000</pubDate>
      <link>https://dev.to/victor_caa_ab4153b4bcf6e/nextjs-14-www-non-www-redirects-gsc-errors-that-wont-die-and-how-i-fixed-them-5g47</link>
      <guid>https://dev.to/victor_caa_ab4153b4bcf6e/nextjs-14-www-non-www-redirects-gsc-errors-that-wont-die-and-how-i-fixed-them-5g47</guid>
      <description>&lt;h1&gt;
  
  
  Next.js 14 www → non-www redirects: GSC errors that won't die (and how I fixed them)
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Switched from &lt;a href="https://www.readytorelease.online" rel="noopener noreferrer"&gt;www.readytorelease.online&lt;/a&gt; → &lt;a href="https://readytorelease.online" rel="noopener noreferrer"&gt;readytorelease.online&lt;/a&gt;.&lt;br&gt;&lt;br&gt;
Vercel 308 redirects = perfect.&lt;br&gt;&lt;br&gt;
Google Search Console: 4 “Crawled - currently not indexed” errors on &lt;strong&gt;WWW&lt;/strong&gt; after 72h.&lt;br&gt;&lt;br&gt;
Here’s the fix that took me 3 days to figure out.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;When you run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://www.readytorelease.online
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;HTTP/2 308
location: https://readytorelease.online/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks fine, right?&lt;br&gt;&lt;br&gt;
Permanent redirect in place, only one GSC property (&lt;code&gt;readytorelease.online&lt;/code&gt;), and correct redirect logic in &lt;code&gt;next.config.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But GSC still showed 4 “&lt;strong&gt;Crawled - currently not indexed&lt;/strong&gt;” errors for &lt;code&gt;www.readytorelease.online&lt;/code&gt; URLs.&lt;br&gt;&lt;br&gt;
Brand new ones (March 21–23).&lt;br&gt;&lt;br&gt;
Deleting the old property or hitting &lt;em&gt;Validate Fix&lt;/em&gt; did nothing for days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Root cause
&lt;/h3&gt;

&lt;p&gt;Google caches &lt;strong&gt;WWW&lt;/strong&gt; versions even after you stop serving them or delete the old property.&lt;br&gt;&lt;br&gt;
Those cached responses can persist 24–72 hours, confusing GSC into thinking redirects are broken.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 3 fixes that actually worked
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. next.config.js redirect (already done, but critical)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.js&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;redirects&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="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/:path*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;has&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;www.readytorelease.online&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://readytorelease.online/:path*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;permanent&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="c1"&gt;// 301&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;p&gt;This catches any request hitting the &lt;code&gt;www.&lt;/code&gt; host and permanently redirects it to the root domain.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Google Search Console → Coverage cleanup
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;Index → Pages → “Excluded”&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Filter all URLs containing &lt;code&gt;"www."&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
Click &lt;strong&gt;“Validate Fix”&lt;/strong&gt; on all of them.&lt;/p&gt;

&lt;p&gt;This tells Google to recrawl those URLs — usually within 24 hours — and refresh their redirect cache.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Aggressive URL Inspection
&lt;/h3&gt;

&lt;p&gt;From GSC:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inspect &lt;code&gt;https://www.readytorelease.online&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Hit &lt;strong&gt;“Test live URL”&lt;/strong&gt; → confirm that 308 redirect responds correctly
&lt;/li&gt;
&lt;li&gt;Then &lt;strong&gt;“Request indexing”&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Even though it sounds redundant, this step forced Google to acknowledge the redirect and finally cleared the WWW errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Result (after 24 h)
&lt;/h2&gt;

&lt;p&gt;✅ GSC shows “&lt;strong&gt;Validated&lt;/strong&gt;” for all previous WWW URLs&lt;br&gt;&lt;br&gt;
✅ Coverage report is clean&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;+15% impressions&lt;/strong&gt; on non-www URLs&lt;br&gt;&lt;br&gt;
✅ No more "Crawled - not indexed" ghosts&lt;/p&gt;




&lt;h2&gt;
  
  
  The key lesson
&lt;/h2&gt;

&lt;p&gt;GSC &lt;em&gt;caches redirects for 24–72 hours&lt;/em&gt;, even if your site returns perfect &lt;code&gt;301&lt;/code&gt; or &lt;code&gt;308&lt;/code&gt; responses.&lt;br&gt;&lt;br&gt;
If &lt;code&gt;curl -I&lt;/code&gt; or &lt;code&gt;fetch()&lt;/code&gt; show correct status codes, don’t panic over small (&amp;lt;10) errors.&lt;br&gt;&lt;br&gt;
But if they persist, use the &lt;strong&gt;Validate Fix + Test live URL&lt;/strong&gt; combo to force Google’s refresh.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Vercel’s redirects are instant.&lt;br&gt;&lt;br&gt;
GSC’s understanding of them isn’t.&lt;br&gt;&lt;br&gt;
Sometimes the crawler is the real bottleneck.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://readytorelease.online" rel="noopener noreferrer"&gt;readytorelease.online&lt;/a&gt; (Next.js 14 + Vercel)&lt;br&gt;&lt;br&gt;
Twitter: &lt;a href="https://twitter.com/shipwithvictor" rel="noopener noreferrer"&gt;@shipwithvictor&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔍 Curious how I used AI to validate my market before launch?&lt;br&gt;&lt;br&gt;
Check out &lt;a href="https://readytorelease.online" rel="noopener noreferrer"&gt;readytorelease.online&lt;/a&gt; — built with Next.js 14, Groq, and Supabase to automate market research for indie products.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>nextjs</category>
      <category>seo</category>
      <category>vercel</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Next.js Server Component fetch cache: no-store: the performance trap</title>
      <dc:creator>Victor Caña</dc:creator>
      <pubDate>Tue, 17 Mar 2026 16:19:56 +0000</pubDate>
      <link>https://dev.to/victor_caa_ab4153b4bcf6e/nextjs-server-component-fetch-cache-no-store-the-performance-trap-ho5</link>
      <guid>https://dev.to/victor_caa_ab4153b4bcf6e/nextjs-server-component-fetch-cache-no-store-the-performance-trap-ho5</guid>
      <description>&lt;p&gt;You added &lt;code&gt;cache: 'no-store'&lt;/code&gt; to a fetch call inside a Server Component because you needed fresh data. Reasonable. Standard. The docs even show it. What they don't tell you is that in the wrong place, that single option can turn one page load into hundreds of server invocations.&lt;/p&gt;




&lt;h3&gt;
  
  
  The context
&lt;/h3&gt;

&lt;p&gt;Server Components in Next.js 14 look deceptively simple. They run on the server, they fetch data, they render HTML. No client bundle, no useEffect, no loading states. Clean.&lt;/p&gt;

&lt;p&gt;But Server Components aren't rendered once and cached like a static page. Depending on where they sit in your component tree and how your routes are configured, they can re-execute on every request, or worse, on every rerender triggered by a parent.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;cache: 'no-store'&lt;/code&gt; to a fetch inside one of these components, and you've just told Next.js: &lt;em&gt;skip every layer of caching, always go to the origin, on every single execution.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The problematic code
&lt;/h3&gt;

&lt;p&gt;This is the pattern that causes the issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/dashboard/page.tsx: a dynamic route&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;getMarketData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://your-api.com/analyze?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&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;cache&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-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 👈 "I need fresh data"&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;res&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&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;DashboardPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;searchParams&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getMarketData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ResultsView&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks fine. But now imagine this page is wrapped in a layout that re-renders on navigation, or the component is used inside a Suspense boundary that retries on error, or you're running in a deployment where Vercel's infrastructure invokes the function per request segment.&lt;/p&gt;

&lt;p&gt;Each execution → one fetch call → one external API hit. At scale, or with any retry/rerender loop, this compounds fast.&lt;/p&gt;

&lt;p&gt;In my case with ReadyToRelease, a single user session generated &lt;strong&gt;776,000 Vercel function invocations&lt;/strong&gt; in under 24 hours. The root cause was exactly this pattern, &lt;code&gt;cache: 'no-store'&lt;/code&gt; inside a Server Component on a dynamic route with no deduplication in place.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why &lt;code&gt;cache: 'no-store'&lt;/code&gt; is dangerous here
&lt;/h3&gt;

&lt;p&gt;Next.js has its own fetch deduplication layer. When you call the same URL multiple times during a single render pass, Next.js deduplicates those requests automatically, but &lt;strong&gt;only when using the default cache behavior&lt;/strong&gt; or &lt;code&gt;cache: 'force-cache'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The moment you use &lt;code&gt;cache: 'no-store'&lt;/code&gt;, you opt out of that deduplication. Every call is treated as unique. Every render = a real network request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is deduplicated across the render tree ✅&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// This is NOT deduplicated: each call hits the network ❌&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/data&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;cache&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-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your component tree renders the same Server Component multiple times (parallel routes, multiple Suspense boundaries, layout + page both fetching), you're making multiple real requests.&lt;/p&gt;




&lt;h3&gt;
  
  
  What to use instead
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Option 1: &lt;code&gt;next: { revalidate: N }&lt;/code&gt;: time-based freshness&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-api.com/analyze&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;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// Fresh every 60 seconds, cached in between&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get reasonably fresh data without hammering the origin on every render. Good for data that changes, but not by the millisecond.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2: Route Segment Config: control at the route level&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/dashboard/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;force-dynamic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// entire route opts out of static&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default-no-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// fetch behavior for this segment&lt;/span&gt;

&lt;span class="c1"&gt;// Then your fetch stays clean:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-api.com/analyze&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separates the concern. The route is dynamic, but fetch deduplication still applies within a single render pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 3: Move the fetch outside the component tree&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/market-data.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;cache&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;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getMarketData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://your-api.com/analyze?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React's &lt;code&gt;cache()&lt;/code&gt; function memoizes the result per request. Call it from ten different components in the same render, it executes once. This is the correct primitive for deduplication in Server Components.&lt;/p&gt;




&lt;h3&gt;
  
  
  The general rule
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;cache: 'no-store'&lt;/code&gt; doesn't mean "always fresh per user session." It means "always fresh per execution." In a Server Component, those are not the same thing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Use &lt;code&gt;cache: 'no-store'&lt;/code&gt; only when you understand exactly how many times that component will execute per request, &lt;br&gt;
and that number is one. Otherwise, use &lt;code&gt;revalidate&lt;/code&gt;, route-level config, or &lt;code&gt;React.cache()&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;The fetch API in Next.js Server Components looks like the standard browser fetch. It isn't. The caching layer underneath has different semantics, and &lt;code&gt;cache: 'no-store'&lt;/code&gt; removes a safety net you probably didn't know was there. Learn where your deduplication comes from before you opt out of it.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
    <item>
      <title>Supabase .maybeSingle() returns null with multiple rows, and it won't tell you why</title>
      <dc:creator>Victor Caña</dc:creator>
      <pubDate>Tue, 10 Mar 2026 14:31:09 +0000</pubDate>
      <link>https://dev.to/victor_caa_ab4153b4bcf6e/supabase-maybesingle-returns-null-with-multiple-rows-and-it-wont-tell-you-why-mc</link>
      <guid>https://dev.to/victor_caa_ab4153b4bcf6e/supabase-maybesingle-returns-null-with-multiple-rows-and-it-wont-tell-you-why-mc</guid>
      <description>&lt;h3&gt;
  
  
  The problem
&lt;/h3&gt;

&lt;p&gt;You query Supabase with &lt;code&gt;.maybeSingle()&lt;/code&gt;, get &lt;code&gt;null&lt;/code&gt; back, and assume the row doesn't exist. It does. There are actually three of them. Your app just silently moved on.&lt;/p&gt;




&lt;h3&gt;
  
  
  Technical context
&lt;/h3&gt;

&lt;p&gt;When building &lt;a href="https://readytorelease.app" rel="noopener noreferrer"&gt;ReadyToRelease&lt;/a&gt;, I had a query that checked whether a user already had an active research session before creating a new one. Classic "upsert-like" logic: if it exists, return it; if not, create it.&lt;/p&gt;

&lt;p&gt;I trusted &lt;code&gt;.maybeSingle()&lt;/code&gt; to handle the "maybe it's there, maybe it's not" case cleanly. It does, but only if your data is clean. The moment you have duplicate rows matching your filter, &lt;code&gt;.maybeSingle()&lt;/code&gt; doesn't throw. It doesn't warn. It returns &lt;code&gt;null&lt;/code&gt;, exactly like it would if nothing matched.&lt;/p&gt;

&lt;p&gt;This is documented behavior. But it's the kind of thing you only truly understand after it burns you in production.&lt;/p&gt;




&lt;h3&gt;
  
  
  The broken code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;research_sessions&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&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="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&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;active&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="nf"&gt;maybeSingle&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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Assumes: no active session exists → create one&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createNewSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&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;p&gt;This looks reasonable. But if the user somehow ended up with two &lt;code&gt;active&lt;/code&gt; rows (a race condition, a bad migration, a test script you forgot to clean up), &lt;code&gt;data&lt;/code&gt; comes back as &lt;code&gt;null&lt;/code&gt;, and you create &lt;em&gt;another&lt;/em&gt; session on top of the existing duplicates.&lt;/p&gt;

&lt;p&gt;No error. No warning. Just &lt;code&gt;null&lt;/code&gt; and a silent cascade.&lt;/p&gt;




&lt;h3&gt;
  
  
  What's actually happening under the hood
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.maybeSingle()&lt;/code&gt; is designed to return:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The row, if &lt;strong&gt;exactly one&lt;/strong&gt; matches&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;null&lt;/code&gt;, if &lt;strong&gt;zero&lt;/strong&gt; match&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;null&lt;/code&gt; + an error, if &lt;strong&gt;more than one&lt;/strong&gt; match &lt;strong&gt;but only in some versions and configurations&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The catch: in Supabase JS v2, the behavior when multiple rows match changed subtly. Instead of always surfacing a &lt;code&gt;PGRST116&lt;/code&gt; error, under certain query patterns it silently collapses to &lt;code&gt;null&lt;/code&gt;. If you're not explicitly checking &lt;code&gt;error&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; validating that &lt;code&gt;null&lt;/code&gt; actually means "not found", you're flying blind.&lt;/p&gt;




&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Two layers of defense:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Always check the error, even when data is null:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;research_sessions&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&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="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&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;active&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="nf"&gt;maybeSingle&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="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Could be PGRST116, multiple rows found&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unexpected query result:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ambiguous session state, manual review needed&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="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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createNewSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&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;p&gt;&lt;strong&gt;2. If you need true single-row safety, use a count check first:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;countError&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;research_sessions&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&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;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;head&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="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&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;active&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="nx"&gt;countError&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Expected 1 active session, found &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;research_sessions&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&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="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&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;active&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="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// safe now&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, it's two queries. For critical paths, it's worth it.&lt;/p&gt;




&lt;h3&gt;
  
  
  The general rule
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;null&lt;/code&gt; from &lt;code&gt;.maybeSingle()&lt;/code&gt; means "zero or ambiguous", not "definitely zero".&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Treat it like you'd treat an HTTP 200 with an empty body: don't assume it means what you think it means without checking everything around it. Always inspect &lt;code&gt;error&lt;/code&gt;. Always add a database-level unique constraint if business logic requires exactly one row per user/status combination.&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;-- The real fix lives here&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;one_active_session_per_user&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;research_sessions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&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="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;uploads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amazonaws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;uploads&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cypnd64lif2ughooz9zf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;png&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That index makes the problem impossible at the data layer, which is where it belongs.&lt;/p&gt;




&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.maybeSingle()&lt;/code&gt; is not broken, your assumption about what &lt;code&gt;null&lt;/code&gt; means is. Add the constraint, check the error, and never let your application logic be the only thing enforcing uniqueness.&lt;/p&gt;




</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>supabase</category>
    </item>
    <item>
      <title>Supabase .maybeSingle() returns null with multiple rows, and it won't tell you why</title>
      <dc:creator>Victor Caña</dc:creator>
      <pubDate>Tue, 10 Mar 2026 14:31:09 +0000</pubDate>
      <link>https://dev.to/victor_caa_ab4153b4bcf6e/supabase-maybesingle-returns-null-with-multiple-rows-and-it-wont-tell-you-why-k85</link>
      <guid>https://dev.to/victor_caa_ab4153b4bcf6e/supabase-maybesingle-returns-null-with-multiple-rows-and-it-wont-tell-you-why-k85</guid>
      <description>&lt;h3&gt;
  
  
  The problem
&lt;/h3&gt;

&lt;p&gt;You query Supabase with &lt;code&gt;.maybeSingle()&lt;/code&gt;, get &lt;code&gt;null&lt;/code&gt; back, and assume the row doesn't exist. It does. There are actually three of them. Your app just silently moved on.&lt;/p&gt;




&lt;h3&gt;
  
  
  Technical context
&lt;/h3&gt;

&lt;p&gt;When building &lt;a href="https://readytorelease.app" rel="noopener noreferrer"&gt;ReadyToRelease&lt;/a&gt;, I had a query that checked whether a user already had an active research session before creating a new one. Classic "upsert-like" logic: if it exists, return it; if not, create it.&lt;/p&gt;

&lt;p&gt;I trusted &lt;code&gt;.maybeSingle()&lt;/code&gt; to handle the "maybe it's there, maybe it's not" case cleanly. It does, but only if your data is clean. The moment you have duplicate rows matching your filter, &lt;code&gt;.maybeSingle()&lt;/code&gt; doesn't throw. It doesn't warn. It returns &lt;code&gt;null&lt;/code&gt;, exactly like it would if nothing matched.&lt;/p&gt;

&lt;p&gt;This is documented behavior. But it's the kind of thing you only truly understand after it burns you in production.&lt;/p&gt;




&lt;h3&gt;
  
  
  The broken code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;research_sessions&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&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="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&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;active&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="nf"&gt;maybeSingle&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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Assumes: no active session exists → create one&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createNewSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&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;p&gt;This looks reasonable. But if the user somehow ended up with two &lt;code&gt;active&lt;/code&gt; rows (a race condition, a bad migration, a test script you forgot to clean up), &lt;code&gt;data&lt;/code&gt; comes back as &lt;code&gt;null&lt;/code&gt;, and you create &lt;em&gt;another&lt;/em&gt; session on top of the existing duplicates.&lt;/p&gt;

&lt;p&gt;No error. No warning. Just &lt;code&gt;null&lt;/code&gt; and a silent cascade.&lt;/p&gt;




&lt;h3&gt;
  
  
  What's actually happening under the hood
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.maybeSingle()&lt;/code&gt; is designed to return:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The row, if &lt;strong&gt;exactly one&lt;/strong&gt; matches&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;null&lt;/code&gt;, if &lt;strong&gt;zero&lt;/strong&gt; match&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;null&lt;/code&gt; + an error, if &lt;strong&gt;more than one&lt;/strong&gt; match &lt;strong&gt;but only in some versions and configurations&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The catch: in Supabase JS v2, the behavior when multiple rows match changed subtly. Instead of always surfacing a &lt;code&gt;PGRST116&lt;/code&gt; error, under certain query patterns it silently collapses to &lt;code&gt;null&lt;/code&gt;. If you're not explicitly checking &lt;code&gt;error&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; validating that &lt;code&gt;null&lt;/code&gt; actually means "not found", you're flying blind.&lt;/p&gt;




&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Two layers of defense:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Always check the error, even when data is null:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;research_sessions&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&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="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&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;active&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="nf"&gt;maybeSingle&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="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Could be PGRST116, multiple rows found&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unexpected query result:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ambiguous session state, manual review needed&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="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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createNewSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&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;p&gt;&lt;strong&gt;2. If you need true single-row safety, use a count check first:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;countError&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;research_sessions&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&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;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;head&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="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&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;active&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="nx"&gt;countError&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Expected 1 active session, found &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;research_sessions&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&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="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&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;active&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="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// safe now&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, it's two queries. For critical paths, it's worth it.&lt;/p&gt;




&lt;h3&gt;
  
  
  The general rule
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;null&lt;/code&gt; from &lt;code&gt;.maybeSingle()&lt;/code&gt; means "zero or ambiguous", not "definitely zero".&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Treat it like you'd treat an HTTP 200 with an empty body: don't assume it means what you think it means without checking everything around it. Always inspect &lt;code&gt;error&lt;/code&gt;. Always add a database-level unique constraint if business logic requires exactly one row per user/status combination.&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;-- The real fix lives here&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;one_active_session_per_user&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;research_sessions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&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="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;uploads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amazonaws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;uploads&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cypnd64lif2ughooz9zf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;png&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That index makes the problem impossible at the data layer, which is where it belongs.&lt;/p&gt;




&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.maybeSingle()&lt;/code&gt; is not broken, your assumption about what &lt;code&gt;null&lt;/code&gt; means is. Add the constraint, check the error, and never let your application logic be the only thing enforcing uniqueness.&lt;/p&gt;




</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>supabase</category>
    </item>
    <item>
      <title>I broke my SaaS with one line of code -&gt; 776,000 function invocations and account paused</title>
      <dc:creator>Victor Caña</dc:creator>
      <pubDate>Wed, 04 Mar 2026 19:16:30 +0000</pubDate>
      <link>https://dev.to/victor_caa_ab4153b4bcf6e/i-broke-my-saas-with-one-line-of-code-776000-function-invocations-and-account-paused-5beb</link>
      <guid>https://dev.to/victor_caa_ab4153b4bcf6e/i-broke-my-saas-with-one-line-of-code-776000-function-invocations-and-account-paused-5beb</guid>
      <description>&lt;p&gt;Three months ago, ReadyToRelease had its peak user count.&lt;br&gt;
Then I broke everything with one line of code.&lt;/p&gt;

&lt;p&gt;Here's the full story.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is ReadyToRelease
&lt;/h2&gt;

&lt;p&gt;An AI tool that generates market research reports in 90 seconds:&lt;br&gt;
TAM/SAM/SOM, competitor analysis, SWOT, and implementation roadmap.&lt;br&gt;
One-time $3 payment. No subscription.&lt;/p&gt;

&lt;p&gt;Stack: Next.js 14 + Groq + Supabase + Stripe&lt;/p&gt;
&lt;h2&gt;
  
  
  The bug that caused 776,000 function invocations
&lt;/h2&gt;

&lt;p&gt;In my reports page, I had this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const res = await fetch(
  `${process.env.NEXT_PUBLIC_BASE_URL}/api/generate-report?id=${params.id}`, 
  { cache: 'no-store' }
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fetch was inside a Next.js Server Component with &lt;br&gt;
cache: 'no-store'.&lt;/p&gt;

&lt;p&gt;Every time someone visited a report page, this triggered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A full call to /api/generate-report&lt;/li&gt;
&lt;li&gt;Which launched 9 parallel scrapers inside Promise.allSettled()&lt;/li&gt;
&lt;li&gt;Which called Supabase, GitHub API, Reddit API, and 5 VC firm scrapers&lt;/li&gt;
&lt;li&gt;Every. Single. Visit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: 776,000 Vercel function invocations in 3 weeks.&lt;br&gt;
Vercel account paused. Product completely down.&lt;/p&gt;

&lt;p&gt;The fix was removing 9 lines of code. The report data was already &lt;br&gt;
being read directly from Supabase above: the fetch was completely &lt;br&gt;
unnecessary.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Supabase egress problem
&lt;/h2&gt;

&lt;p&gt;At the same time, I had this in my dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { data: reportsData } = await supabase
  .from("reports")
  .select(`
    id, title, market_data, competitors, 
    trends, swot_analysis, financial_projections
  `)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I was loading full JSONB fields (150KB+ each) for a list view &lt;br&gt;
that only needed id, title, and created_at.&lt;/p&gt;

&lt;p&gt;20 reports × 150KB = 3MB per dashboard visit.&lt;br&gt;
Result: 19GB of Supabase egress on the free tier in 3 weeks.&lt;br&gt;
Account restricted. 402 errors everywhere.&lt;/p&gt;

&lt;p&gt;Fix: select only the fields you actually need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { data: reportsData } = await supabase
  .from("reports")
  .select("id, title, created_at, status, business_model")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The webhook that didn't exist
&lt;/h2&gt;

&lt;p&gt;I built the entire Stripe payment flow but the webhook endpoint &lt;br&gt;
file was in the wrong path.&lt;/p&gt;

&lt;p&gt;My code pointed to: /api/stripe/webhook&lt;br&gt;
My file was at: /api/stripe-webhook/route.ts&lt;/p&gt;

&lt;p&gt;Every payment succeeded on Stripe. &lt;br&gt;
Nothing was recorded in the database.&lt;br&gt;
Users paid and got nothing.&lt;/p&gt;

&lt;p&gt;Fix: stripe listen --forward-to localhost:3000/api/stripe-webhook&lt;/p&gt;
&lt;h2&gt;
  
  
  The .maybeSingle() that wasn't
&lt;/h2&gt;

&lt;p&gt;After migrating to a new Supabase project, users who had &lt;br&gt;
multiple rows in report_payments were getting blocked.&lt;/p&gt;

&lt;p&gt;The query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { data: payment } = await supabaseAdmin
  .from("report_payments")
  .select("id")
  .eq("user_id", user.id)
  .maybeSingle() // fails silently with multiple rows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When maybeSingle() finds multiple rows it returns null.&lt;br&gt;
So !payment was true and the endpoint returned 402.&lt;/p&gt;

&lt;p&gt;Fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { data: payments } = await supabaseAdmin
  .from("report_payments")
  .select("id")
  .eq("user_id", user.id)
  .limit(1)

if (!payments || payments.length === 0) {
  return NextResponse.json({ error: "Payment required" }, 
  { status: 402 })
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I rebuilt in 3 months of silence
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Migrated to a new Supabase project (old one hit egress limits)&lt;/li&gt;
&lt;li&gt;Fixed all payment verification bugs&lt;/li&gt;
&lt;li&gt;Removed ReactFlow from bundle (it was imported but never used — 
dead code adding ~150kB)&lt;/li&gt;
&lt;li&gt;Added dynamic imports for heavy components&lt;/li&gt;
&lt;li&gt;Added rate limiting (max 3 reports/hour per user)&lt;/li&gt;
&lt;li&gt;Added skeleton loaders on dashboard&lt;/li&gt;
&lt;li&gt;Added /api/health endpoint + UptimeRobot monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The unit economics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AI cost per report: $0.0125 (Groq, 11 LLM calls)&lt;/li&gt;
&lt;li&gt;Price: $3&lt;/li&gt;
&lt;li&gt;Margin: 240x&lt;/li&gt;
&lt;li&gt;Infrastructure cost: $0/month (free tiers)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where I am now
&lt;/h2&gt;

&lt;p&gt;Product is back live: readytorelease.online&lt;br&gt;
Demo (no signup needed): readytorelease.online/demo&lt;br&gt;
0 paying customers. Relaunching today.&lt;/p&gt;

&lt;p&gt;If you're building with Next.js + Supabase, the main lessons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Never use cache: 'no-store' in Server Components without 
understanding what it triggers&lt;/li&gt;
&lt;li&gt;Always select specific fields, never .select("*") on heavy tables&lt;/li&gt;
&lt;li&gt;Test your webhook path before going live&lt;/li&gt;
&lt;li&gt;.maybeSingle() returns null for multiple rows — use .limit(1) instead&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Happy to answer questions about the stack or any of the bugs.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>saas</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
