<?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: Mahdi BEN RHOUMA</title>
    <description>The latest articles on DEV Community by Mahdi BEN RHOUMA (@mahdi_benrhouma_fe1c6005).</description>
    <link>https://dev.to/mahdi_benrhouma_fe1c6005</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%2F3623634%2F405ea568-6e7f-4cab-923a-240f9a44bf72.png</url>
      <title>DEV Community: Mahdi BEN RHOUMA</title>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mahdi_benrhouma_fe1c6005"/>
    <language>en</language>
    <item>
      <title>Test Supabase RLS Policies Before You Ship</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Mon, 24 Aug 2026 21:19:33 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/test-supabase-rls-policies-before-you-ship-3c77</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/test-supabase-rls-policies-before-you-ship-3c77</guid>
      <description>&lt;p&gt;There is a specific way RLS bugs reach production, and it is almost never that&lt;br&gt;
the developer forgot to write a policy. It is that the policy was tested in a&lt;br&gt;
context that does not enforce it.&lt;/p&gt;

&lt;p&gt;Three contexts, all of them defaults:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;Supabase SQL editor&lt;/strong&gt; runs your query as a privileged role.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;service key&lt;/strong&gt; you use in server-side code carries &lt;code&gt;BYPASSRLS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;owner of the table&lt;/strong&gt; — the role your migrations run as — is exempt from
its own policies unless you explicitly say otherwise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In all three, &lt;code&gt;select * from documents&lt;/code&gt; returns every row whether your policy&lt;br&gt;
is correct, inverted, or absent. Then the same query runs from a browser with&lt;br&gt;
an &lt;code&gt;anon&lt;/code&gt; key and returns nothing, or worse, returns everything.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Postgres skips your policy
&lt;/h2&gt;

&lt;p&gt;Row level security has two exemptions built into the engine, and they exist for&lt;br&gt;
good operational reasons — backups and migrations would be miserable otherwise.&lt;/p&gt;

&lt;p&gt;The first is the &lt;strong&gt;superuser exemption&lt;/strong&gt;: any role with &lt;code&gt;rolsuper&lt;/code&gt;, and any role&lt;br&gt;
with the &lt;code&gt;BYPASSRLS&lt;/code&gt; attribute, skips policy evaluation entirely. Supabase&lt;br&gt;
creates &lt;code&gt;service_role&lt;/code&gt; with exactly that attribute, which is the whole reason&lt;br&gt;
server-side code with the service key can read across all tenants. It is also&lt;br&gt;
why "it works with the service key" is not a passing test — see&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/supabase-service-role-key-nextjs" rel="noopener noreferrer"&gt;service_role bypasses RLS&lt;/a&gt; for what&lt;br&gt;
that key can reach and how to contain it.&lt;/p&gt;

&lt;p&gt;The second is the &lt;strong&gt;owner exemption&lt;/strong&gt;: the role that owns a table is not subject&lt;br&gt;
to that table's policies by default. This one surprises people, because it means&lt;br&gt;
the connection your migration tool uses, and the connection most SQL consoles&lt;br&gt;
use, sees straight through RLS.&lt;/p&gt;

&lt;p&gt;You turn the owner exemption off per 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;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;documents&lt;/span&gt; &lt;span class="k"&gt;force&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FORCE&lt;/code&gt; is a table-level setting, not a session one. It does not affect&lt;br&gt;
superusers or &lt;code&gt;BYPASSRLS&lt;/code&gt; roles — nothing does. To escape those, you have to&lt;br&gt;
stop being them.&lt;/p&gt;
&lt;h2&gt;
  
  
  The procedure
&lt;/h2&gt;

&lt;p&gt;Four statements. Everything else is detail.&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;begin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 1. Feed the JWT the policy will read. `true` = transaction-local.&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;set_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'request.jwt.claims'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'{"sub":"11111111-1111-1111-1111-111111111111","role":"authenticated"}'&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;span class="c1"&gt;-- 2. Stop being the owner.&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="k"&gt;local&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. Run the exact query your client runs.&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;documents&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;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;rollback&lt;/code&gt; matters more than it looks. It discards the role change, the&lt;br&gt;
claims, and anything the statement wrote — which is what lets you test an&lt;br&gt;
&lt;code&gt;INSERT&lt;/code&gt; repeatedly against identical seed data instead of accumulating junk&lt;br&gt;
rows between runs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;auth.uid()&lt;/code&gt; is not magic. In a Supabase project it is a small SQL function that&lt;br&gt;
reads &lt;code&gt;request.jwt.claims&lt;/code&gt; and pulls &lt;code&gt;sub&lt;/code&gt; out of it. Setting that GUC by hand is&lt;br&gt;
precisely what PostgREST does for you when a request arrives with a bearer token,&lt;br&gt;
which is why this test reproduces the real path rather than approximating it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Run it as more than one person
&lt;/h3&gt;

&lt;p&gt;This is the step almost everyone skips, and it is the one that catches actual&lt;br&gt;
leaks.&lt;/p&gt;

&lt;p&gt;A policy like &lt;code&gt;using (true)&lt;/code&gt; passes a single-user test perfectly: you are signed&lt;br&gt;
in, you asked for your rows, you got rows. The test only fails when a &lt;em&gt;second&lt;/em&gt;&lt;br&gt;
identity runs the same query and gets the &lt;em&gt;same&lt;/em&gt; rows back.&lt;/p&gt;

&lt;p&gt;So run the block above at least three times — once with no claims and&lt;br&gt;
&lt;code&gt;set local role anon&lt;/code&gt;, once as user A, once as user B — and compare:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Identity&lt;/th&gt;
&lt;th&gt;Expected on a correctly scoped table&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;anon&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;zero rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;user A&lt;/td&gt;
&lt;td&gt;only A's rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;user B&lt;/td&gt;
&lt;td&gt;only B's rows, and not A's&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;service_role&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;everything, always — this is not a signal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If A and B see the same non-empty set, you have a cross-tenant leak. If both see&lt;br&gt;
zero, you have the silent failure covered in&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/supabase-rls-silent-failures-debug" rel="noopener noreferrer"&gt;why RLS returns zero rows with no error&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Doing all four at once
&lt;/h2&gt;

&lt;p&gt;Typing that transaction four times per policy change is why the step gets&lt;br&gt;
skipped, so I built the tool I wanted: the&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/tools/supabase-rls-playground" rel="noopener noreferrer"&gt;Supabase RLS Playground&lt;/a&gt; boots a real&lt;br&gt;
PostgreSQL engine compiled to WebAssembly inside the browser tab, applies your&lt;br&gt;
schema and your &lt;code&gt;CREATE POLICY&lt;/code&gt; statements, and runs one query four times — as&lt;br&gt;
&lt;code&gt;anon&lt;/code&gt;, as two different signed-in users, and as &lt;code&gt;service_role&lt;/code&gt; — showing the&lt;br&gt;
four result sets side by side.&lt;/p&gt;

&lt;p&gt;It does the two things this article is about, because otherwise it would lie to&lt;br&gt;
you the same way the SQL editor does: every persona query runs under &lt;code&gt;SET LOCAL&lt;br&gt;
ROLE&lt;/code&gt;, and every RLS-enabled table gets &lt;code&gt;FORCE ROW LEVEL SECURITY&lt;/code&gt; before the&lt;br&gt;
first query runs. The &lt;code&gt;auth&lt;/code&gt; schema is shimmed the way Supabase defines it —&lt;br&gt;
&lt;code&gt;auth.uid()&lt;/code&gt;, &lt;code&gt;auth.jwt()&lt;/code&gt;, &lt;code&gt;auth.role()&lt;/code&gt;, &lt;code&gt;auth.email()&lt;/code&gt; — so a policy pasted&lt;br&gt;
straight out of your project runs verbatim.&lt;/p&gt;

&lt;p&gt;Nothing is uploaded; there is no server component to upload it to. Once the&lt;br&gt;
WebAssembly build is cached the page works offline.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the measurements look like
&lt;/h2&gt;

&lt;p&gt;Two experiments against a real Postgres engine, because the two exemptions&lt;br&gt;
behave differently and the difference decides which flag you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The owner exemption, isolated.&lt;/strong&gt; A table owned by an ordinary non-superuser&lt;br&gt;
role, RLS enabled, and a policy deliberately written to match nothing —&lt;br&gt;
&lt;code&gt;using (user_id = 'nobody')&lt;/code&gt;. Queried by the owner:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Rows returned to the owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;enable row level security&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2 of 2&lt;/strong&gt; — the policy is skipped entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;+ force row level security&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0&lt;/strong&gt; — the policy applies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A policy that blocks everything returns everything, until &lt;code&gt;FORCE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four identities, on a normal policy.&lt;/strong&gt; Three rows in &lt;code&gt;documents&lt;/code&gt;, two&lt;br&gt;
owned by user A and one by user B, with the textbook rule&lt;br&gt;
&lt;code&gt;for select to authenticated using ((select auth.uid()) = user_id)&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Identity&lt;/th&gt;
&lt;th&gt;Rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;anon&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;user A&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;user B&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;service_role&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the superuser connection, no &lt;code&gt;SET ROLE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same policy, same data, five different answers depending only on who asked. The&lt;br&gt;
last two rows of that table are the ones you get by default from a SQL console&lt;br&gt;
and from server-side code holding the service key — and they are the two that&lt;br&gt;
tell you nothing, because neither of them ever consulted the policy.&lt;/p&gt;

&lt;p&gt;Note which flag fixes which: &lt;code&gt;FORCE&lt;/code&gt; removes the &lt;em&gt;owner&lt;/em&gt; exemption, and nothing&lt;br&gt;
removes the &lt;em&gt;superuser&lt;/em&gt; exemption. To escape that one you have to stop being a&lt;br&gt;
superuser, which is what &lt;code&gt;SET LOCAL ROLE&lt;/code&gt; is for. You need both.&lt;/p&gt;
&lt;h2&gt;
  
  
  Studio impersonation is closer, but it is still not production
&lt;/h2&gt;

&lt;p&gt;Supabase Studio has a role-impersonation feature in the SQL editor, and it is a&lt;br&gt;
genuine improvement over running as the owner. Treat it as a good approximation&lt;br&gt;
rather than a proof, because it is reproducing production behaviour rather than&lt;br&gt;
being it.&lt;/p&gt;

&lt;p&gt;A concrete example: &lt;a href="https://github.com/supabase/supabase/issues/27841" rel="noopener noreferrer"&gt;supabase/supabase#27841&lt;/a&gt;&lt;br&gt;
reported that impersonation did not invoke the project's Custom Access Token&lt;br&gt;
Hook, so claims the hook adds to &lt;code&gt;app_metadata&lt;/code&gt; were missing from&lt;br&gt;
&lt;code&gt;select auth.jwt()&lt;/code&gt; during impersonation while being present in production. That&lt;br&gt;
specific gap was addressed in a later pull request — the durable lesson is not&lt;br&gt;
that one bug existed, but that the JWT your test sees and the JWT your users&lt;br&gt;
carry are assembled by different code paths, and any policy keyed on a custom&lt;br&gt;
claim is only as trustworthy as the claim you fed it.&lt;/p&gt;

&lt;p&gt;Which is also the argument for setting &lt;code&gt;request.jwt.claims&lt;/code&gt; explicitly, as above:&lt;br&gt;
you know exactly what the policy read, because you wrote it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Put it in CI
&lt;/h2&gt;

&lt;p&gt;Everything here runs in plain SQL, so it belongs in a migration test rather than&lt;br&gt;
in your memory. The shape that works:&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;begin&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;set_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'request.jwt.claims'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{"sub":"...A...","role":"authenticated"}'&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;span class="k"&gt;set&lt;/span&gt; &lt;span class="k"&gt;local&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;-- Fails the migration if the policy ever stops isolating users.&lt;/span&gt;
  &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
  &lt;span class="k"&gt;declare&lt;/span&gt; &lt;span class="n"&gt;visible&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;count&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;into&lt;/span&gt; &lt;span class="n"&gt;visible&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'...A...'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="n"&gt;visible&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
      &lt;span class="n"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="s1"&gt;'RLS leak: user A can see % foreign rows'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visible&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A policy change that reintroduces a leak then fails a deploy instead of failing a&lt;br&gt;
customer. For the wider set of things worth asserting before a release, the&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/guides/supabase-rls-policy-not-working-debug-checklist" rel="noopener noreferrer"&gt;RLS debug checklist&lt;/a&gt;&lt;br&gt;
covers the GRANTs, the session plumbing and the cache behaviour that sit around&lt;br&gt;
the policies, and&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/guides/supabase-rls-policy-design-patterns" rel="noopener noreferrer"&gt;RLS policy design patterns&lt;/a&gt; covers&lt;br&gt;
the shapes that survive multi-tenancy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Testing an RLS policy means answering one question: &lt;em&gt;what does someone who is not&lt;br&gt;
me get back?&lt;/em&gt; Any context that answers with your own privileges — the SQL editor,&lt;br&gt;
the service key, the table owner — has not answered it.&lt;/p&gt;

&lt;p&gt;Two flags and one transaction fix that. And if the policy you are testing throws&lt;br&gt;
&lt;code&gt;infinite recursion detected in policy for relation&lt;/code&gt;, that is a different&lt;br&gt;
problem with a known shape:&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/supabase-rls-infinite-recursion-fix" rel="noopener noreferrer"&gt;fix infinite recursion in a Supabase policy&lt;/a&gt;.&lt;br&gt;
For everything else that goes wrong on the way,&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/debugging-supabase-rls-issues" rel="noopener noreferrer"&gt;debugging Supabase RLS issues&lt;/a&gt; walks the&lt;br&gt;
full failure catalogue.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.iloveblogs.blog/post/test-supabase-rls-policies-before-you-ship" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>rls</category>
      <category>security</category>
    </item>
    <item>
      <title>Supabase 42501: permission denied for schema auth</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Mon, 24 Aug 2026 21:18:51 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/supabase-42501-permission-denied-for-schema-auth-1m5h</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/supabase-42501-permission-denied-for-schema-auth-1m5h</guid>
      <description>&lt;p&gt;Three different attempts, one error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: 42501: permission denied for schema auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A client-side &lt;code&gt;supabase.from('users')&lt;/code&gt; pointed at the wrong schema. A view you&lt;br&gt;
created over &lt;code&gt;auth.users&lt;/code&gt; that works in the SQL editor and 42501s from the app.&lt;br&gt;
A row level security policy that joins &lt;code&gt;auth.users&lt;/code&gt; to check something about the&lt;br&gt;
caller. All the same underlying refusal, and all fixed the same way — which is&lt;br&gt;
not the way most search results suggest.&lt;/p&gt;
&lt;h2&gt;
  
  
  This is a boundary, not an oversight
&lt;/h2&gt;

&lt;p&gt;Supabase's &lt;a href="https://supabase.com/docs/guides/troubleshooting/database-api-42501-errors" rel="noopener noreferrer"&gt;troubleshooting guide for 42501 errors on the database API&lt;/a&gt;&lt;br&gt;
is explicit: the managed schemas — &lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;vault&lt;/code&gt; — cannot be accessed directly&lt;br&gt;
through the data API for security reasons, and are reachable only through&lt;br&gt;
security definer functions.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;auth&lt;/code&gt; schema holds &lt;code&gt;auth.users&lt;/code&gt;, which contains every account's email,&lt;br&gt;
phone number, hashed password, provider identities, and raw metadata. The API&lt;br&gt;
roles are the ones your browser key resolves to. Keeping one out of reach of the&lt;br&gt;
other is the point.&lt;/p&gt;

&lt;p&gt;That is why this error behaves differently from its more familiar sibling. When&lt;br&gt;
&lt;code&gt;anon&lt;/code&gt; loses its grants on the &lt;code&gt;public&lt;/code&gt; schema you have a broken project, and&lt;br&gt;
restoring the grants is the fix — that case is&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/supabase-client-permission-denied-for-schema-public" rel="noopener noreferrer"&gt;permission denied for schema public&lt;/a&gt;.&lt;br&gt;
When &lt;code&gt;anon&lt;/code&gt; is refused on &lt;code&gt;auth&lt;/code&gt;, the project is behaving correctly.&lt;/p&gt;

&lt;p&gt;So the tempting one-liner:&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;-- Do not do this.&lt;/span&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;auth&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;p&gt;does make the message disappear. It also means the only thing standing between a&lt;br&gt;
key published in your JavaScript bundle and the user table is whatever table&lt;br&gt;
grants remain. Every subsequent mistake — a permissive policy, a view, a&lt;br&gt;
function that forgets to filter — becomes a full account dump.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where each failure actually comes from
&lt;/h2&gt;
&lt;h3&gt;
  
  
  A client query against auth.users
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;supabase.from('users')&lt;/code&gt; resolves against the exposed schema (&lt;code&gt;public&lt;/code&gt; by&lt;br&gt;
default), so it usually fails with &lt;code&gt;relation "public.users" does not exist&lt;/code&gt;&lt;br&gt;
rather than 42501. Explicitly reaching for the auth schema — through&lt;br&gt;
&lt;code&gt;.schema('auth')&lt;/code&gt;, a Postgres view, or raw SQL over a direct connection — is what&lt;br&gt;
produces the permission error.&lt;/p&gt;

&lt;p&gt;There is no configuration that makes this work safely. Move the data instead.&lt;/p&gt;
&lt;h3&gt;
  
  
  A view over auth.users
&lt;/h3&gt;

&lt;p&gt;This one deserves care, because the intuitive version of the story is backwards&lt;br&gt;
and the backwards version is the dangerous 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="k"&gt;view&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;user_emails&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt;
  &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;from&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;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL's &lt;strong&gt;default for views is definer semantics&lt;/strong&gt;: the view's underlying&lt;br&gt;
query is checked against the &lt;em&gt;view owner's&lt;/em&gt; privileges, not the caller's. So this&lt;br&gt;
view, created by a privileged role, does not 42501 when &lt;code&gt;authenticated&lt;/code&gt; selects&lt;br&gt;
from it. It works. It hands out every email address in your project through the&lt;br&gt;
data API, and the auth schema restriction you were relying on is simply routed&lt;br&gt;
around.&lt;/p&gt;

&lt;p&gt;I verified both branches against a real Postgres engine — a table in a schema the&lt;br&gt;
calling role has no &lt;code&gt;USAGE&lt;/code&gt; on, exposed through two views:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;View&lt;/th&gt;
&lt;th&gt;Declared&lt;/th&gt;
&lt;th&gt;Result as the API role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;v_default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;nothing (definer, the default)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 rows returned&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;v_invoker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;with (security_invoker = true)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;permission denied for table users&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Supabase's own database linter flags exactly this as&lt;br&gt;
&lt;a href="https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view" rel="noopener noreferrer"&gt;0010_security_definer_view&lt;/a&gt;,&lt;br&gt;
noting that Postgres's default setting for views is &lt;code&gt;SECURITY DEFINER&lt;/code&gt;, which&lt;br&gt;
means they use the permissions of the view's creator rather than the querying&lt;br&gt;
user — and recommending &lt;code&gt;with (security_invoker = on)&lt;/code&gt; so RLS applies normally.&lt;/p&gt;

&lt;p&gt;Which reframes the error. If your view over &lt;code&gt;auth.users&lt;/code&gt; returns 42501, it is&lt;br&gt;
almost certainly declared &lt;code&gt;security_invoker = true&lt;/code&gt; — that is, it is the&lt;br&gt;
&lt;em&gt;correctly configured&lt;/em&gt; view, and the boundary is doing its job. The fix is not to&lt;br&gt;
remove &lt;code&gt;security_invoker&lt;/code&gt;; that would trade an error message for a data leak.&lt;br&gt;
The fix is one of the two patterns below.&lt;/p&gt;
&lt;h3&gt;
  
  
  A policy that joins auth.users
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 42501 when evaluated as anon/authenticated.&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;"admins can read everything"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;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="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;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="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_app_meta_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'role'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'admin'&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;Policies are evaluated as the calling role. The calling role cannot see the&lt;br&gt;
&lt;code&gt;auth&lt;/code&gt; schema, so the policy itself errors rather than returning false.&lt;/p&gt;

&lt;p&gt;Note the asymmetry that confuses people: &lt;code&gt;auth.uid()&lt;/code&gt;, &lt;code&gt;auth.jwt()&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;auth.role()&lt;/code&gt; are &lt;em&gt;functions&lt;/em&gt; that Supabase grants to the API roles, so calling&lt;br&gt;
them from a policy is fine. Selecting from the &lt;code&gt;auth.users&lt;/code&gt; &lt;em&gt;table&lt;/em&gt; is not. If&lt;br&gt;
you need a claim, read it out of the JWT you already have:&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;using&lt;/span&gt; &lt;span class="p"&gt;(&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;jwt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'app_metadata'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'role'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That version needs no access to the auth schema at all, and it is cheaper —&lt;br&gt;
no subquery per row.&lt;/p&gt;

&lt;p&gt;If you want to see the difference between those two policies actually execute&lt;br&gt;
rather than take my word for it, both run in the&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/tools/supabase-rls-playground" rel="noopener noreferrer"&gt;Supabase RLS Playground&lt;/a&gt;, which applies your&lt;br&gt;
policies against a real Postgres engine in the browser and runs them as &lt;code&gt;anon&lt;/code&gt;,&lt;br&gt;
as two signed-in users and as &lt;code&gt;service_role&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix: a security definer function
&lt;/h2&gt;

&lt;p&gt;When you genuinely need data that only lives in &lt;code&gt;auth.users&lt;/code&gt;, expose the minimum&lt;br&gt;
through a function that runs as its owner:&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="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;replace&lt;/span&gt; &lt;span class="k"&gt;function&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;current_user_email&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;
&lt;span class="k"&gt;language&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt;
&lt;span class="k"&gt;security&lt;/span&gt; &lt;span class="k"&gt;definer&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
&lt;span class="k"&gt;stable&lt;/span&gt;
&lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
  &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;from&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;users&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;id&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="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;revoke&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;function&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;current_user_email&lt;/span&gt;&lt;span class="p"&gt;()&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="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;execute&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;function&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;current_user_email&lt;/span&gt;&lt;span class="p"&gt;()&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;Four things make this safe, and all four matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;security definer&lt;/code&gt;&lt;/strong&gt; — the function body runs with the owner's privileges,
so it can read the auth schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;set search_path = ''&lt;/code&gt;&lt;/strong&gt; — pins resolution so a caller cannot shadow
&lt;code&gt;auth.users&lt;/code&gt; with an object of their own. A security definer function without
a pinned search path is a privilege escalation waiting to be found. Fully
qualify every name in the body when you do this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;where&lt;/code&gt; clause&lt;/strong&gt; — the function returns &lt;em&gt;the caller's&lt;/em&gt; row, not an
arbitrary one. A definer function that takes a user id parameter and returns
that user's email is a lookup oracle for your whole user table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;revoke&lt;/code&gt; then &lt;code&gt;grant execute&lt;/code&gt;&lt;/strong&gt; — you decide who can call it, rather than
inheriting &lt;code&gt;public&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The better fix: stop needing the auth schema
&lt;/h2&gt;

&lt;p&gt;Most applications that reach for &lt;code&gt;auth.users&lt;/code&gt; want a display name, an avatar and&lt;br&gt;
an email next to their own rows. Join a table you own 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;create&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="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;references&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;users&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;cascade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;full_name&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;avatar_url&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt;  &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"read own profile"&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="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;id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Populate it as accounts are created.&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;replace&lt;/span&gt; &lt;span class="k"&gt;function&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;handle_new_user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt;
&lt;span class="k"&gt;language&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;
&lt;span class="k"&gt;security&lt;/span&gt; &lt;span class="k"&gt;definer&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
&lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;avatar_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_user_meta_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'full_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_user_meta_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'avatar_url'&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="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="n"&gt;on_auth_user_created&lt;/span&gt;
  &lt;span class="k"&gt;after&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;on&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;users&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;each&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;execute&lt;/span&gt; &lt;span class="k"&gt;function&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;handle_new_user&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every query your client makes stays inside &lt;code&gt;public&lt;/code&gt;, joins work normally,&lt;br&gt;
RLS applies the way it does everywhere else, and the auth schema stays sealed.&lt;/p&gt;

&lt;p&gt;Two operational notes. The trigger runs inside the signup transaction, so if it&lt;br&gt;
raises, the signup fails with &lt;code&gt;Database error saving new user&lt;/code&gt; — a distinct&lt;br&gt;
failure with its own causes, covered in&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/fix/fix-supabase-database-error-saving-new-user-trigger" rel="noopener noreferrer"&gt;the trigger rollback fix&lt;/a&gt;.&lt;br&gt;
And existing accounts need a one-time backfill; the trigger only fires on new&lt;br&gt;
inserts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Error mentions &lt;strong&gt;schema &lt;code&gt;auth&lt;/code&gt;&lt;/strong&gt; → you are crossing a deliberate boundary.
Never grant your way through it.&lt;/li&gt;
&lt;li&gt;Need a &lt;strong&gt;claim&lt;/strong&gt; (role, tenant, email) → read &lt;code&gt;auth.jwt()&lt;/code&gt;; no auth schema
access required.&lt;/li&gt;
&lt;li&gt;Need a &lt;strong&gt;column&lt;/strong&gt; that only exists on &lt;code&gt;auth.users&lt;/code&gt; → security definer
function, &lt;code&gt;search_path&lt;/code&gt; pinned, scoped to the caller.&lt;/li&gt;
&lt;li&gt;Need it &lt;strong&gt;joined to your own data&lt;/strong&gt; → mirror it into &lt;code&gt;public.profiles&lt;/code&gt; with a
trigger.&lt;/li&gt;
&lt;li&gt;Error mentions &lt;strong&gt;schema &lt;code&gt;public&lt;/code&gt;&lt;/strong&gt; instead → different problem, different fix:
&lt;a href="https://www.iloveblogs.blog/post/supabase-client-permission-denied-for-schema-public" rel="noopener noreferrer"&gt;permission denied for schema public&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Query succeeds but returns &lt;strong&gt;nothing&lt;/strong&gt; → not a permission error at all, that
is &lt;a href="https://www.iloveblogs.blog/post/supabase-rls-silent-failures-debug" rel="noopener noreferrer"&gt;RLS filtering silently&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the wider set of policy shapes that survive multi-tenancy without reaching&lt;br&gt;
into managed schemas,&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/guides/supabase-rls-policy-design-patterns" rel="noopener noreferrer"&gt;RLS policy design patterns&lt;/a&gt; is the&lt;br&gt;
companion piece.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.iloveblogs.blog/post/supabase-permission-denied-for-schema-auth-42501" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>rls</category>
      <category>security</category>
    </item>
    <item>
      <title>Supabase Hangs After onAuthStateChange: The Deadlock</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Mon, 24 Aug 2026 21:18:45 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/supabase-hangs-after-onauthstatechange-the-deadlock-61h</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/supabase-hangs-after-onauthstatechange-the-deadlock-61h</guid>
      <description>&lt;p&gt;The bug report is always the same shape, and it never mentions authentication:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After a while, every Supabase query in the app just hangs. No error, nothing&lt;br&gt;
in the network tab. Reloading fixes it for about an hour.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nothing rejects, so there is nothing to catch. The promise from&lt;br&gt;
&lt;code&gt;supabase.from('profiles').select()&lt;/code&gt; simply never settles. Your &lt;code&gt;try/catch&lt;/code&gt; is&lt;br&gt;
irrelevant, your error boundary never renders, and your loading state stays true&lt;br&gt;
forever.&lt;/p&gt;

&lt;p&gt;If your app calls anything on the Supabase client inside an&lt;br&gt;
&lt;code&gt;onAuthStateChange&lt;/code&gt; callback, that is the cause, and it is documented.&lt;/p&gt;
&lt;h2&gt;
  
  
  The mechanism
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;supabase-js&lt;/code&gt; serialises auth events. When a session changes — sign-in,&lt;br&gt;
sign-out, and the hourly token refresh — the client takes an internal lock,&lt;br&gt;
delivers the event to every subscriber, and releases the lock when the callbacks&lt;br&gt;
have returned.&lt;/p&gt;

&lt;p&gt;Any Supabase call you make needs that same lock, because it has to resolve the&lt;br&gt;
current access token before it can attach it to the request.&lt;/p&gt;

&lt;p&gt;So this:&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="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onAuthStateChange&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;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// waits for a lock this callback is already holding&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;profiles&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;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;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="nf"&gt;setProfile&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="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;…waits, inside the lock, for the lock. The callback cannot return until the&lt;br&gt;
query resolves; the query cannot resolve until the callback returns.&lt;/p&gt;

&lt;p&gt;Supabase's own troubleshooting entry,&lt;br&gt;
&lt;a href="https://supabase.com/docs/guides/troubleshooting/why-is-my-supabase-api-call-not-returning-PGzXw0" rel="noopener noreferrer"&gt;Why is my supabase API call not returning?&lt;/a&gt;,&lt;br&gt;
states it plainly: there is a bug in supabase-js which results in a deadlock if&lt;br&gt;
any async API call is made in &lt;code&gt;onAuthStateChange&lt;/code&gt; code, and if a call is made in&lt;br&gt;
the handler then the next Supabase call anywhere using that client will hang and&lt;br&gt;
not return. The tracking issue is&lt;br&gt;
&lt;a href="https://github.com/supabase/auth-js/issues/762" rel="noopener noreferrer"&gt;supabase/auth-js#762&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That second half is what makes it so hard to attribute. The deadlock does not&lt;br&gt;
stay in the callback. Every later call on that client — a query in a completely&lt;br&gt;
unrelated component, a &lt;code&gt;getSession()&lt;/code&gt; in your middleware helper, a realtime&lt;br&gt;
subscribe — queues behind a lock that will never be released.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why it looks intermittent
&lt;/h2&gt;

&lt;p&gt;Three events run through the same callback: &lt;code&gt;SIGNED_IN&lt;/code&gt;, &lt;code&gt;TOKEN_REFRESHED&lt;/code&gt;, and&lt;br&gt;
&lt;code&gt;INITIAL_SESSION&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Access tokens are short-lived and refreshed automatically, so&lt;br&gt;
&lt;code&gt;TOKEN_REFRESHED&lt;/code&gt; fires roughly every hour for an open tab. An app can therefore&lt;br&gt;
work perfectly through a morning of navigation and then freeze at 11:04 with no&lt;br&gt;
deploy, no config change and no error — which is exactly how it gets&lt;br&gt;
misdiagnosed as a network problem, a Vercel cold start, or a Supabase incident.&lt;/p&gt;

&lt;p&gt;The same timing explains the "reloading fixes it" report: a reload builds a fresh&lt;br&gt;
client with a fresh lock.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Keep the callback synchronous with respect to Supabase. Set your state, then let&lt;br&gt;
the work happen after the callback has returned.&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="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onAuthStateChange&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&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="c1"&gt;// Synchronous: safe, and this is all most apps actually need here.&lt;/span&gt;
  &lt;span class="nf"&gt;setSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&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;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&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="c1"&gt;// Deferred: runs on the next macrotask, after the lock is released.&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&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="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;loadProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;setTimeout(..., 0)&lt;/code&gt; is not a superstition here. It moves the query into a later&lt;br&gt;
task, after &lt;code&gt;onAuthStateChange&lt;/code&gt; has returned and released the lock. A&lt;br&gt;
microtask — &lt;code&gt;Promise.resolve().then(...)&lt;/code&gt;, or just dropping the &lt;code&gt;await&lt;/code&gt; — is not&lt;br&gt;
reliably enough, because microtasks can drain before the synchronous caller&lt;br&gt;
finishes unwinding.&lt;/p&gt;

&lt;p&gt;In React the cleaner version avoids the timer entirely: store the session in&lt;br&gt;
state and let a separate effect do the fetching.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onAuthStateChange&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&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="nf"&gt;setSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// nothing but state&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&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;span class="nf"&gt;useEffect&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="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;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&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;profiles&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;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;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&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;=&amp;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;cancelled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setProfile&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="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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cancelled&lt;/span&gt; &lt;span class="o"&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="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two effects, one job each. The subscription only ever touches React state, and&lt;br&gt;
the query runs in a normal render cycle where nothing holds the auth lock.&lt;/p&gt;
&lt;h3&gt;
  
  
  What still counts as "a Supabase call"
&lt;/h3&gt;

&lt;p&gt;The rule is broader than &lt;code&gt;.from()&lt;/code&gt;. Inside the callback, avoid awaiting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;supabase.from(...)&lt;/code&gt;, &lt;code&gt;.rpc(...)&lt;/code&gt;, &lt;code&gt;.storage.from(...)&lt;/code&gt; — anything that needs a token&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;supabase.auth.getSession()&lt;/code&gt; and &lt;code&gt;supabase.auth.getUser()&lt;/code&gt; — you already have
the session as the second argument; use it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;supabase.auth.refreshSession()&lt;/code&gt;, &lt;code&gt;setSession()&lt;/code&gt;, &lt;code&gt;signOut()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;anything that transitively calls one of the above, including your own
&lt;code&gt;fetchProfile()&lt;/code&gt; helper or a data-layer wrapper&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Calls to your own API routes are fine, as long as they do not funnel back through&lt;br&gt;
the same browser client.&lt;/p&gt;
&lt;h2&gt;
  
  
  Confirming it is this and not something else
&lt;/h2&gt;

&lt;p&gt;Two checks, thirty seconds each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One.&lt;/strong&gt; Comment out the body of your &lt;code&gt;onAuthStateChange&lt;/code&gt; callback, leaving only&lt;br&gt;
a &lt;code&gt;console.log&lt;/code&gt;. If the hangs stop, you have your answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two.&lt;/strong&gt; Instrument the boundary — if the first line logs and the second does&lt;br&gt;
not, the query never settled:&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="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onAuthStateChange&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;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[auth] callback in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&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;profiles&lt;/span&gt;&lt;span class="dl"&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&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;limit&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[auth] callback out&lt;/span&gt;&lt;span class="dl"&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="c1"&gt;// never prints when deadlocked&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A hang with &lt;code&gt;in&lt;/code&gt; and no &lt;code&gt;out&lt;/code&gt; is a deadlock. An error in between is a different&lt;br&gt;
bug — probably RLS or a missing session, and&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/debugging-supabase-rls-issues" rel="noopener noreferrer"&gt;debugging Supabase RLS issues&lt;/a&gt; covers that&lt;br&gt;
path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related failures that look identical from the outside
&lt;/h2&gt;

&lt;p&gt;"Supabase stopped working after a while" has a small family of causes, and they&lt;br&gt;
are worth ruling out in order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The session is not being persisted or refreshed across navigations —
&lt;a href="https://www.iloveblogs.blog/post/fix-supabase-auth-session-persistence" rel="noopener noreferrer"&gt;fix Supabase auth session persistence&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Cookies are set in the wrong place for the App Router, so the server and the
client disagree about who is signed in —
&lt;a href="https://www.iloveblogs.blog/guides/supabase-auth-complete-session-middleware-guide" rel="noopener noreferrer"&gt;the complete session and middleware guide&lt;/a&gt;
and &lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-ssr-session-management" rel="noopener noreferrer"&gt;SSR session management&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The redirect after sign-in lands on a URL that never receives the session —
&lt;a href="https://www.iloveblogs.blog/post/supabase-auth-redirect-fix" rel="noopener noreferrer"&gt;Supabase auth redirect fix&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The console is full of the &lt;code&gt;getUser()&lt;/code&gt; security warning, which is a different
message about a different mistake —
&lt;a href="https://www.iloveblogs.blog/fix/fix-supabase-getuser-warning" rel="noopener noreferrer"&gt;fix the Supabase getUser warning&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The deadlock is the one that produces silence rather than a message. If nothing&lt;br&gt;
is logged, nothing is thrown, and nothing appears in the network tab, look at&lt;br&gt;
what your auth callback is awaiting.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.iloveblogs.blog/post/supabase-onauthstatechange-hangs-deadlock-fix" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>supabaseauth</category>
      <category>nextjs</category>
      <category>react</category>
    </item>
    <item>
      <title>Fix AuthSessionMissingError: Auth session missing!</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Mon, 24 Aug 2026 21:18:03 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/fix-authsessionmissingerror-auth-session-missing-b1p</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/fix-authsessionmissingerror-auth-session-missing-b1p</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AuthSessionMissingError: Auth session missing!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That string comes from &lt;code&gt;@supabase/auth-js&lt;/code&gt;, not from the Supabase API. The SDK&lt;br&gt;
raises it locally, before any network request, whenever a method that needs a&lt;br&gt;
session cannot find one. Reading it as "Supabase is broken" sends you looking in&lt;br&gt;
the wrong place; reading it as "this particular client object is empty" sends you&lt;br&gt;
straight to the cause.&lt;/p&gt;

&lt;p&gt;There are four ways a client ends up empty, and they need four different fixes.&lt;/p&gt;
&lt;h2&gt;
  
  
  First: which call produced it?
&lt;/h2&gt;

&lt;p&gt;The SDK is not consistent about how it surfaces this, and the inconsistency&lt;br&gt;
causes real bugs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;getUser()&lt;/code&gt; &lt;strong&gt;returns&lt;/strong&gt; the error:&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="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// data.user === null&lt;/span&gt;
&lt;span class="c1"&gt;// error?.name === 'AuthSessionMissingError'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;updateUser()&lt;/code&gt;, &lt;code&gt;setSession()&lt;/code&gt; and &lt;code&gt;refreshSession()&lt;/code&gt; &lt;strong&gt;throw&lt;/strong&gt; it.&lt;/p&gt;

&lt;p&gt;So this code is wrong in a way that will not show up until production:&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="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="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="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;renderDashboard&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;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// data.user is null, no exception was thrown&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;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;redirectToLogin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;               &lt;span class="c1"&gt;// never runs&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the &lt;code&gt;error&lt;/code&gt; value, or check &lt;code&gt;data.user&lt;/code&gt; for null. A &lt;code&gt;try/catch&lt;/code&gt; alone will&lt;br&gt;
walk a signed-out visitor straight into your authenticated UI.&lt;/p&gt;
&lt;h2&gt;
  
  
  Cause 1: a server client built without cookies
&lt;/h2&gt;

&lt;p&gt;This is the App Router classic, and it accounts for most of the reports.&lt;/p&gt;

&lt;p&gt;In the browser, the Supabase client persists the session in &lt;code&gt;localStorage&lt;/code&gt; and&lt;br&gt;
finds it there on the next call. On the server there is no &lt;code&gt;localStorage&lt;/code&gt; and no&lt;br&gt;
ambient state — a server-side client only knows what you hand it. If you create&lt;br&gt;
it with the anon key and nothing else, it has no session by construction, and&lt;br&gt;
every &lt;code&gt;getUser()&lt;/code&gt; returns &lt;code&gt;Auth session missing!&lt;/code&gt; no matter who is signed in.&lt;/p&gt;

&lt;p&gt;The server client must be built from the incoming request's cookie store:&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;// utils/supabase/server.js&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;createServerClient&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;@supabase/ssr&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cookies&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/headers&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;createClient&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;cookieStore&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;cookies&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createServerClient&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;NEXT_PUBLIC_SUPABASE_URL&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;NEXT_PUBLIC_SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;getAll&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="nx"&gt;cookieStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="nf"&gt;setAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cookiesToSet&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="nx"&gt;cookiesToSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
              &lt;span class="nx"&gt;cookieStore&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&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;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Called from a Server Component: middleware handles the refresh.&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;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;Two details people trip on. &lt;code&gt;cookies()&lt;/code&gt; must be awaited in current Next.js&lt;br&gt;
versions — if you have older code destructuring it synchronously, that is a&lt;br&gt;
separate error worth fixing at the same time. And the &lt;code&gt;try/catch&lt;/code&gt; around&lt;br&gt;
&lt;code&gt;setAll&lt;/code&gt; is deliberate: Server Components cannot write cookies, so the write&lt;br&gt;
throws there and middleware has to do the refreshing instead.&lt;/p&gt;

&lt;p&gt;Full wiring, including the client/server split and the route handler cases, is in&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/guides/supabase-auth-complete-session-middleware-guide" rel="noopener noreferrer"&gt;the complete Supabase session and middleware guide&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Cause 2: no middleware refreshing the session
&lt;/h2&gt;

&lt;p&gt;Access tokens are short-lived. The browser client refreshes them on its own&lt;br&gt;
timer; the server has no timer, so it depends on middleware running on each&lt;br&gt;
request to exchange the refresh token and write the new cookies back.&lt;/p&gt;

&lt;p&gt;Without it the failure is time-dependent, which is why it reads as flaky: sign&lt;br&gt;
in, everything works, come back an hour later and every server render reports&lt;br&gt;
&lt;code&gt;Auth session missing!&lt;/code&gt; while the browser tab still believes it is signed in.&lt;/p&gt;

&lt;p&gt;The middleware must both read and write:&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;// middleware.js&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;createServerClient&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;@supabase/ssr&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="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;middleware&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;let&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;next&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createServerClient&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;NEXT_PUBLIC_SUPABASE_URL&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;NEXT_PUBLIC_SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;getAll&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="nf"&gt;setAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cookiesToSet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;cookiesToSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;cookies&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&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;next&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;cookiesToSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;cookies&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&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;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// This call is what performs the refresh. Do not remove it.&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="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="p"&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;/((?!_next/static|_next/image|favicon.ico).*)&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returning a &lt;code&gt;response&lt;/code&gt; object that is not the one the cookie writer mutated is&lt;br&gt;
the subtle way to break this: the refreshed cookies get written to an object you&lt;br&gt;
then discard, and the browser never receives them. The symptom is identical to&lt;br&gt;
having no middleware at all.&lt;/p&gt;
&lt;h2&gt;
  
  
  Cause 3: reading the session too early on the client
&lt;/h2&gt;

&lt;p&gt;On first paint the browser client has not finished restoring the session from&lt;br&gt;
storage. A &lt;code&gt;getUser()&lt;/code&gt; fired at module scope, or in a component body rather than&lt;br&gt;
an effect, can run before the restore completes and report the session as&lt;br&gt;
missing — then a re-render a moment later shows it present.&lt;/p&gt;

&lt;p&gt;Subscribe rather than poll:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&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="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&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;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setUser&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;user&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onAuthStateChange&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&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="nf"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&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;One warning: do not await other Supabase calls inside that callback. It&lt;br&gt;
deadlocks the client and every later query hangs forever — the mechanism and the&lt;br&gt;
fix are in&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/supabase-onauthstatechange-hangs-deadlock-fix" rel="noopener noreferrer"&gt;Supabase hangs after onAuthStateChange&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Cause 4: nobody is signed in
&lt;/h2&gt;

&lt;p&gt;The dull answer, and often the right one.&lt;/p&gt;

&lt;p&gt;For a visitor who never signed in, or who just signed out, &lt;code&gt;Auth session&lt;br&gt;
missing!&lt;/code&gt; is the correct and expected result. If it is filling your logs, the&lt;br&gt;
problem is that your code treats a normal state as an exception:&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="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&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="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// not an error path — a branch&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="o"&gt;&amp;amp;&amp;amp;&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;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AuthSessionMissingError&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;captureException&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="c1"&gt;// this one is a genuine failure&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Filter the expected case out of your error reporting and the remaining&lt;br&gt;
occurrences become meaningful again.&lt;/p&gt;
&lt;h2&gt;
  
  
  A quick way to tell the causes apart
&lt;/h2&gt;

&lt;p&gt;Log one line in the failing server-side code path:&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;store&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;cookies&lt;/span&gt;&lt;span class="p"&gt;();&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[auth] cookies seen:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAll&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;sb-…-auth-token&lt;/code&gt; cookie at all&lt;/strong&gt; — the browser never sent one. The
visitor is signed out (cause 4), or the sign-in redirect never landed on your
origin: see &lt;a href="https://www.iloveblogs.blog/post/supabase-auth-redirect-fix" rel="noopener noreferrer"&gt;the Supabase auth redirect fix&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cookie present, still missing on the server&lt;/strong&gt; — the client was built without
the cookie store (cause 1).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cookie present but stale, and it only fails after a while&lt;/strong&gt; — middleware is
missing or discarding the refreshed response (cause 2).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related failures with different messages
&lt;/h2&gt;

&lt;p&gt;Once a session exists, the next class of problem is what that session is allowed&lt;br&gt;
to read. An authenticated request that returns zero rows is not an auth problem&lt;br&gt;
at all — that is row level security filtering silently, covered in&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/supabase-rls-silent-failures-debug" rel="noopener noreferrer"&gt;why RLS returns zero rows&lt;/a&gt;, and you&lt;br&gt;
can reproduce your own policy against real Postgres in the&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/tools/supabase-rls-playground" rel="noopener noreferrer"&gt;RLS Playground&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two adjacent messages worth not confusing with this one: the&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/fix/fix-supabase-getuser-warning" rel="noopener noreferrer"&gt;getUser() security warning&lt;/a&gt;, which is about&lt;br&gt;
trusting &lt;code&gt;getSession()&lt;/code&gt; on the server, and&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/fix-supabase-auth-session-persistence" rel="noopener noreferrer"&gt;session persistence failures&lt;/a&gt;,&lt;br&gt;
where the session exists but does not survive navigation.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.iloveblogs.blog/post/supabase-auth-session-missing-error-fix" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>supabaseauth</category>
      <category>nextjs</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>Better Auth vs Supabase Auth 2026: Honest Verdict</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Mon, 24 Aug 2026 21:17:56 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/better-auth-vs-supabase-auth-2026-honest-verdict-24bo</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/better-auth-vs-supabase-auth-2026-honest-verdict-24bo</guid>
      <description>&lt;p&gt;Most comparisons of these two list sign-in methods side by side, notice that&lt;br&gt;
both support email, OAuth, magic links and passkeys, and conclude that it is a&lt;br&gt;
matter of taste.&lt;/p&gt;

&lt;p&gt;It is not a matter of taste. There is one decision underneath, and it is&lt;br&gt;
architectural: &lt;strong&gt;where is authorisation enforced?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Supabase Auth exists to put an identity &lt;em&gt;inside the database&lt;/em&gt;, so that Postgres&lt;br&gt;
itself can decide what a request may read. Better Auth exists to put identity&lt;br&gt;
&lt;em&gt;inside your application&lt;/em&gt;, so that your TypeScript code can decide. Everything&lt;br&gt;
else — the provider list, the plugin catalogue, the pricing — follows from that&lt;br&gt;
and matters far less.&lt;/p&gt;
&lt;h2&gt;
  
  
  The mechanism you are actually choosing between
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Supabase Auth: the database knows who you are
&lt;/h3&gt;

&lt;p&gt;When a Supabase client makes a request, it carries a JWT signed with your&lt;br&gt;
project's secret. PostgREST validates it, sets the Postgres role from the&lt;br&gt;
token's &lt;code&gt;role&lt;/code&gt; claim, and puts the whole claim set into a session variable.&lt;br&gt;
&lt;code&gt;auth.uid()&lt;/code&gt; is then a small SQL function that reads &lt;code&gt;sub&lt;/code&gt; out of that variable.&lt;/p&gt;

&lt;p&gt;That is the entire trick, and it is why a row level security policy can say:&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;"read own documents"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;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="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;p&gt;The consequence is real: a developer who forgets a &lt;code&gt;where user_id = ...&lt;/code&gt; in a&lt;br&gt;
query does not leak data, because the database refuses the rows regardless. Your&lt;br&gt;
authorisation rules live in one place, next to the data, and apply to every&lt;br&gt;
client that ever connects — your web app, your mobile app, a script someone runs&lt;br&gt;
at 2am.&lt;/p&gt;

&lt;p&gt;The cost is that those rules are written in SQL, are awkward to unit test, and&lt;br&gt;
fail in ways that are quiet by design. That is why I built the&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/tools/supabase-rls-playground" rel="noopener noreferrer"&gt;RLS Playground&lt;/a&gt; — it runs a real Postgres engine&lt;br&gt;
in the browser and executes your policies as &lt;code&gt;anon&lt;/code&gt;, as two different signed-in&lt;br&gt;
users and as &lt;code&gt;service_role&lt;/code&gt;, side by side, so "does this policy actually isolate&lt;br&gt;
tenants?" becomes a question you answer by looking rather than by reasoning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better Auth: your application knows who you are
&lt;/h3&gt;

&lt;p&gt;Better Auth describes itself as a framework-agnostic authentication and&lt;br&gt;
authorisation framework for TypeScript, with database management and migrations&lt;br&gt;
handled for you and capabilities such as two-factor, organisations, rate&lt;br&gt;
limiting and access control available in the core or as plugins.&lt;/p&gt;

&lt;p&gt;Practically, it means users and sessions become ordinary tables in your&lt;br&gt;
database. You can join them. You can migrate them with the rest of your schema.&lt;br&gt;
You can read them in a test without mocking a network service. Your &lt;code&gt;getSession()&lt;/code&gt;&lt;br&gt;
is a database read in your own process, not a round trip to an identity&lt;br&gt;
provider.&lt;/p&gt;

&lt;p&gt;Authorisation then happens in your server code — in a service layer, a&lt;br&gt;
middleware, or whatever pattern your team already uses. It is easy to test,&lt;br&gt;
easy to express complex rules in, and completely dependent on every query going&lt;br&gt;
through it. A raw query written in a hurry bypasses the whole thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake to avoid: assuming they compose
&lt;/h2&gt;

&lt;p&gt;The tempting architecture is Better Auth for identity, Supabase for the&lt;br&gt;
database. It is a reasonable thing to want, and it works — but not by default,&lt;br&gt;
and the failure mode is silent.&lt;/p&gt;

&lt;p&gt;Two cases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You connect to Postgres directly&lt;/strong&gt; from your server, with Drizzle or Prisma&lt;br&gt;
over a normal connection. That connection authenticates as a database user of&lt;br&gt;
your choosing, not as &lt;code&gt;authenticated&lt;/code&gt;, and no JWT claims are set. Your RLS&lt;br&gt;
policies are simply not consulted in any meaningful way, and if the role is the&lt;br&gt;
table owner or a superuser they are skipped entirely. Everything appears to&lt;br&gt;
work — because every query returns everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You go through PostgREST&lt;/strong&gt; (the &lt;code&gt;supabase-js&lt;/code&gt; client). Then the request needs&lt;br&gt;
a JWT signed with your project's secret and carrying &lt;code&gt;sub&lt;/code&gt; and &lt;code&gt;role&lt;/code&gt;, or the&lt;br&gt;
API rejects it. Minting one from Better Auth is possible, but you are now&lt;br&gt;
maintaining a token bridge and two sources of truth about session lifetime and&lt;br&gt;
revocation.&lt;/p&gt;

&lt;p&gt;If you choose this combination, decide deliberately which of the two you are&lt;br&gt;
doing, and write it down. The version I would avoid is drifting into case one&lt;br&gt;
while still believing RLS protects you. The&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/test-supabase-rls-policies-before-you-ship" rel="noopener noreferrer"&gt;testing procedure here&lt;/a&gt; will&lt;br&gt;
tell you within a minute whether your policies are being enforced at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each one is clearly right
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Supabase Auth is the right call when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You already run Supabase. Auth is provisioned, and storage, realtime and the
auto-generated API all expect its JWTs. Replacing it means replacing their
authorisation model too.&lt;/li&gt;
&lt;li&gt;Authorisation is data-shaped: rows belong to users, users belong to
organisations, and the rules are expressible as predicates. See
&lt;a href="https://www.iloveblogs.blog/guides/supabase-rls-policy-design-patterns" rel="noopener noreferrer"&gt;RLS policy design patterns&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;More than one client will reach the database, and you want the guarantee to
hold for all of them rather than trusting each one.&lt;/li&gt;
&lt;li&gt;You want the shortest path from zero to a working signed-in app — the
&lt;a href="https://www.iloveblogs.blog/guides/supabase-auth-nextjs-complete-guide-2026" rel="noopener noreferrer"&gt;complete Supabase auth guide for Next.js&lt;/a&gt;
is roughly an afternoon.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Better Auth is the right call when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your database is your own Postgres — Neon, Hetzner, Scaleway, RDS, whatever —
and you have no intention of adopting the rest of the Supabase platform.&lt;/li&gt;
&lt;li&gt;Authorisation is behaviour-shaped rather than row-shaped: quotas, workflow
states, plan limits, per-endpoint permissions. Expressing those in SQL policies
is possible and unpleasant.&lt;/li&gt;
&lt;li&gt;You want auth in your repository, reviewable in pull requests and testable in
CI, rather than configured in a dashboard.&lt;/li&gt;
&lt;li&gt;Cost predictability at scale matters. Users in your own tables have no
monthly-active-user tier to cross.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The parts that do not decide it
&lt;/h2&gt;

&lt;p&gt;A few axes get more attention than they deserve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provider coverage.&lt;/strong&gt; Both do email and password, the mainstream OAuth&lt;br&gt;
providers, magic links, OTP and passkeys. Check the specific niche provider you&lt;br&gt;
need; otherwise this is a tie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Organisations and RBAC.&lt;/strong&gt; Better Auth ships organisation and access-control&lt;br&gt;
capability as part of the framework. Supabase gives you tables and policies and&lt;br&gt;
expects you to build the model — which is more work up front and completely&lt;br&gt;
yours afterwards. See&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-multi-tenant-saas-architecture" rel="noopener noreferrer"&gt;multi-tenant SaaS architecture on Supabase&lt;/a&gt;&lt;br&gt;
for what that build looks like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lock-in.&lt;/strong&gt; Both store users in Postgres, so in either case you can export the&lt;br&gt;
table. What is not portable is the &lt;em&gt;authorisation layer&lt;/em&gt;: RLS policies assume&lt;br&gt;
Supabase-minted JWTs, and application-layer rules assume your session object.&lt;br&gt;
Migrating auth is cheap; migrating enforcement is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note for EU teams
&lt;/h2&gt;

&lt;p&gt;If you are handling personal data under the GDPR, the question is where the&lt;br&gt;
identity records physically live.&lt;/p&gt;

&lt;p&gt;With Better Auth they live in your database, so an EU-hosted Postgres keeps them&lt;br&gt;
in the EU and adds no additional processor to your record of processing&lt;br&gt;
activities. With Supabase Auth they live in your Supabase project — fine for EU&lt;br&gt;
residency provided you pick an EU region &lt;strong&gt;when you create the project&lt;/strong&gt;, since&lt;br&gt;
the region cannot be changed later. Migrating a live project across regions is&lt;br&gt;
a data migration, not a setting.&lt;/p&gt;

&lt;p&gt;Either way, both give you the account deletion primitive you need for erasure&lt;br&gt;
requests; with Better Auth it is a delete in your own schema, and with Supabase&lt;br&gt;
it is an admin API call plus whatever cascades you defined on your own tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verdict
&lt;/h2&gt;

&lt;p&gt;Ask one question: &lt;strong&gt;if a developer on your team writes a query without a&lt;br&gt;
&lt;code&gt;where&lt;/code&gt; clause on the tenant column, what stops the leak?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer needs to be "the database", you want Supabase Auth, and you should&lt;br&gt;
invest the afternoon it takes to test your policies properly rather than&lt;br&gt;
assuming them. If the answer can be "our service layer, which is reviewed and&lt;br&gt;
tested", Better Auth gives you a cleaner, cheaper, more testable system that&lt;br&gt;
lives entirely in your repository.&lt;/p&gt;

&lt;p&gt;Both are good. They are good at different things, and the thing they differ on&lt;br&gt;
is not one you can change later without a rewrite.&lt;/p&gt;

&lt;p&gt;For the third option in this space — a managed provider that owns the identity&lt;br&gt;
layer entirely — the trade-offs are laid out in&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/guides/supabase-auth-vs-clerk-2026-honest-comparison" rel="noopener noreferrer"&gt;Supabase Auth vs Clerk&lt;/a&gt;,&lt;br&gt;
and the wider field is surveyed in&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/nextjs-authentication-comparison-2026" rel="noopener noreferrer"&gt;Next.js auth in 2026&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.iloveblogs.blog/post/better-auth-vs-supabase-auth-2026" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>betterauth</category>
      <category>authentication</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Switch Databases in psql: The \connect Command (2026)</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Fri, 21 Aug 2026 21:14:18 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/switch-databases-in-psql-the-connect-command-2026-2pia</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/switch-databases-in-psql-the-connect-command-2026-2pia</guid>
      <description>&lt;h2&gt;
  
  
  The muscle-memory trap
&lt;/h2&gt;

&lt;p&gt;You came from MySQL. You opened &lt;code&gt;psql&lt;/code&gt;, ran &lt;code&gt;\l&lt;/code&gt; to list databases, and typed&lt;br&gt;
&lt;code&gt;USE mydb;&lt;/code&gt;. psql replied with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;mydb=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;use mydb&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;ERROR:  syntax error at or near "use"
&lt;/span&gt;&lt;span class="gp"&gt;LINE 1: use mydb;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no &lt;code&gt;USE&lt;/code&gt; in PostgreSQL, and there is no in-session database switch.&lt;br&gt;
Each PostgreSQL database is an isolated connection target. Switching databases&lt;br&gt;
means &lt;strong&gt;opening a new connection&lt;/strong&gt;, and psql gives you one meta-command for it:&lt;br&gt;
&lt;code&gt;\connect&lt;/code&gt;, abbreviated &lt;code&gt;\c&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is one of the highest-traffic psql questions on Stack Overflow (the&lt;br&gt;
"switch databases in psql" thread has 1.3M+ views) precisely because of that&lt;br&gt;
MySQL muscle memory.&lt;/p&gt;
&lt;h2&gt;
  
  
  The command
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\c dbname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;or the long form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\connect dbname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;psql prints the new connection prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postgres=&amp;gt; \c mydb
You are now connected to database "mydb" as user "postgres".
mydb=&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt changes to reflect the database you are in. Your current session&lt;br&gt;
state — temporary tables, &lt;code&gt;SET&lt;/code&gt; variables, prepared statements — is gone,&lt;br&gt;
because it is a brand new connection.&lt;/p&gt;
&lt;h2&gt;
  
  
  Switch user, host, or port at the same time
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;\c&lt;/code&gt; accepts a full connection spec. You can switch to a different database&lt;br&gt;
&lt;strong&gt;and&lt;/strong&gt; a different user in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\c dbname newuser
\c dbname newuser newhost 5433
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you omit the user, psql reuses the current one. When you omit host and&lt;br&gt;
port, it reuses the current connection's. If the new database needs a password,&lt;br&gt;
psql prompts for it (or uses the &lt;code&gt;PGPASSWORD&lt;/code&gt; env var / &lt;code&gt;~/.pgpass&lt;/code&gt; file).&lt;/p&gt;
&lt;h2&gt;
  
  
  The transaction gotcha
&lt;/h2&gt;

&lt;p&gt;You cannot switch databases inside an open transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mydb=&amp;gt; BEGIN;
BEGIN
mydb=*# \c otherdb
FATAL:  terminating connection due to administrator command
could not connect to server: ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some psql versions phrase it as &lt;em&gt;"there is a transaction in progress"&lt;/em&gt;. Either&lt;br&gt;
way the rule is the same: finish the transaction first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mydb=*# COMMIT;   -- or ROLLBACK
COMMIT
mydb=# \c otherdb
You are now connected to database "otherdb".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This exists because switching databases silently drops the connection — and&lt;br&gt;
PostgreSQL will not let uncommitted work vanish without an explicit decision.&lt;/p&gt;
&lt;h2&gt;
  
  
  Connect at launch instead
&lt;/h2&gt;

&lt;p&gt;If you know the database up front, skip &lt;code&gt;\c&lt;/code&gt; entirely and connect at launch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="nt"&gt;-d&lt;/span&gt; mydb
psql &lt;span class="nt"&gt;-d&lt;/span&gt; mydb &lt;span class="nt"&gt;-U&lt;/span&gt; app_user &lt;span class="nt"&gt;-h&lt;/span&gt; localhost &lt;span class="nt"&gt;-p&lt;/span&gt; 5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or with a connection string (the form Supabase gives you):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="s2"&gt;"postgresql://user:pass@host:5432/mydb"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the common "list databases, pick one, connect" flow, the sequence is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\l              -- list databases
\c target_db    -- connect to the one you want
\dt             -- list tables in the new database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you are in, the tables are not in a single list you can scan without&lt;br&gt;
context — see &lt;a href="https://www.iloveblogs.blog/post/how-to-show-tables-in-postgresql-psql-supabase" rel="noopener noreferrer"&gt;show tables in PostgreSQL&lt;/a&gt;&lt;br&gt;
for the full &lt;code&gt;\dt&lt;/code&gt;, &lt;code&gt;\d&lt;/code&gt;, and &lt;code&gt;information_schema&lt;/code&gt; walkthrough, and the&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/postgres-describe-table-equivalent" rel="noopener noreferrer"&gt;PostgreSQL DESCRIBE TABLE equivalent&lt;/a&gt;&lt;br&gt;
for inspecting columns.&lt;/p&gt;
&lt;h2&gt;
  
  
  Exit psql
&lt;/h2&gt;

&lt;p&gt;While we are on meta-commands: to leave psql, it is &lt;code&gt;\q&lt;/code&gt; (or Ctrl+D):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mydb=&amp;gt; \q
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not &lt;code&gt;exit&lt;/code&gt;, not &lt;code&gt;quit&lt;/code&gt; — though recent psql versions accept those as aliases.&lt;br&gt;
&lt;code&gt;\q&lt;/code&gt; works everywhere.&lt;/p&gt;
&lt;h2&gt;
  
  
  Supabase: connect to a specific project
&lt;/h2&gt;

&lt;p&gt;Supabase exposes a Postgres database per project. From the dashboard:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project Settings → Database → Connection string → URI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Copy it (the pooling variant, port &lt;code&gt;6543&lt;/code&gt;, is what you want for CLI work):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="s2"&gt;"postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You land in the &lt;code&gt;postgres&lt;/code&gt; database — the only user database. There is rarely&lt;br&gt;
a reason to &lt;code&gt;\c&lt;/code&gt; to another database on Supabase; you switch between &lt;strong&gt;schemas&lt;/strong&gt;&lt;br&gt;
(&lt;code&gt;public&lt;/code&gt;, &lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;storage&lt;/code&gt;) with &lt;code&gt;SET search_path&lt;/code&gt;, not databases:&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;SET&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="k"&gt;TO&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;auth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&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;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need to reset the password on that connection, the&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/how-to-change-postgresql-user-password-alter-role-supabase" rel="noopener noreferrer"&gt;change PostgreSQL user password guide&lt;/a&gt;&lt;br&gt;
covers the &lt;code&gt;ALTER ROLE&lt;/code&gt; flow that works on Supabase too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Typing &lt;code&gt;USE dbname&lt;/code&gt;.&lt;/strong&gt; There is no &lt;code&gt;USE&lt;/code&gt; in psql. It is &lt;code&gt;\c dbname&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switching inside a transaction.&lt;/strong&gt; &lt;code&gt;COMMIT&lt;/code&gt; or &lt;code&gt;ROLLBACK&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting the prompt change.&lt;/strong&gt; The prompt shows the current database;
if your queries hit the wrong tables, check it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixing up database and schema.&lt;/strong&gt; On PostgreSQL (and Supabase) you usually
want to switch &lt;strong&gt;schema&lt;/strong&gt; with &lt;code&gt;SET search_path&lt;/code&gt;, not database with &lt;code&gt;\c&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connecting on port 5432 to Supabase.&lt;/strong&gt; That is the direct connection; use
6543 (pooler) unless you need a feature PgBouncer does not support.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/how-to-show-tables-in-postgresql-psql-supabase" rel="noopener noreferrer"&gt;Show Tables in PostgreSQL (psql)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/postgres-describe-table-equivalent" rel="noopener noreferrer"&gt;PostgreSQL DESCRIBE TABLE Equivalent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/how-to-change-postgresql-user-password-alter-role-supabase" rel="noopener noreferrer"&gt;Change a PostgreSQL User Password&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/supabase-client-permission-denied-for-schema-public" rel="noopener noreferrer"&gt;Supabase "permission denied for schema public" Fix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/hubs/postgresql" rel="noopener noreferrer"&gt;PostgreSQL Migration Rollback Playbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-database-design-optimization" rel="noopener noreferrer"&gt;Next.js + Supabase Database Design and Optimization&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.iloveblogs.blog/post/postgres-switch-database-psql-connect-command" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>psql</category>
      <category>database</category>
      <category>supabase</category>
    </item>
    <item>
      <title>How to Drop All Tables in PostgreSQL Safely (2026)</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Fri, 21 Aug 2026 14:32:35 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/how-to-drop-all-tables-in-postgresql-safely-2026-1a47</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/how-to-drop-all-tables-in-postgresql-safely-2026-1a47</guid>
      <description>&lt;h2&gt;
  
  
  Why you are here
&lt;/h2&gt;

&lt;p&gt;You ran a migration that went sideways, or you are resetting a local dev&lt;br&gt;
database, or a test suite needs a clean slate. The &lt;code&gt;psql&lt;/code&gt; console has 40 tables&lt;br&gt;
and you do not want to type &lt;code&gt;DROP TABLE&lt;/code&gt; 40 times.&lt;/p&gt;

&lt;p&gt;There is a one-command way to do it. There is also a PostgreSQL 15 change that&lt;br&gt;
silently breaks it the first time you try, and a Supabase-specific caveat that&lt;br&gt;
makes the naive command dangerous on a hosted project. This article is the&lt;br&gt;
reset path I use, with the gotchas inline.&lt;/p&gt;

&lt;p&gt;The original Stack Overflow thread ("How can I drop all the tables in a&lt;br&gt;
PostgreSQL database?") has been viewed more than 1.5 million times for a&lt;br&gt;
reason: every developer hits this during a migration at least once.&lt;/p&gt;
&lt;h2&gt;
  
  
  The one-command reset
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ⚠️  Destructive. Drops every table, view, sequence, and function in public.&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;DROP SCHEMA public CASCADE&lt;/code&gt; removes the &lt;code&gt;public&lt;/code&gt; schema and &lt;strong&gt;every object that&lt;br&gt;
depends on it&lt;/strong&gt; — tables, views, materialized views, sequences, functions, and&lt;br&gt;
triggers. &lt;code&gt;CASCADE&lt;/code&gt; is what saves you from listing the dependencies in order.&lt;br&gt;
Then you recreate the empty schema and restore the default grants.&lt;/p&gt;

&lt;p&gt;That second &lt;code&gt;GRANT ... TO public&lt;/code&gt; line is where the PostgreSQL 15 gotcha lives.&lt;/p&gt;
&lt;h2&gt;
  
  
  The PostgreSQL 15 gotcha that breaks this
&lt;/h2&gt;

&lt;p&gt;PostgreSQL 15 changed two defaults on the &lt;code&gt;public&lt;/code&gt; schema, and both of them&lt;br&gt;
bite you when you drop and recreate it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PUBLIC&lt;/code&gt; no longer has &lt;code&gt;CREATE&lt;/code&gt; on &lt;code&gt;public&lt;/code&gt; by default.&lt;/strong&gt; Before 15, every
role could create objects in &lt;code&gt;public&lt;/code&gt;. From 15 onward, on new databases and
new clusters, that privilege is revoked. This is the secure-schema pattern
PostgreSQL has recommended since CVE-2018-1058, now made the default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The owner of &lt;code&gt;public&lt;/code&gt; is now &lt;code&gt;pg_database_owner&lt;/code&gt;&lt;/strong&gt;, not the bootstrap
superuser. This lets database owners manage &lt;code&gt;public&lt;/code&gt; without being a
superuser.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you run &lt;code&gt;CREATE SCHEMA public&lt;/code&gt; on a 15+ database, the schema comes back&lt;br&gt;
with these new secure defaults. Your application role — the one that runs&lt;br&gt;
&lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;CREATE TABLE&lt;/code&gt;, migrations — no longer has &lt;code&gt;CREATE&lt;/code&gt; on &lt;code&gt;public&lt;/code&gt;. The&lt;br&gt;
next migration or insert throws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: permission denied for schema public
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you already hit that, the fix is to grant explicitly to the role your app&lt;br&gt;
actually uses, instead of relying on &lt;code&gt;PUBLIC&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="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;app_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt;  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;app_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Supabase, the anon and authenticated roles also need &lt;code&gt;USAGE&lt;/code&gt; on &lt;code&gt;public&lt;/code&gt; or&lt;br&gt;
your client queries fail with the same error. For a deeper write-up of that&lt;br&gt;
specific failure, see &lt;a href="https://www.iloveblogs.blog/post/supabase-client-permission-denied-for-schema-public" rel="noopener noreferrer"&gt;Supabase client "permission denied for schema public"&lt;br&gt;
fix&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The variant that keeps extensions
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;DROP SCHEMA public CASCADE&lt;/code&gt; also drops every extension installed &lt;strong&gt;in the&lt;br&gt;
public schema&lt;/strong&gt;. &lt;code&gt;uuid-ossp&lt;/code&gt;, &lt;code&gt;pgcrypto&lt;/code&gt;, &lt;code&gt;pg_stat_statements&lt;/code&gt; (if installed&lt;br&gt;
there), and — critically on Supabase — &lt;code&gt;pgvector&lt;/code&gt; all live somewhere. If you&lt;br&gt;
rely on &lt;code&gt;gen_random_uuid()&lt;/code&gt;, &lt;code&gt;uuid_generate_v4()&lt;/code&gt;, or vector columns, they&lt;br&gt;
vanish and you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: function gen_random_uuid() does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reinstall them after recreating the schema:&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="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Reinstall the extensions you actually use.&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="nv"&gt;"uuid-ossp"&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;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pgcrypto&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;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- pgvector&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you never want to lose extensions during a reset, drop the tables only and&lt;br&gt;
leave the schema (and its extensions) intact — see the selective script below.&lt;/p&gt;
&lt;h2&gt;
  
  
  Drop only tables, keep everything else
&lt;/h2&gt;

&lt;p&gt;When extensions and functions must survive the reset, drop tables individually&lt;br&gt;
via &lt;code&gt;information_schema&lt;/code&gt; instead of nuking the whole schema:&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;DO&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
  &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="n"&gt;RECORD&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_tables&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
  &lt;span class="n"&gt;LOOP&lt;/span&gt;
    &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="s1"&gt;'DROP TABLE IF EXISTS public.'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;quote_ident&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' CASCADE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;LOOP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="err"&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 loops over every table in &lt;code&gt;public&lt;/code&gt; and drops it with &lt;code&gt;CASCADE&lt;/code&gt; (so views&lt;br&gt;
and foreign keys depending on it go too). Functions, sequences created by&lt;br&gt;
&lt;code&gt;SERIAL&lt;/code&gt; columns (they get dropped with their tables), and extensions stay.&lt;/p&gt;

&lt;p&gt;To also reset sequences after reloading data:&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;setval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_get_serial_sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'public.'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;quote_ident&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'id'&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="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_tables&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Supabase path: do not run DROP SCHEMA on production
&lt;/h2&gt;

&lt;p&gt;On a hosted Supabase project, &lt;code&gt;public&lt;/code&gt; is not the only schema that matters. The&lt;br&gt;
&lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;storage&lt;/code&gt;, &lt;code&gt;realtime&lt;/code&gt;, and &lt;code&gt;graphql&lt;/code&gt; schemas are all managed by Supabase&lt;br&gt;
and reference objects that your tables depend on. Running&lt;br&gt;
&lt;code&gt;DROP SCHEMA public CASCADE&lt;/code&gt; on a production project can cascade into Supabase's&lt;br&gt;
internal objects and break the dashboard, auth, and storage.&lt;/p&gt;

&lt;p&gt;For a &lt;strong&gt;dev project&lt;/strong&gt;, use the controlled reset:&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;# CLI: drops and rebuilds everything from your migrations folder&lt;/span&gt;
supabase db reset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or in the dashboard: &lt;strong&gt;Project Settings → Database → Reset database&lt;/strong&gt;. Both&lt;br&gt;
rebuild the schema from your migration files, which is the safe path — your&lt;br&gt;
schema is reproducible from code, not from memory.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;production&lt;/strong&gt;, never reset. Restore from a backup or a &lt;code&gt;pg_dump&lt;/code&gt; snapshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pg_dump &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; c &lt;span class="nt"&gt;-f&lt;/span&gt; backup.dump
&lt;span class="c"&gt;# ... later, to restore:&lt;/span&gt;
pg_restore &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; backup.dump
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the reset is part of a failed migration, follow the&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/hubs/postgresql" rel="noopener noreferrer"&gt;PostgreSQL migration rollback playbook&lt;/a&gt;&lt;br&gt;
instead of dropping tables by hand.&lt;/p&gt;
&lt;h2&gt;
  
  
  The transaction wrapper for dev
&lt;/h2&gt;

&lt;p&gt;On a local database, wrap the reset in a transaction so a typo does not leave&lt;br&gt;
you in a half-dropped state:&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;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- sanity check: SELECT 1;&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- if something looked wrong: ROLLBACK; (but the schema is already gone)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: &lt;code&gt;DROP/CREATE SCHEMA&lt;/code&gt; inside a transaction is safe to &lt;code&gt;ROLLBACK&lt;/code&gt; —&lt;br&gt;
PostgreSQL transactional DDL means a rollback restores the dropped schema. This&lt;br&gt;
is one of the features that makes Postgres migrations far less scary than other&lt;br&gt;
engines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production safety checklist
&lt;/h2&gt;

&lt;p&gt;Before you run any of this against anything that is not a throwaway dev&lt;br&gt;
database:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Take a backup first.&lt;/strong&gt; &lt;code&gt;pg_dump -F c&lt;/code&gt; is cheap insurance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirm the connection string.&lt;/strong&gt; &lt;code&gt;\conninfo&lt;/code&gt; in &lt;code&gt;psql&lt;/code&gt; — verify you are
not pointed at prod. The number of times a reset has hit the wrong database
is exactly why this section exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block new connections&lt;/strong&gt; during the reset if other services share the DB:
&lt;code&gt;ALTER DATABASE yourdb CONNECTION LIMIT 0;&lt;/code&gt; then terminate backends, reset,
then restore the limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have a tested restore path.&lt;/strong&gt; A backup you have never restored from is a
wish, not a backup.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the broader posture — when to roll forward vs roll back, how to stage&lt;br&gt;
migrations so a reset is never the plan — read the&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-database-design-optimization" rel="noopener noreferrer"&gt;database design and optimization guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting &lt;code&gt;CASCADE&lt;/code&gt;.&lt;/strong&gt; Without it, &lt;code&gt;DROP SCHEMA&lt;/code&gt; refuses to run if any
object exists in it. &lt;code&gt;CASCADE&lt;/code&gt; is the whole point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running it in the wrong database.&lt;/strong&gt; Use &lt;code&gt;SELECT current_database();&lt;/code&gt; to
confirm before you press enter. &lt;code&gt;\l&lt;/code&gt; lists databases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not re-granting on PostgreSQL 15+.&lt;/strong&gt; The schema recreates with secure
defaults and your app role loses &lt;code&gt;CREATE&lt;/code&gt;. See the gotcha section above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dropping on a hosted Supabase prod.&lt;/strong&gt; Use &lt;code&gt;supabase db reset&lt;/code&gt; on dev only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dropping extensions by accident.&lt;/strong&gt; Reinstall &lt;code&gt;uuid-ossp&lt;/code&gt;, &lt;code&gt;pgcrypto&lt;/code&gt;,
&lt;code&gt;pgvector&lt;/code&gt; after &lt;code&gt;CREATE SCHEMA&lt;/code&gt;, or use the table-only loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When you should not do this at all
&lt;/h2&gt;

&lt;p&gt;A full reset is a dev convenience. In production, "drop everything and start&lt;br&gt;
over" is almost always the wrong instinct. The right move is usually a targeted&lt;br&gt;
migration: &lt;code&gt;ALTER TABLE&lt;/code&gt;, a backfill, a column drop behind a feature flag. If&lt;br&gt;
you are resetting because a migration is too tangled to untangle, the fix is&lt;br&gt;
to write smaller, reversible migrations going forward — not to make resetting&lt;br&gt;
easier.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.iloveblogs.blog/post/debugging-supabase-rls-issues" rel="noopener noreferrer"&gt;RLS debugging guide&lt;/a&gt; covers the same&lt;br&gt;
instinct for policy drift: most "blow it away" urges are really "I cannot see&lt;br&gt;
what changed" problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Dev reset, PostgreSQL 15+:&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;app_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- re-grant your app role&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="nv"&gt;"uuid-ossp"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;-- restore extensions&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pgcrypto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Supabase dev: &lt;code&gt;supabase db reset&lt;/code&gt;. On Supabase prod: restore from a backup,&lt;br&gt;
do not drop the schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/how-to-show-tables-in-postgresql-psql-supabase" rel="noopener noreferrer"&gt;Show Tables in PostgreSQL (psql)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/postgres-describe-table-equivalent" rel="noopener noreferrer"&gt;PostgreSQL DESCRIBE TABLE Equivalent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/how-to-change-postgresql-user-password-alter-role-supabase" rel="noopener noreferrer"&gt;Change a PostgreSQL User Password&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/supabase-client-permission-denied-for-schema-public" rel="noopener noreferrer"&gt;Supabase "permission denied for schema public" Fix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/hubs/postgresql" rel="noopener noreferrer"&gt;PostgreSQL Migration Rollback Playbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-database-design-optimization" rel="noopener noreferrer"&gt;Next.js + Supabase Database Design and Optimization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/debugging-supabase-rls-issues" rel="noopener noreferrer"&gt;Debug Supabase RLS Issues&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.iloveblogs.blog/post/postgres-drop-all-tables-reset-database-safely" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>psql</category>
      <category>schema</category>
      <category>migration</category>
    </item>
    <item>
      <title>Next.js useSearchParams Suspense: Static Rendering Fix 2026</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Fri, 21 Aug 2026 14:31:53 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/nextjs-usesearchparams-suspense-static-rendering-fix-2026-31nn</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/nextjs-usesearchparams-suspense-static-rendering-fix-2026-31nn</guid>
      <description>&lt;p&gt;I hit the build error &lt;code&gt;useSearchParams() should be wrapped in a suspense boundary at page "/"&lt;/code&gt; for the first time while shipping a Supabase-backed filter UI in Next.js 15, and the fix was a one-line wrap: put the component that calls &lt;code&gt;useSearchParams&lt;/code&gt; inside a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary, and the rest of the page prerenders normally. This guide covers the exact pattern, the real reason the build refuses to ship without a boundary, and a verification step that proves the route stayed in the static output. I also link to the longer-form version of this fix in &lt;a href="https://www.iloveblogs.blog/post/fix-nextjs-missing-suspense-boundary-use-searchparams" rel="noopener noreferrer"&gt;Missing Suspense Boundary with useSearchParams (Next.js)&lt;/a&gt; and to the broader query-reading primer in &lt;a href="https://www.iloveblogs.blog/post/how-to-get-query-string-parameters-in-nextjs" rel="noopener noreferrer"&gt;How to get query string parameters in Next.js&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Shortcut:&lt;/strong&gt; the free &lt;a href="https://www.iloveblogs.blog/tools/nextjs-build-error-decoder" rel="noopener noreferrer"&gt;Next.js Build Error Decoder&lt;/a&gt; recognizes this exact prerender error — paste your build output and jump straight to the matching fix.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The exact error and when it fires
&lt;/h2&gt;

&lt;p&gt;The error you see in your terminal during &lt;code&gt;next build&lt;/code&gt; looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ npx next build

   ▲ Next.js 15.0.0

Error: useSearchParams() should be wrapped in a suspense boundary at page "/products".

Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fires when three things are true at the same time. First, a Client Component — a file with the &lt;code&gt;"use client"&lt;/code&gt; directive at the top — calls the &lt;code&gt;useSearchParams&lt;/code&gt; hook from &lt;code&gt;next/navigation&lt;/code&gt;. Second, that Client Component is being prerendered, which is the default in the App Router for any route that does not opt out. Third, there is no &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary above the call in the rendered tree. When all three conditions are met, Next.js refuses to fall back to dynamic rendering silently and instead throws, because silently bailing out would hide a real problem: the prerendered HTML would either be wrong or empty.&lt;/p&gt;

&lt;p&gt;The same error appears in the browser console in dev mode when you navigate to the page directly, and it appears in your CI logs the moment you run &lt;code&gt;next build&lt;/code&gt; against a route that violates the invariant. The official documentation page that the error links to is &lt;code&gt;https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout&lt;/code&gt;, and that page is the only place I trust for the canonical wording — the message has been stable across the 15.x line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Next.js refuses to statically render useSearchParams
&lt;/h2&gt;

&lt;p&gt;The hook reads the current URL's query string. At build time there is no browser and no URL — there is only a render function being asked to produce HTML for a route that may eventually be visited with any query string at all. Next.js handles this with a two-phase strategy. During prerender it renders a static shell, and during client hydration the URL is read and the actual UI is filled in. The mechanism that lets that two-phase render happen is &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;: the boundary tells the prerender pass "you can stop here, emit a fallback, and let the rest resolve on the client."&lt;/p&gt;

&lt;p&gt;Without a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary above the call, the prerender pass has no instruction for how to stop. It would have to either invent a value for the search params (which would be wrong) or refuse to prerender the page at all. Next.js chooses the second option, which is why the build fails loudly rather than shipping a broken page. The same mechanism is what lets a Server Component fetch data and a Client Component read the URL in the same tree, as long as the Client Component is wrapped.&lt;/p&gt;

&lt;p&gt;The reason this matters beyond just copying the fix is that the same invariant shows up in other forms. If you ever hit &lt;code&gt;Dynamic Server Usage: couldn't be rendered statically because it used cookies&lt;/code&gt; or &lt;code&gt;headers&lt;/code&gt;, that is the same boundary idea, and the fix is structurally identical — narrow the dynamic read to the smallest possible client subtree. I unpack that case in &lt;a href="https://www.iloveblogs.blog/post/nextjs-dynamic-server-usage-couldnt-be-rendered-statically-fix" rel="noopener noreferrer"&gt;Fix Dynamic Server Usage Error in Next.js App Router&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: split the page and wrap the reads in a Suspense boundary
&lt;/h2&gt;

&lt;p&gt;The minimal change is to take any file that has &lt;code&gt;"use client"&lt;/code&gt; at the top and calls &lt;code&gt;useSearchParams&lt;/code&gt;, and refactor it so the page itself is a Server Component shell that hands the search-reading work to a child Client Component inside &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;. The Server Component renders the static output, the Suspense boundary is the marker that tells the prerender pass where to stop, and the child Client Component hydrates on the client with the real URL.&lt;/p&gt;

&lt;p&gt;Here is the broken version that throws during build. Do not ship it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/(shop)/products/page.tsx&lt;/span&gt;
&lt;span class="c1"&gt;// ❌ Throws "useSearchParams() should be wrapped in a suspense boundary".&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSearchParams&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="s2"&gt;next/navigation&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;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductsPage&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;sp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSearchParams&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;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sp&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="s2"&gt;category&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;all&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Products&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Filtering by: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;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;And here is the fix. The page becomes a Server Component, the search-reading logic moves to a child file marked &lt;code&gt;"use client"&lt;/code&gt;, and that child is rendered inside a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary with a sensible fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/(shop)/products/page.tsx&lt;/span&gt;
&lt;span class="c1"&gt;// ✅ Server Component shell. Renders to the static output at build time.&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;Suspense&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="s2"&gt;react&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;ProductsFilter&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./products-filter&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Products&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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductsPage&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Products&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Loading filters…&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProductsFilter&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/(shop)/products/products-filter.tsx&lt;/span&gt;
&lt;span class="c1"&gt;// ✅ Client Component. Called inside a &amp;lt;Suspense&amp;gt; boundary, so the&lt;/span&gt;
&lt;span class="c1"&gt;// prerender pass can emit the fallback and hydrate on the client.&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSearchParams&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="s2"&gt;next/navigation&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;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductsFilter&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;sp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSearchParams&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;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sp&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="s2"&gt;category&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;all&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Filtering by: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&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;Two things stand out about this diff. First, the &lt;code&gt;page.tsx&lt;/code&gt; no longer has &lt;code&gt;"use client"&lt;/code&gt;, so it runs on the server and gets the full Server Component benefits — &lt;code&gt;metadata&lt;/code&gt; exports work, server-only imports work, and the route is eligible for static prerendering. Second, the fallback you pass to &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; is what actually ships in the static HTML, so it should be a real loading state, not an empty fragment. A short paragraph or skeleton is usually enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-step: ship it without losing the static shell
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Find every file in your &lt;code&gt;app/&lt;/code&gt; tree that calls &lt;code&gt;useSearchParams&lt;/code&gt; from &lt;code&gt;next/navigation&lt;/code&gt;. Grep for it with &lt;code&gt;rg "useSearchParams" app components&lt;/code&gt; to get a list in one shot.&lt;/li&gt;
&lt;li&gt;For each one, check whether the file has &lt;code&gt;"use client"&lt;/code&gt; at the top. If the page itself is a Client Component, decide whether it really needs to be — most pages do not.&lt;/li&gt;
&lt;li&gt;Refactor the file so the page exports a Server Component, and the search-reading logic moves to a sibling Client Component. The Server Component imports the child and renders it inside &lt;code&gt;&amp;lt;Suspense fallback={...}&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the child component was previously exported as the default of a page file, rename it to something like &lt;code&gt;*-view.tsx&lt;/code&gt; or &lt;code&gt;*-filter.tsx&lt;/code&gt; and update the import in &lt;code&gt;page.tsx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Re-run &lt;code&gt;next build&lt;/code&gt; and confirm the offending route shows the &lt;code&gt;○&lt;/code&gt; marker in the route table, which means it was prerendered. If the marker is &lt;code&gt;λ&lt;/code&gt;, the Suspense boundary is not doing its job and the route is still dynamic.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Verify the route stayed static
&lt;/h2&gt;

&lt;p&gt;The fastest way to confirm the fix is to read the route table that &lt;code&gt;next build&lt;/code&gt; prints. Run a clean production build and look for the marker next to your route:&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. Clean any stale build output so the route table is fresh.&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; .next

&lt;span class="c"&gt;# 2. Run the production build and capture the full route table.&lt;/span&gt;
npx next build

&lt;span class="c"&gt;# 3. Look for the route marker. ○ = static, λ = dynamic, ƒ = function.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output you want looks like this. The &lt;code&gt;○&lt;/code&gt; (hollow circle) marker is the indicator that the route was prerendered at build time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Route (app)                                Size     First Load JS
┌ ƒ /api/products                          0 B            0 B
├ ○ /                                      123 B          82 kB
├ ○ /products                              1.4 kB         89 kB
└ ○ /_not-found                            871 B          82 kB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see &lt;code&gt;λ&lt;/code&gt; (lambda) instead of &lt;code&gt;○&lt;/code&gt; next to &lt;code&gt;/products&lt;/code&gt;, the route is being treated as dynamic, which usually means a Server Component somewhere in the tree is reading &lt;code&gt;searchParams&lt;/code&gt;, &lt;code&gt;cookies()&lt;/code&gt;, or &lt;code&gt;headers()&lt;/code&gt;. The &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary does not rescue that case — it only helps when the reads are confined to a Client Component subtree. If your build still shows &lt;code&gt;λ&lt;/code&gt; after the refactor, walk the tree again and check for stray &lt;code&gt;cookies()&lt;/code&gt; or &lt;code&gt;headers()&lt;/code&gt; calls.&lt;/p&gt;

&lt;p&gt;A second useful check is the prerendered HTML itself. Inspect the page source after deployment and confirm the Suspense fallback is in the static output, not the search-reading markup. The fallback is what search engines and link-preview bots see, so it should be a sensible message rather than an empty fragment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two patterns that still trip you up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The whole page is &lt;code&gt;"use client"&lt;/code&gt;.&lt;/strong&gt; This is the most common cause I see in code reviews. A developer adds &lt;code&gt;"use client"&lt;/code&gt; to a page file because they need one tiny bit of interactivity, and the whole page becomes a Client Component. The fix still works inside the same file — you can wrap the search-reading JSX in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; directly — but you lose Server Component benefits for that route, including the &lt;code&gt;metadata&lt;/code&gt; export, server-only imports, and the ability to call Supabase from the page itself. If the page is on a Supabase stack and the page itself should be able to fetch from the database, split it. The page becomes a Server Component, the search-reading JSX moves to a child file marked &lt;code&gt;"use client"&lt;/code&gt;, and the rest works as in the diff above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary is below the &lt;code&gt;useSearchParams&lt;/code&gt; call.&lt;/strong&gt; Boundaries do not work retroactively. If your tree is &lt;code&gt;&amp;lt;ProductsPage&amp;gt;&lt;/code&gt; (Client Component, calls &lt;code&gt;useSearchParams&lt;/code&gt;) → &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; → &lt;code&gt;&amp;lt;Child&amp;gt;&lt;/code&gt;, the prerender pass has already hit the hook before it sees the boundary, and you still get the build error. The boundary has to be above the call, which usually means the hook and the boundary live in the same Client Component subtree, or the page itself is the boundary's parent.&lt;/p&gt;

&lt;p&gt;A related case is reading &lt;code&gt;searchParams&lt;/code&gt; in a Server Component, which does not require &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; at all. The &lt;code&gt;searchParams&lt;/code&gt; prop on a Server Component page is fully resolved at request time, and the page is then marked dynamic. That is a different problem from this one — I cover it separately in &lt;a href="https://www.iloveblogs.blog/post/how-to-get-query-string-parameters-in-nextjs" rel="noopener noreferrer"&gt;How to get query string parameters in Next.js&lt;/a&gt; — but the two errors often get conflated, so it pays to be clear about which one you are actually hitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happens and how to keep it from coming back
&lt;/h2&gt;

&lt;p&gt;The underlying invariant is simple: any Client Component that reads runtime browser state (URL, &lt;code&gt;window&lt;/code&gt;, &lt;code&gt;localStorage&lt;/code&gt;) must live inside a boundary that the prerender pass knows how to stop at. &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; is that boundary, and the hook contract on &lt;code&gt;useSearchParams&lt;/code&gt; is that the caller guarantees one is present. There is no point in trying to read &lt;code&gt;useSearchParams&lt;/code&gt; outside a boundary — the hook has no way to return a value during prerender, so the only correct options are the boundary or making the route dynamic.&lt;/p&gt;

&lt;p&gt;The cheapest way to keep this from regressing is to add a build step to CI that runs &lt;code&gt;next build&lt;/code&gt; and fails the pipeline on this specific error string. I have a short script for this in &lt;a href="https://www.iloveblogs.blog/hubs/nextjs-supabase" rel="noopener noreferrer"&gt;Next.js + Supabase CI/CD Pipelines with GitHub Actions&lt;/a&gt;, and the same pattern works whether the destination is Vercel, Fly, or a plain Node server. The other half of prevention is the file-level rule of thumb: if a file in &lt;code&gt;app/&lt;/code&gt; calls &lt;code&gt;useSearchParams&lt;/code&gt;, the file that imports it must be a Server Component that wraps the import in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;. Code review catches this fast, and &lt;code&gt;rg "useSearchParams" app&lt;/code&gt; should be a routine command in your day-to-day work.&lt;/p&gt;

&lt;p&gt;If your stack ships to production behind a build step — and on a Next.js + Supabase project it should, because the static output is what makes the edge fast — make this rule a checklist item in the deployment guide. I keep the production checklist in &lt;a href="https://www.iloveblogs.blog/guides/deploying-nextjs-supabase-production" rel="noopener noreferrer"&gt;Deploying Next.js + Supabase to Production&lt;/a&gt;, and the Suspense invariant is one of the items I check before a green build.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What does "useSearchParams() should be wrapped in a suspense boundary" mean in plain English?&lt;/strong&gt; It means a Client Component in your tree is reading the URL during a render that Next.js is trying to prerender, and there is no &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary above the call to tell the prerender pass where to stop. Add the boundary, with a fallback, and the route can ship as a static page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I just make the route dynamic and skip the Suspense boundary?&lt;/strong&gt; Yes. Adding &lt;code&gt;export const dynamic = "force-dynamic"&lt;/code&gt; to the page, or reading &lt;code&gt;searchParams&lt;/code&gt; in a Server Component, both turn off static prerendering for that route. The trade-off is real: you lose the static cache and the page renders on every request. Wrap the reads in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; if you can; fall back to dynamic only if the page genuinely cannot be split.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the Suspense fallback hurt SEO or accessibility?&lt;/strong&gt; No. The fallback is what ships in the static HTML, and search engines index the static HTML. The search-reading subtree hydrates on the client and replaces the fallback with the real content. Make the fallback descriptive — "Loading filters…" is fine — and you do not lose anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I added Suspense and the build still throws. What now?&lt;/strong&gt; Walk the tree above the failing call. The boundary must be a parent of the component that calls the hook, not a sibling or a child. If the page itself is &lt;code&gt;"use client"&lt;/code&gt;, the boundary inside the same file works but you lose Server Component benefits — split the file. If the page is a Server Component, the boundary must wrap the child that calls the hook, not the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there a way to read the query string without a Client Component at all?&lt;/strong&gt; Yes. In the App Router, a Server Component page receives &lt;code&gt;searchParams&lt;/code&gt; as a prop. That prop is fully resolved at request time, so the page becomes dynamic, but you do not need &lt;code&gt;useSearchParams&lt;/code&gt; and you do not need &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;. Use the prop when the whole page is request-dependent; use &lt;code&gt;useSearchParams&lt;/code&gt; inside &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; when only a small subtree is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/fix-nextjs-missing-suspense-boundary-use-searchparams" rel="noopener noreferrer"&gt;Missing Suspense Boundary with useSearchParams (Next.js)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/how-to-get-query-string-parameters-in-nextjs" rel="noopener noreferrer"&gt;How to get query string parameters in Next.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/nextjs-dynamic-server-usage-couldnt-be-rendered-statically-fix" rel="noopener noreferrer"&gt;Fix Dynamic Server Usage Error in Next.js App Router&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/deploying-nextjs-supabase-production" rel="noopener noreferrer"&gt;Deploying Next.js + Supabase to Production&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/hubs/nextjs-supabase" rel="noopener noreferrer"&gt;Next.js + Supabase CI/CD Pipelines with GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.iloveblogs.blog/post/nextjs-usesearchparams-suspense-static-rendering-docs" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>suspense</category>
      <category>troubleshooting</category>
      <category>staticrendering</category>
    </item>
    <item>
      <title>Sign In to Supabase Without Email Verification (2026)</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Fri, 21 Aug 2026 14:31:47 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/sign-in-to-supabase-without-email-verification-2026-1o53</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/sign-in-to-supabase-without-email-verification-2026-1o53</guid>
      <description>&lt;h2&gt;
  
  
  The friction
&lt;/h2&gt;

&lt;p&gt;You are building locally. You sign up a test user, Supabase replies&lt;br&gt;
"Check your email to confirm your account", and now you have to open the&lt;br&gt;
inbox, click the link, and only then can you log in. Every. Single. Iteration.&lt;/p&gt;

&lt;p&gt;On hosted Supabase, email confirmation is &lt;strong&gt;on by default&lt;/strong&gt;. On self-hosted&lt;br&gt;
and local (&lt;code&gt;supabase start&lt;/code&gt;) it is &lt;strong&gt;off by default&lt;/strong&gt;. So the friction is worst&lt;br&gt;
when you are developing against a hosted project — which is most people.&lt;/p&gt;

&lt;p&gt;The original ask for this is &lt;a href="https://github.com/supabase/supabase/issues/5113" rel="noopener noreferrer"&gt;supabase/supabase#5113&lt;/a&gt;:&lt;br&gt;
a flag to let users sign in without email confirmation. The flag exists in&lt;br&gt;
three forms, depending on whether you want to disable it globally or just&lt;br&gt;
confirm one user.&lt;/p&gt;
&lt;h2&gt;
  
  
  Option 1 — Turn it off in the dashboard (hosted dev)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Dashboard → Authentication → Sign In / Providers → Email → Confirm email:&lt;br&gt;
OFF&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;URL: &lt;code&gt;https://supabase.com/dashboard/project/_/auth/providers&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;New signups can now sign in immediately. No confirmation email is sent.&lt;/p&gt;

&lt;p&gt;This is project-wide. It affects &lt;strong&gt;every&lt;/strong&gt; signup on that project, so use it&lt;br&gt;
only on a dev project, not production.&lt;/p&gt;
&lt;h2&gt;
  
  
  Option 2 — Turn it off in config (self-hosted / local)
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;supabase/config.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[auth.email]&lt;/span&gt;
&lt;span class="py"&gt;enable_confirmations&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart the auth container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;supabase stop &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; supabase start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Self-hosted and local projects ship with &lt;code&gt;enable_confirmations = false&lt;/code&gt;, so&lt;br&gt;
you only need this if you previously turned it on.&lt;/p&gt;
&lt;h2&gt;
  
  
  Option 3 — Confirm one user with the admin API (production-safe)
&lt;/h2&gt;

&lt;p&gt;When you want to keep confirmation on globally but pre-confirm one user — a&lt;br&gt;
team member, an invited customer, a test account — use the admin API with the&lt;br&gt;
&lt;strong&gt;service role key&lt;/strong&gt;:&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;// server-only — NEVER expose the service role key to the browser&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;createClient&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;@supabase/supabase-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;supabaseAdmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&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;NEXT_PUBLIC_SUPABASE_URL&lt;/span&gt;&lt;span class="o"&gt;!&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;SUPABASE_SERVICE_ROLE_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// service role, not anon&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="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;supabaseAdmin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;teammate@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a-strong-temporary-password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;email_confirm&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;// ✅ user is already confirmed, no email sent&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;email_confirm: true&lt;/code&gt; writes &lt;code&gt;email_confirmed_at&lt;/code&gt; for you. The user can sign&lt;br&gt;
in immediately. This is the right tool for production invites.&lt;/p&gt;
&lt;h2&gt;
  
  
  Option 4 — Confirm an existing user in SQL
&lt;/h2&gt;

&lt;p&gt;For a user who already exists but is stuck unconfirmed:&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;update&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;users&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;email_confirmed_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;confirmed_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&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;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'teammate@example.com'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this in the SQL editor with the service role. It is the surgical option —&lt;br&gt;
no setting changes, no API call, just flip the column. Useful when a&lt;br&gt;
confirmation email bounced or a user lost access to their inbox.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Do not expose this as a client-callable function. Anyone who can run it&lt;br&gt;
against &lt;code&gt;auth.users&lt;/code&gt; can confirm any email — which is account takeover. It&lt;br&gt;
is a server-side escape hatch, not a feature.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When you must keep confirmation on
&lt;/h2&gt;

&lt;p&gt;In production, keep email confirmation &lt;strong&gt;on&lt;/strong&gt;. Three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Account takeover.&lt;/strong&gt; Without confirmation, anyone can sign up with an
email they do not own and lock the real owner out of that address on your
app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spam signups.&lt;/strong&gt; Bots fill your &lt;code&gt;auth.users&lt;/code&gt; table with garbage, which
costs you rows and skews analytics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Abuse of free tier.&lt;/strong&gt; Unverified signups are the main vector for
Supabase free-tier quota abuse; a project with confirmation off is a
magnet for it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The production pattern: confirmation &lt;strong&gt;on&lt;/strong&gt;, plus the admin API for pre-&lt;br&gt;
confirmed invites. Disable confirmation only on dev and staging projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The redirect after sign-in
&lt;/h2&gt;

&lt;p&gt;Once confirmation is off (or the user is confirmed), the next friction is&lt;br&gt;
where they land after &lt;code&gt;signInWithPassword&lt;/code&gt;. If the post-login redirect is&lt;br&gt;
broken, the user is authenticated but staring at the wrong page — see&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/supabase-auth-redirect-fix" rel="noopener noreferrer"&gt;fix Supabase auth redirects that land on the wrong URL&lt;/a&gt;.&lt;br&gt;
And if the session itself does not persist across refreshes, it is the&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/fix-supabase-auth-session-persistence" rel="noopener noreferrer"&gt;Supabase auth session persistence&lt;/a&gt;&lt;br&gt;
issue, not email confirmation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disabling confirmation in production.&lt;/strong&gt; It is a security control, not a
convenience. Use the admin API to pre-confirm individuals instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using the service role key in the browser.&lt;/strong&gt; The service role bypasses
RLS. If it ships in client code, your database is public. The
&lt;a href="https://www.iloveblogs.blog/fix/fix-supabase-getuser-warning" rel="noopener noreferrer"&gt;getSession() security warning&lt;/a&gt; covers the
same boundary from the read side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting the OAuth path still verifies.&lt;/strong&gt; Turning off email
confirmation does not affect OAuth providers — Google, GitHub already
verify the email upstream, so &lt;code&gt;email_confirmed_at&lt;/code&gt; is set on the OAuth
callback regardless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating &lt;code&gt;confirmed_at&lt;/code&gt; and &lt;code&gt;email_confirmed_at&lt;/code&gt; as different.&lt;/strong&gt; Set both
in SQL; Supabase reads both depending on the flow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hosted dev:&lt;/strong&gt; dashboard → Auth Providers → Email → Confirm email OFF.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local/self-hosted:&lt;/strong&gt; &lt;code&gt;auth.email.enable_confirmations = false&lt;/code&gt; in
&lt;code&gt;config.toml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production:&lt;/strong&gt; keep it ON. Pre-confirm individuals with the admin API
(&lt;code&gt;email_confirm: true&lt;/code&gt;) or SQL (&lt;code&gt;update auth.users set email_confirmed_at =
now()&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/fix/fix-supabase-getuser-warning" rel="noopener noreferrer"&gt;Stop the Supabase getSession() Security Warning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/supabase-auth-redirect-fix" rel="noopener noreferrer"&gt;Supabase Auth Redirect Fix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/fix-supabase-auth-session-persistence" rel="noopener noreferrer"&gt;Fix Supabase Auth Session Persistence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-ssr-session-management" rel="noopener noreferrer"&gt;Next.js + Supabase SSR Session Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-security-best-practices" rel="noopener noreferrer"&gt;Next.js + Supabase Security Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/debugging-supabase-rls-issues" rel="noopener noreferrer"&gt;Debug Supabase RLS Issues&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.iloveblogs.blog/post/fix-supabase-signin-without-email-verification" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>auth</category>
      <category>email</category>
      <category>dev</category>
    </item>
    <item>
      <title>Supabase "Database Error Saving New User" Trigger Fix</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Fri, 21 Aug 2026 14:31:04 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/supabase-database-error-saving-new-user-trigger-fix-3dh1</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/supabase-database-error-saving-new-user-trigger-fix-3dh1</guid>
      <description>&lt;h2&gt;
  
  
  The error
&lt;/h2&gt;

&lt;p&gt;You add a trigger to copy new auth users into a &lt;code&gt;public.users&lt;/code&gt; (or&lt;br&gt;
&lt;code&gt;public.profiles&lt;/code&gt;) table. The first signup after that throws a vague error on&lt;br&gt;
the client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database error saving new user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No user row in &lt;code&gt;auth.users&lt;/code&gt;. No row in &lt;code&gt;public.users&lt;/code&gt;. The signup is dead. The&lt;br&gt;
message is intentionally generic — Supabase hides the underlying Postgres&lt;br&gt;
error from the client — but the cause is almost always the same: &lt;strong&gt;your&lt;br&gt;
trigger threw, and the transaction rolled back the auth insert with it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the exact scenario in &lt;a href="https://github.com/supabase/supabase/issues/563" rel="noopener noreferrer"&gt;supabase/supabase#563&lt;/a&gt;:&lt;br&gt;
an &lt;code&gt;AFTER INSERT&lt;/code&gt; trigger on &lt;code&gt;auth.users&lt;/code&gt; copies the row into &lt;code&gt;public.users&lt;/code&gt;,&lt;br&gt;
the copy fails for a constraint or permission reason, and the exception kills&lt;br&gt;
the signup.&lt;/p&gt;
&lt;h2&gt;
  
  
  The reproduction
&lt;/h2&gt;

&lt;p&gt;The trigger from that issue, lightly cleaned up:&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="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;replace&lt;/span&gt; &lt;span class="k"&gt;function&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;signup_copy_to_users&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt;
&lt;span class="k"&gt;language&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;
&lt;span class="k"&gt;security&lt;/span&gt; &lt;span class="k"&gt;definer&lt;/span&gt;
&lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&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="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="n"&gt;signup_copy&lt;/span&gt;
  &lt;span class="k"&gt;after&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;on&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;users&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;each&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;execute&lt;/span&gt; &lt;span class="k"&gt;function&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;signup_copy_to_users&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 fine. It is not. Three things go wrong with it in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AFTER INSERT&lt;/code&gt; runs in the same transaction.&lt;/strong&gt; Any exception in the body
rolls back the &lt;code&gt;auth.users&lt;/code&gt; insert. Signup dies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SECURITY DEFINER&lt;/code&gt; with no &lt;code&gt;search_path&lt;/code&gt;.&lt;/strong&gt; This is the
CVE-2018-1058 search-path hijacking pattern. A hostile user with &lt;code&gt;CREATE&lt;/code&gt;
on &lt;code&gt;public&lt;/code&gt; could shadow &lt;code&gt;public.users&lt;/code&gt; with a malicious function. Postgres
15 made &lt;code&gt;CREATE&lt;/code&gt; on &lt;code&gt;public&lt;/code&gt; non-default (see
&lt;a href="https://www.iloveblogs.blog/post/supabase-client-permission-denied-for-schema-public" rel="noopener noreferrer"&gt;Supabase "permission denied for schema public" fix&lt;/a&gt;),
but you still pin &lt;code&gt;search_path&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No error handling.&lt;/strong&gt; A single bad row — a NULL on a NOT NULL column, a
duplicate email, an RLS denial — breaks every signup until you fix it.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Two changes: pin the search path, and make the profile copy non-fatal.&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="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;replace&lt;/span&gt; &lt;span class="k"&gt;function&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;handle_new_user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt;
&lt;span class="k"&gt;language&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;
&lt;span class="k"&gt;security&lt;/span&gt; &lt;span class="k"&gt;definer&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;          &lt;span class="c1"&gt;-- ✅ pin search_path, block hijacking&lt;/span&gt;
&lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&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="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;exception&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;others&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="c1"&gt;-- ✅ log the failure but do NOT roll back auth.users&lt;/span&gt;
    &lt;span class="n"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="s1"&gt;'handle_new_user failed for %: %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sqlerrm&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="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Replace the old trigger&lt;/span&gt;
&lt;span class="k"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="n"&gt;signup_copy&lt;/span&gt; &lt;span class="k"&gt;on&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;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="n"&gt;on_auth_user_created&lt;/span&gt; &lt;span class="k"&gt;on&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;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="n"&gt;on_auth_user_created&lt;/span&gt;
  &lt;span class="k"&gt;after&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;on&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;users&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;each&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;execute&lt;/span&gt; &lt;span class="k"&gt;function&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;handle_new_user&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two edits that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;set search_path = public&lt;/code&gt;&lt;/strong&gt; on a &lt;code&gt;SECURITY DEFINER&lt;/code&gt; function is now
required best practice. Without it, the function resolves unqualified names
against the caller's &lt;code&gt;search_path&lt;/code&gt;, which an attacker can manipulate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;exception when others then return new&lt;/code&gt;&lt;/strong&gt; decouples the profile copy from
the auth insert. A failed copy logs the error and lets the signup proceed.
The user can sign in; you fix the orphaned profile later from the log.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can route the log to Supabase's log explorer or a &lt;code&gt;public.signup_errors&lt;/code&gt;&lt;br&gt;
table if you want to drive a backfill job from it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why SECURITY DEFINER at all
&lt;/h2&gt;

&lt;p&gt;Without &lt;code&gt;SECURITY DEFINER&lt;/code&gt;, the trigger runs as &lt;code&gt;auth.users&lt;/code&gt;'s owner on the&lt;br&gt;
&lt;code&gt;auth&lt;/code&gt; schema but inserts into &lt;code&gt;public.users&lt;/code&gt; as whoever the caller is. If&lt;br&gt;
&lt;code&gt;public.users&lt;/code&gt; has RLS, the &lt;code&gt;anon&lt;/code&gt; role may have no &lt;code&gt;INSERT&lt;/code&gt; policy, and the&lt;br&gt;
copy fails — rolling back the signup. &lt;code&gt;SECURITY DEFINER&lt;/code&gt; runs the function as&lt;br&gt;
its owner (typically the migrations role, which bypasses RLS), so the insert&lt;br&gt;
goes through.&lt;/p&gt;

&lt;p&gt;That is the same reason the &lt;code&gt;getSession()&lt;/code&gt; security warning exists: relying on&lt;br&gt;
the wrong privilege boundary. The rule for auth triggers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SECURITY DEFINER&lt;/code&gt;&lt;/strong&gt; so RLS on &lt;code&gt;public.users&lt;/code&gt; cannot block the copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SET search_path = public&lt;/code&gt;&lt;/strong&gt; so the definer cannot be hijacked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;exception when others&lt;/code&gt;&lt;/strong&gt; so a copy failure cannot kill a signup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the matching server-side rule on reads, see&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/fix/fix-supabase-getuser-warning" rel="noopener noreferrer"&gt;stop the Supabase getSession() security warning&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The RLS angle
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Debugging a policy instead of a trigger?&lt;/strong&gt; The free &lt;a href="https://www.iloveblogs.blog/tools/supabase-rls-debugger" rel="noopener noreferrer"&gt;Supabase RLS Policy Debugger&lt;/a&gt; matches the common failure signatures (WITH CHECK, recursion, GRANTs) and returns the corrected SQL.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If &lt;code&gt;public.users&lt;/code&gt; has RLS, the &lt;code&gt;handle_new_user&lt;/code&gt; insert runs as the function&lt;br&gt;
owner, not the new user. That is usually what you want — the new user has no&lt;br&gt;
row yet, so a policy like &lt;code&gt;using (id = auth.uid())&lt;/code&gt; would block the very insert&lt;br&gt;
that creates their row. Keep &lt;code&gt;SECURITY DEFINER&lt;/code&gt; so the trigger can seed the&lt;br&gt;
row, then let RLS govern subsequent access.&lt;/p&gt;

&lt;p&gt;A common misconfiguration: a &lt;code&gt;WITH CHECK (id = auth.uid())&lt;/code&gt; policy on&lt;br&gt;
&lt;code&gt;public.users&lt;/code&gt; that works for updates but makes the initial insert depend on&lt;br&gt;
&lt;code&gt;auth.uid()&lt;/code&gt; resolving before the user exists. The trigger runs after the&lt;br&gt;
&lt;code&gt;auth.users&lt;/code&gt; insert, so &lt;code&gt;auth.uid()&lt;/code&gt; is valid — but if you accidentally run&lt;br&gt;
the insert as &lt;code&gt;anon&lt;/code&gt; instead of via &lt;code&gt;SECURITY DEFINER&lt;/code&gt;, it fails. The&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/debugging-supabase-rls-issues" rel="noopener noreferrer"&gt;Supabase RLS debugging guide&lt;/a&gt; covers&lt;br&gt;
the &lt;code&gt;auth.uid()&lt;/code&gt; resolution order in depth.&lt;/p&gt;
&lt;h2&gt;
  
  
  Check the target table first
&lt;/h2&gt;

&lt;p&gt;Before blaming the trigger, confirm the target table is insert-able in&lt;br&gt;
isolation:&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;-- As the migrations role, not anon:&lt;/span&gt;
&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'00000000-0000-0000-0000-000000000000'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'test@example.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that fails, the trigger is not your problem — the table is. Common causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;NOT NULL&lt;/code&gt; column without a default that the trigger does not populate.&lt;/li&gt;
&lt;li&gt;A unique constraint on a column you are not setting.&lt;/li&gt;
&lt;li&gt;A foreign key to a row that does not exist.&lt;/li&gt;
&lt;li&gt;A column type mismatch (e.g. &lt;code&gt;text&lt;/code&gt; vs &lt;code&gt;uuid&lt;/code&gt; on &lt;code&gt;id&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix the table, then the trigger stops throwing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production recommendations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deploy the trigger, then test signup immediately&lt;/strong&gt; with a throwaway email.
A broken trigger breaks every signup, so verify before you walk away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not run DDL that recreates the trigger during a signup burst.&lt;/strong&gt; The
&lt;code&gt;drop trigger&lt;/code&gt; / &lt;code&gt;create trigger&lt;/code&gt; pair has a tiny window where inserts have
no trigger; that is usually fine, but it does not need to be in your hot path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log exceptions to a table you monitor&lt;/strong&gt;, not just &lt;code&gt;raise log&lt;/code&gt;. The log
explorer is easy to miss; a &lt;code&gt;signup_errors&lt;/code&gt; table with a Slack nudge is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin &lt;code&gt;search_path&lt;/code&gt; on every &lt;code&gt;SECURITY DEFINER&lt;/code&gt; function&lt;/strong&gt;, not just this
one. It is the cheapest security win in Supabase. The
&lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-security-best-practices" rel="noopener noreferrer"&gt;Supabase security best practices guide&lt;/a&gt;
treats this as a baseline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&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="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;replace&lt;/span&gt; &lt;span class="k"&gt;function&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;handle_new_user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt;
&lt;span class="k"&gt;language&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;
&lt;span class="k"&gt;security&lt;/span&gt; &lt;span class="k"&gt;definer&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;
&lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&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="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;exception&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;others&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="n"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="s1"&gt;'handle_new_user %: %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sqlerrm&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="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- never let the copy roll back auth.users&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="n"&gt;on_auth_user_created&lt;/span&gt;
  &lt;span class="k"&gt;after&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;on&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;users&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;each&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;execute&lt;/span&gt; &lt;span class="k"&gt;function&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;handle_new_user&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things: pin the search path, use &lt;code&gt;SECURITY DEFINER&lt;/code&gt; so RLS cannot block&lt;br&gt;
the seed insert, and swallow exceptions so a copy failure never breaks a signup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/fix/fix-supabase-getuser-warning" rel="noopener noreferrer"&gt;Stop the Supabase getSession() Security Warning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/debugging-supabase-rls-issues" rel="noopener noreferrer"&gt;Debug Supabase RLS Issues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/supabase-client-permission-denied-for-schema-public" rel="noopener noreferrer"&gt;Supabase "permission denied for schema public" Fix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-security-best-practices" rel="noopener noreferrer"&gt;Next.js + Supabase Security Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-database-design-optimization" rel="noopener noreferrer"&gt;Next.js + Supabase Database Design and Optimization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-ssr-session-management" rel="noopener noreferrer"&gt;Next.js + Supabase SSR Session Management&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.iloveblogs.blog/post/fix-supabase-database-error-saving-new-user-trigger" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>auth</category>
      <category>trigger</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Next.js "Server Is Running Out of Memory" Fix (2026)</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Fri, 21 Aug 2026 14:30:58 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/nextjs-server-is-running-out-of-memory-fix-2026-2im0</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/nextjs-server-is-running-out-of-memory-fix-2026-2im0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Shortcut:&lt;/strong&gt; paste your failing output into the free &lt;a href="https://www.iloveblogs.blog/tools/nextjs-build-error-decoder" rel="noopener noreferrer"&gt;Next.js Build Error Decoder&lt;/a&gt; — it recognizes the heap-out-of-memory signature (and 12 others) and links straight to the fix that applies.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The message
&lt;/h2&gt;

&lt;p&gt;You are running &lt;code&gt;next dev&lt;/code&gt; (or a Node production server), the terminal prints&lt;br&gt;
this, and the server restarts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The server is running out of memory, restarting to free up memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Routes reload slowly, sessions drop, and in dev your HMR stops working for a&lt;br&gt;
few seconds every time it fires. This was filed as&lt;br&gt;
&lt;a href="https://github.com/vercel/next.js/issues/46756" rel="noopener noreferrer"&gt;vercel/next.js#46756&lt;/a&gt; (dev&lt;br&gt;
App Router + RSC accumulation) and tracked as NEXT-1353, with the production&lt;br&gt;
side in &lt;a href="https://github.com/vercel/next.js/issues/49929" rel="noopener noreferrer"&gt;vercel/next.js#49929&lt;/a&gt;&lt;br&gt;
(NEXT-1314, high memory in deployed projects).&lt;/p&gt;

&lt;p&gt;The message is a &lt;strong&gt;symptom&lt;/strong&gt;, not a diagnosis. Next.js notices the Node heap&lt;br&gt;
crossed its limit and restarts to recover. Your job is to find out &lt;em&gt;why&lt;/em&gt; it&lt;br&gt;
crossed — and there are only three possible buckets.&lt;/p&gt;
&lt;h2&gt;
  
  
  The three causes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dev-mode accumulation&lt;/strong&gt; — the classic App Router + RSC hot-reload leak
in Next.js 13.x. Repeated refreshes grow the heap until it restarts. This
is a tooling problem, not your code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real leak in your app or deps&lt;/strong&gt; — an in-memory cache without eviction,
event listeners never removed, a closure holding a growing dataset, a
&lt;code&gt;Map&lt;/code&gt; that never gets pruned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A legitimate heap that is too big for the limit&lt;/strong&gt; — a route that loads a
large dataset into memory at build time, a heavy &lt;code&gt;getStaticProps&lt;/code&gt; /
&lt;code&gt;generateStaticParams&lt;/code&gt; job, or sourcemaps in production.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You diagnose by bucket. The fixes are different.&lt;/p&gt;
&lt;h2&gt;
  
  
  First, stop the restarts (band-aid)
&lt;/h2&gt;

&lt;p&gt;Raise the V8 heap limit so you can actually work while you investigate:&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;# 4 GB heap (dev)&lt;/span&gt;
&lt;span class="nv"&gt;NODE_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;--max-old-space-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4096 npm run dev

&lt;span class="c"&gt;# 8 GB heap (large production builds)&lt;/span&gt;
&lt;span class="nv"&gt;NODE_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;--max-old-space-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8192 npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a fix. It is the room you need to take a heap snapshot instead of&lt;br&gt;
getting restarted out of the inspector. Set it, then move to diagnosis.&lt;/p&gt;
&lt;h2&gt;
  
  
  If it only happens in dev — switch to Turbopack
&lt;/h2&gt;

&lt;p&gt;The 13.x dev-mode leak in webpack's App Router pipeline is the single most&lt;br&gt;
common cause of this message. If you are on Next.js 14+ and still hitting it&lt;br&gt;
in dev, try Turbopack first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;next dev &lt;span class="nt"&gt;--turbo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turbopack's module graph does not accumulate on hot reload the way webpack&lt;br&gt;
did, so for the classic dev leak it resolves immediately. This is the lowest-&lt;br&gt;
effort, highest-hit fix in this whole article.&lt;/p&gt;

&lt;p&gt;If you are on an older 13.x and cannot move to Turbopack, upgrade to the&lt;br&gt;
latest stable — the dev accumulation was steadily reduced across 13.x and&lt;br&gt;
largely resolved in 14+. Do not stay on a leaking canary.&lt;/p&gt;
&lt;h2&gt;
  
  
  Diagnose a real leak with a heap snapshot
&lt;/h2&gt;

&lt;p&gt;If Turbopack did not help, or the leak is in production, take a heap snapshot.&lt;br&gt;
Run the dev server under the inspector:&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="nv"&gt;NODE_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'--inspect'&lt;/span&gt; npm run dev
&lt;span class="c"&gt;# or for a production Node server:&lt;/span&gt;
&lt;span class="nv"&gt;NODE_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'--inspect'&lt;/span&gt; node server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;chrome://inspect&lt;/code&gt; → &lt;strong&gt;inspect&lt;/strong&gt; → &lt;strong&gt;Memory&lt;/strong&gt; tab → &lt;strong&gt;Heap snapshot&lt;/strong&gt;.&lt;br&gt;
Use the &lt;strong&gt;three-snapshot technique&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Snapshot A — after the app warms up.&lt;/li&gt;
&lt;li&gt;Snapshot B — after running the leaking flow once.&lt;/li&gt;
&lt;li&gt;Snapshot C — after running it again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the comparison view (C vs B vs A), objects that grow between every snapshot&lt;br&gt;
are your leak. Filter to "retained size" and look for your own domain objects&lt;br&gt;
(users, rows, requests) — if you see your own types growing, the leak is in&lt;br&gt;
your code. If you only see framework / node internals, the leak is upstream&lt;br&gt;
(or you are holding references to your data in a structure you forgot about).&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;server.js&lt;/code&gt;, log the heap on an interval to catch growth across requests:&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;// server.js (or a small instrumentation file)&lt;/span&gt;
&lt;span class="nf"&gt;setInterval&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;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memoryUsage&lt;/span&gt;&lt;span class="p"&gt;();&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`rss=&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rss&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1048576&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="s2"&gt;MB `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s2"&gt;`heapUsed=&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heapUsed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1048576&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="s2"&gt;MB `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s2"&gt;`heapTotal=&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heapTotal&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1048576&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="s2"&gt;MB `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s2"&gt;`external=&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;external&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1048576&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="s2"&gt;MB`&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;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A flat line means no leak. A steady climb that never settles means a leak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common app-level leaks
&lt;/h2&gt;

&lt;p&gt;The usual suspects, in order of how often I have seen them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Module-level &lt;code&gt;Map&lt;/code&gt; / &lt;code&gt;Set&lt;/code&gt; / array caches.&lt;/strong&gt; A &lt;code&gt;const cache = new Map()&lt;/code&gt;
at module scope that you write to on every request and never evict from.
Fix: bound it (an LRU), move it to a real cache (Redis / KV), or recompute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event listeners never removed.&lt;/strong&gt; A &lt;code&gt;setInterval&lt;/code&gt; or an event emitter you
attach on every request and never &lt;code&gt;removeListener&lt;/code&gt; / &lt;code&gt;clearInterval&lt;/code&gt;. Each
request leaks the listener and everything it closes over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closures holding large datasets.&lt;/strong&gt; A request handler that builds a
giant array inside a closure passed to a long-lived callback. The array
lives as long as the callback does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sourcemaps in production.&lt;/strong&gt; &lt;code&gt;productionBrowserSourceMaps: true&lt;/code&gt; or a
misconfigured build keeps large source content in memory. Disable for the
Node server, keep them only for client error reporting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy &lt;code&gt;generateStaticParams&lt;/code&gt; / build-time fetches.&lt;/strong&gt; Pulling a large
dataset into memory at build time to enumerate pages. Stream it from disk
or a DB cursor instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Production levers
&lt;/h2&gt;

&lt;p&gt;For deployed Next.js (Node server or serverless), memory is a resource you&lt;br&gt;
budget for. The levers, biggest first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Move large in-memory caches out of the process.&lt;/strong&gt; Anything that grows
with traffic belongs in Redis, Upstash, or your platform's KV, not in a
&lt;code&gt;Map&lt;/code&gt;. This is the single largest production win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stream large responses.&lt;/strong&gt; Do not &lt;code&gt;await&lt;/code&gt; an entire dataset into memory
then &lt;code&gt;JSON.stringify&lt;/code&gt; it. Stream it. &lt;code&gt;Response&lt;/code&gt; with a &lt;code&gt;ReadableStream&lt;/code&gt;,
or the &lt;code&gt;streaming&lt;/code&gt; render APIs, keep the working set small.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prune dependencies.&lt;/strong&gt; &lt;code&gt;next build&lt;/code&gt; shows the largest server modules.
A heavy dep loaded into the server bundle sits in memory for the life of
the process. Lazy-import the rare ones with &lt;code&gt;await import()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disable sourcemaps on the server.&lt;/strong&gt; They are for dev and error
reporting; shipping them on the server inflates resident memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raise the serverless memory budget.&lt;/strong&gt; On Vercel, serverless functions
have a per-function memory ceiling (1 GB on Hobby, more on Pro — confirm
on your plan). A route that genuinely needs more memory should be split
out and given its own configuration rather than dragging the whole app
up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the Edge Runtime for stateless routes.&lt;/strong&gt; Edge functions have a
smaller, fixed memory profile and no Node heap growth; move the routes
that are pure request/response work over. For the gotchas of what you
cannot run on Edge (no native TCP, no sync FS), the
&lt;a href="https://www.iloveblogs.blog/post/nextjs-dynamic-server-usage-couldnt-be-rendered-statically-fix" rel="noopener noreferrer"&gt;dynamic server usage / could not be rendered statically fix&lt;/a&gt;
covers the boundary.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The dev-vs-prod split
&lt;/h2&gt;

&lt;p&gt;A leak that only reproduces in &lt;code&gt;next dev&lt;/code&gt; and never in &lt;code&gt;next build&lt;/code&gt; / &lt;code&gt;next&lt;br&gt;
start&lt;/code&gt; is almost always the webpack dev pipeline. Do not spend two days&lt;br&gt;
hunting a leak in your code that is actually webpack's hot-reload graph —&lt;br&gt;
try Turbopack for thirty seconds first.&lt;/p&gt;

&lt;p&gt;A leak that reproduces in &lt;code&gt;next start&lt;/code&gt; against a production build is your&lt;br&gt;
code or your deps. Take the three-snapshot path above.&lt;/p&gt;

&lt;p&gt;And the failure mode that is not a leak at all: the build passes locally, you&lt;br&gt;
deploy, and it breaks in production for a different reason. That is the&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/nextjs-build-passes-production-broken-postmortem" rel="noopener noreferrer"&gt;build passes locally, broken in production&lt;/a&gt;&lt;br&gt;
postmortem, not this one. They look similar from the outside and have&lt;br&gt;
completely different causes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;NODE_OPTIONS=--max-old-space-size&lt;/code&gt; set for the build and the server.&lt;/li&gt;
&lt;li&gt;[ ] No module-level unbounded caches; every cache has an eviction policy
  or lives outside the process.&lt;/li&gt;
&lt;li&gt;[ ] No &lt;code&gt;setInterval&lt;/code&gt; / listeners without a matching teardown.&lt;/li&gt;
&lt;li&gt;[ ] Sourcemaps disabled on the Node server.&lt;/li&gt;
&lt;li&gt;[ ] Large routes streamed, not buffered.&lt;/li&gt;
&lt;li&gt;[ ] Heavy routes split out with their own memory budget if on serverless.&lt;/li&gt;
&lt;li&gt;[ ] Stateless routes on Edge where possible.&lt;/li&gt;
&lt;li&gt;[ ] An alert on &lt;code&gt;process.memoryUsage().heapUsed&lt;/code&gt; so you see growth before
  the restart message does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the broader performance posture — what to measure, what to move to Edge,&lt;br&gt;
how to keep the bundle thin — the&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/guides/nextjs-performance-optimization" rel="noopener noreferrer"&gt;Next.js performance optimization guide&lt;/a&gt;&lt;br&gt;
is the longer read. For the deploy-time failure modes that look like memory&lt;br&gt;
issues but are not (middleware not running, static assets 404), see&lt;br&gt;
&lt;a href="https://www.iloveblogs.blog/post/nextjs-middleware-not-running-vercel-production" rel="noopener noreferrer"&gt;middleware not running in Vercel production&lt;/a&gt;&lt;br&gt;
and the &lt;a href="https://www.iloveblogs.blog/guides/deploying-nextjs-supabase-production" rel="noopener noreferrer"&gt;deploying Next.js + Supabase to production&lt;/a&gt;&lt;br&gt;
guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Restarting on memory? Raise the heap with &lt;code&gt;NODE_OPTIONS=--max-old-space-size=4096&lt;/code&gt;
to stop the bleeding.&lt;/li&gt;
&lt;li&gt;Only in dev? Switch to &lt;code&gt;next dev --turbo&lt;/code&gt; — the classic 13.x webpack leak
is the most common cause and Turbopack sidesteps it.&lt;/li&gt;
&lt;li&gt;In production? Take three heap snapshots, find what grows, move caches out
of the process, stream large responses, disable server sourcemaps.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/nextjs-build-passes-production-broken-postmortem" rel="noopener noreferrer"&gt;Next.js Build Passes Locally, Broken in Production&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/nextjs-performance-optimization" rel="noopener noreferrer"&gt;Next.js Performance Optimization Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/nextjs-dynamic-server-usage-couldnt-be-rendered-statically-fix" rel="noopener noreferrer"&gt;Fix "Dynamic Server Usage: Could Not Be Rendered Statically"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/nextjs-middleware-not-running-vercel-production" rel="noopener noreferrer"&gt;Next.js Middleware Not Running in Vercel Production&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/deploying-nextjs-supabase-production" rel="noopener noreferrer"&gt;Deploying Next.js + Supabase to Production&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/nextjs-hydration-mismatch-fix" rel="noopener noreferrer"&gt;Fix Next.js Hydration Mismatch in App Router&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/fix-nextjs-missing-suspense-boundary-use-searchparams" rel="noopener noreferrer"&gt;Fix Missing Suspense Boundary with useSearchParams&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.iloveblogs.blog/post/fix-nextjs-server-running-out-of-memory-restarting" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>memory</category>
      <category>performance</category>
      <category>node</category>
    </item>
    <item>
      <title>Fix Next.js 'Cannot Have a Negative Time Stamp' Error</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Sun, 16 Aug 2026 08:03:09 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/fix-nextjs-cannot-have-a-negative-time-stamp-error-4bji</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/fix-nextjs-cannot-have-a-negative-time-stamp-error-4bji</guid>
      <description>&lt;p&gt;You refresh a page on your Turbopack dev server — usually one that just called &lt;code&gt;notFound()&lt;/code&gt; — and the dev overlay slams this in your face:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unhandled Runtime Error

Failed to execute 'measure' on 'Performance':
'NotFound' cannot have a negative time stamp.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The quoted component name varies: &lt;code&gt;'NotFound'&lt;/code&gt;, &lt;code&gt;'Page'&lt;/code&gt;, &lt;code&gt;'SlugPage'&lt;/code&gt; for a &lt;code&gt;[slug]/page.tsx&lt;/code&gt; route, or &lt;code&gt;'AdministrationPage [Prerender]'&lt;/code&gt; behind an auth guard. On Firefox the wording differs — &lt;code&gt;Performance.measure: Given attribute end cannot be negative&lt;/code&gt; — but it is the same failure. The stack trace points into &lt;code&gt;react-server-dom-turbopack-client.browser.development.js&lt;/code&gt;, and the error appears &lt;strong&gt;only in &lt;code&gt;next dev&lt;/code&gt; with Turbopack&lt;/strong&gt;. Your production build is fine.&lt;/p&gt;

&lt;p&gt;The bug is tracked upstream in &lt;a href="https://github.com/vercel/next.js/issues/86060" rel="noopener noreferrer"&gt;vercel/next.js issue #86060&lt;/a&gt;, first reported against Next.js 16.0.2-canary with React 19.2.0 and reproduced by commenters on stable 16.1.1 and 16.1.6. It is still open as of August 2026. Below are the triggers in descending order of how often they show up in that thread, each with a confirmation test and a fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 1: notFound() on a dynamic or catch-all route
&lt;/h2&gt;

&lt;p&gt;This is the original report and the most common trigger. React's development-only performance track initialises an internal &lt;code&gt;childrenEndTime&lt;/code&gt; variable to &lt;code&gt;-Infinity&lt;/code&gt;. When &lt;code&gt;notFound()&lt;/code&gt; interrupts rendering before any children are processed, that variable is never updated — and React then calls &lt;code&gt;performance.measure()&lt;/code&gt; with the still-negative value. The happy path guards against this (&lt;code&gt;0 &amp;lt;= childrenEndTime&lt;/code&gt;); the abort and rejection paths do not, which is exactly what &lt;a href="https://github.com/vercel/next.js/pull/88688" rel="noopener noreferrer"&gt;PR #88688&lt;/a&gt; identified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirm it:&lt;/strong&gt; hit a URL that does not exist under a dynamic segment — &lt;code&gt;/blog/does-not-exist&lt;/code&gt; for a &lt;code&gt;[slug]&lt;/code&gt; route with &lt;code&gt;notFound()&lt;/code&gt; in it. If the overlay error names your route's component (&lt;code&gt;'SlugPage'&lt;/code&gt;, &lt;code&gt;'NotFound'&lt;/code&gt;), this is your cause. One commenter noted the error is intermittent but becomes near-certain after editing &lt;code&gt;not-found.tsx&lt;/code&gt; and refreshing a few times — HMR makes the race more likely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; a commenter on the issue resolved it by moving &lt;code&gt;not-found.tsx&lt;/code&gt; &lt;strong&gt;out of the dynamic route folder into its parent directory&lt;/strong&gt;. The catch-all still resolves the 404 correctly (Next.js walks up the tree for the nearest &lt;code&gt;not-found&lt;/code&gt; file), but the render-abort no longer happens inside the segment whose timing React is measuring. If restructuring is not an option, jump to the workarounds section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 2: redirect() in a Server Component
&lt;/h2&gt;

&lt;p&gt;The second cluster of reports involves &lt;code&gt;redirect()&lt;/code&gt; rather than &lt;code&gt;notFound()&lt;/code&gt; — typically auth or admin guards that redirect before the page finishes rendering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/[locale]/admin/page.tsx&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;AdministrationPage&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;session&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;getSession&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;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;isAdmin&lt;/span&gt;&lt;span class="p"&gt;)&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// ← interrupts the render mid-measurement&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AdminDashboard&lt;/span&gt; &lt;span class="p"&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;The mechanism is the same: a server-side redirect completes before the component's performance measurement window closes, so the computed duration goes negative. A February 2026 comment on the issue pinned the throwing call to the &lt;code&gt;flushComponentPerformance&lt;/code&gt; function at line 3776 of the Turbopack development bundle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirm it:&lt;/strong&gt; the error names the guarded page (often with a &lt;code&gt;[Prerender]&lt;/code&gt; suffix) and fires on the initial load of a route that conditionally redirects — not on a 404.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; there is no code-level fix on your side that keeps the redirect logic intact; the guard is correct, React's dev instrumentation is not. Use one of the workarounds below. Do &lt;strong&gt;not&lt;/strong&gt; move the redirect into a &lt;code&gt;useEffect&lt;/code&gt; just to silence a dev-only overlay — that would ship a real flash-of-content regression to users to hide a cosmetic error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 3: next/dynamic with ssr: false on an older React
&lt;/h2&gt;

&lt;p&gt;An earlier variant of the same error came from &lt;code&gt;next/dynamic&lt;/code&gt; components with &lt;code&gt;{ ssr: false }&lt;/code&gt; inside client components. There, a fiber that never completed its work phase kept its initial &lt;code&gt;startTime&lt;/code&gt; of &lt;code&gt;-1.1&lt;/code&gt;, and React's &lt;code&gt;logComponentMount&lt;/code&gt; fed that into &lt;code&gt;performance.measure()&lt;/code&gt;. React fixed this one upstream in &lt;a href="https://github.com/react/react/pull/32823" rel="noopener noreferrer"&gt;facebook/react PR #32823&lt;/a&gt;, merged in April 2025.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirm it:&lt;/strong&gt; you are on a React 19 build from before mid-2025 (check &lt;code&gt;npm ls react&lt;/code&gt;) and the error appears when a &lt;code&gt;dynamic(() =&amp;gt; import(...), { ssr: false })&lt;/code&gt; component mounts — no &lt;code&gt;notFound()&lt;/code&gt; or &lt;code&gt;redirect()&lt;/code&gt; involved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; upgrade Next.js, which pins a patched React. If you are on Next.js 15.3+ or any 16.x release, this variant is already fixed and your error is Cause 1 or 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 4: Firefox timer precision makes it worse
&lt;/h2&gt;

&lt;p&gt;A July 2026 comment on the issue added a browser dimension: Firefox ships &lt;code&gt;privacy.reduceTimerPrecision&lt;/code&gt; enabled by default, which rounds &lt;code&gt;performance.now()&lt;/code&gt; values. That rounding makes tiny negative deltas between two nearly simultaneous timestamps far more likely, so Firefox users hit the error more often than Chrome users on identical code — one Next.js maintainer could not even reproduce the original report in Chrome on macOS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirm it:&lt;/strong&gt; the error fires in Firefox but not Chrome on the same route, with Firefox's own wording (&lt;code&gt;Given attribute end cannot be negative&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; none needed beyond the workarounds below — but if your team is split on "works on my machine", the browser difference is why. This is also worth knowing before you burn an afternoon profiling: the measurement that crashes is React's internal component track, not anything from your own code, so tools from our &lt;a href="https://www.iloveblogs.blog/guides/nextjs-performance-optimization" rel="noopener noreferrer"&gt;Next.js performance optimisation guide&lt;/a&gt; will show nothing wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the fix stands upstream
&lt;/h2&gt;

&lt;p&gt;As of August 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/vercel/next.js/issues/86060" rel="noopener noreferrer"&gt;Issue #86060&lt;/a&gt; is &lt;strong&gt;open&lt;/strong&gt; and triaged by the Next.js team (labelled for their internal tracker).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/vercel/next.js/pull/88688" rel="noopener noreferrer"&gt;PR #88688&lt;/a&gt;, which added the missing &lt;code&gt;0 &amp;lt;= childrenEndTime&lt;/code&gt; guard to the error and abort paths in Next.js's vendored React files, was &lt;strong&gt;closed unmerged&lt;/strong&gt; in January 2026. Maintainer &lt;a class="mentioned-user" href="https://dev.to/eps1lon"&gt;@eps1lon&lt;/a&gt;'s reasoning: patching the compiled output masks the symptom, and the real fix belongs in facebook/react.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ssr: false&lt;/code&gt; variant was fixed in &lt;a href="https://github.com/react/react/pull/32823" rel="noopener noreferrer"&gt;react#32823&lt;/a&gt; (April 2025) and has long since shipped inside Next.js.&lt;/li&gt;
&lt;li&gt;No stable Next.js release fixes the &lt;code&gt;notFound()&lt;/code&gt;/&lt;code&gt;redirect()&lt;/code&gt; variant yet. Upgrading to the latest canary is worth a try after each React sync, but as of 16.1.6 the reports keep coming.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So unlike a &lt;a href="https://www.iloveblogs.blog/post/nextjs-turbopack-stuck-fix" rel="noopener noreferrer"&gt;stuck Turbopack compile&lt;/a&gt;, you cannot currently upgrade your way out of this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workarounds while you wait
&lt;/h2&gt;

&lt;p&gt;Three options, all confirmed by commenters on the issue, all dev-only:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Run the dev server on webpack.&lt;/strong&gt; Several people independently confirmed this eliminates the error, because the unguarded measure call lives in the Turbopack-specific bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;next dev &lt;span class="nt"&gt;--webpack&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trade-off is real — you give up Turbopack's faster HMR — so treat it as the fallback, not the default. (Note the flag is &lt;code&gt;--webpack&lt;/code&gt;; there is no environment variable that disables Turbopack.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Relocate &lt;code&gt;not-found.tsx&lt;/code&gt;.&lt;/strong&gt; If your trigger is Cause 1, moving the file from inside the dynamic segment (&lt;code&gt;app/[slug]/not-found.tsx&lt;/code&gt;) up to its parent (&lt;code&gt;app/not-found.tsx&lt;/code&gt;) resolved it for at least one reporter, with 404 behaviour unchanged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Patch &lt;code&gt;performance.measure&lt;/code&gt; in development.&lt;/strong&gt; The first comment on the issue shares this approach: wrap the native method and swallow only the negative-timestamp failure. Load it from your root layout, guarded so it can never reach production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// instrumentation-client.ts (or imported in app/layout.tsx)&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;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;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;measure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Parameters&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;original&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;original&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&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;e&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="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;e&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="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;negative time stamp&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;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PerformanceMeasure&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;measure&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the bluntest option — it also hides any &lt;em&gt;legitimate&lt;/em&gt; negative-timestamp bug in your own instrumentation — so delete it once the upstream fix lands.&lt;/p&gt;

&lt;p&gt;Whichever you choose, remember the scope: this error never reaches your users. The throwing code is in a &lt;code&gt;*.development.js&lt;/code&gt; bundle that production builds exclude, which is why nobody has ever reported it from a deployed site. If you are seeing runtime errors in production, you are looking at a different failure — start with our breakdown of &lt;a href="https://www.iloveblogs.blog/post/nextjs-chunkloaderror-loading-chunk-failed-fix" rel="noopener noreferrer"&gt;ChunkLoadError: Loading chunk failed&lt;/a&gt;, and for other dev-mode-only surprises that look scarier than they are, see &lt;a href="https://www.iloveblogs.blog/post/why-useeffect-runs-twice-in-nextjs-dev" rel="noopener noreferrer"&gt;why useEffect runs twice in dev&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.iloveblogs.blog/post/nextjs-turbopack-performance-measure-negative-timestamp" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>turbopack</category>
      <category>react19</category>
      <category>devserver</category>
    </item>
  </channel>
</rss>
