<?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>Prisma: Can't reach database server at database:5432 on M1</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Sun, 13 Sep 2026 17:08:08 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/prisma-cant-reach-database-server-at-database5432-on-m1-4ohf</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/prisma-cant-reach-database-server-at-database5432-on-m1-4ohf</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you see &lt;code&gt;Can't reach database server at 'database':'5432'&lt;/code&gt; after moving your Docker Compose setup to an Apple Silicon Mac, the root cause is almost always a race condition: Prisma tries to connect before PostgreSQL finishes starting. Fix it by appending &lt;code&gt;?connect_timeout=300&lt;/code&gt; to your &lt;code&gt;DATABASE_URL&lt;/code&gt;. If that doesn't work, switch to Node 17 or 18 inside your container.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; PrismaClientInitializationError P1001: Can't reach database server at &lt;code&gt;database&lt;/code&gt;:&lt;code&gt;5432&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause:&lt;/strong&gt; PostgreSQL isn't ready when Prisma first connects; M1 Docker startup is slower.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Add &lt;code&gt;?connect_timeout=300&lt;/code&gt; to the connection string, or upgrade Node to v17+.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification:&lt;/strong&gt; Run &lt;code&gt;docker-compose exec postgres pg_isready&lt;/code&gt; and check the app logs.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The error, decoded
&lt;/h2&gt;

&lt;p&gt;A developer on Stack Overflow &lt;a href="https://stackoverflow.com/questions/68476229/m1-related-prisma-cant-reach-database-server-at-database5432" rel="noopener noreferrer"&gt;reported this exact symptom&lt;/a&gt; after switching to an M1 Mac. Their &lt;code&gt;docker-compose.yml&lt;/code&gt; defined a PostgreSQL service named &lt;code&gt;test-postgres&lt;/code&gt; and a Next.js app using Prisma. The &lt;code&gt;DATABASE_URL&lt;/code&gt; pointed to &lt;code&gt;postgres://postgres:postgres@localhost:15432/postgres&lt;/code&gt; — note the host &lt;code&gt;localhost&lt;/code&gt; and the mapped port &lt;code&gt;15432&lt;/code&gt;. When they ran &lt;code&gt;docker-compose run --publish 5555:5555 next npx prisma migrate dev&lt;/code&gt;, Prisma threw:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can't reach database server at `test-postgres`:`5432`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same error appeared when the app tried to connect at runtime. The PostgreSQL container was running (visible in Docker Desktop), but the Prisma client inside the &lt;code&gt;next&lt;/code&gt; container couldn't establish a TCP connection to the database hostname. The container logs also showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR:  relation "_prisma_migrations" does not exist at character 126
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That secondary error is a red herring — it means Prisma never got far enough to create the migrations table because the initial connection failed.&lt;/p&gt;

&lt;p&gt;The setup is typical: two services in a Compose file, the app referencing the database by its service name (&lt;code&gt;test-postgres&lt;/code&gt;), and a port mapping from &lt;code&gt;15432:5432&lt;/code&gt; on the host. On Intel Macs and Linux, this works. On M1, it breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why M1 Macs break Docker networking for Prisma and PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Docker Desktop on Apple Silicon runs inside a lightweight Linux VM (using QEMU or Apple's Virtualization framework). The default bridge network and DNS resolution work, but the startup sequence is slower than on bare-metal Linux. PostgreSQL, especially when pulled as a multi-architecture image, may take a few extra seconds to initialize its data directory and begin accepting connections. Prisma, on the other hand, attempts to connect immediately when &lt;code&gt;prisma migrate dev&lt;/code&gt; or the app starts.&lt;/p&gt;

&lt;p&gt;The error &lt;code&gt;Can't reach database server at 'database':'5432'&lt;/code&gt; is a Prisma-level timeout. Under the hood, Prisma uses the &lt;code&gt;pg&lt;/code&gt; driver (or its own engine) to open a TCP socket. If the remote host isn't listening yet, the connection is refused, and Prisma retries a few times before giving up. On M1, the window between "container started" and "PostgreSQL ready" is wider, so the default retry window isn't enough.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;localhost&lt;/code&gt; hostname in the original &lt;code&gt;DATABASE_URL&lt;/code&gt; is also a problem. Inside a container, &lt;code&gt;localhost&lt;/code&gt; refers to the container itself, not the Docker host. The developer had mapped port &lt;code&gt;15432&lt;/code&gt; on the host to &lt;code&gt;5432&lt;/code&gt; in the PostgreSQL container, but the app container can't reach the host's &lt;code&gt;localhost&lt;/code&gt; unless you use &lt;code&gt;host.docker.internal&lt;/code&gt; (which requires Docker Desktop 4.x+ and may need extra configuration on M1). The correct approach is to use the Compose service name (&lt;code&gt;test-postgres&lt;/code&gt;) and the container port (&lt;code&gt;5432&lt;/code&gt;), because both containers are on the same Compose network.&lt;/p&gt;

&lt;p&gt;The accepted answer on Stack Overflow (81 upvotes) identified the fix: add &lt;code&gt;?connect_timeout=300&lt;/code&gt; to the connection string. This tells Prisma to wait up to 300 seconds for the database to become available, giving PostgreSQL enough time to finish starting. Other answers pointed to Node.js version differences: Node 16 on ARM sometimes exhibits slower DNS resolution or different socket behavior, and upgrading to Node 17 or 18 resolved the issue for some users.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: Add a connection timeout to DATABASE_URL
&lt;/h2&gt;

&lt;p&gt;The minimal change is to append &lt;code&gt;?connect_timeout=300&lt;/code&gt; to your &lt;code&gt;DATABASE_URL&lt;/code&gt;. If you're using the service name, the URL should look 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;# Before (fails on M1)
DATABASE_URL="postgres://postgres:postgres@test-postgres:5432/postgres"

# After (works)
DATABASE_URL="postgres://postgres:postgres@test-postgres:5432/postgres?connect_timeout=300"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you were mistakenly using &lt;code&gt;localhost&lt;/code&gt; and a mapped port, switch to the service name and the internal port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Wrong: localhost inside a container points to the container itself
DATABASE_URL="postgres://postgres:postgres@localhost:15432/postgres"

# Correct: use the Compose service name and the container port
DATABASE_URL="postgres://postgres:postgres@test-postgres:5432/postgres?connect_timeout=300"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update your &lt;code&gt;docker-compose.yml&lt;/code&gt; to ensure the app depends on the database and uses the correct environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;test-postgres'&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;postgres:13'&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;15432:5432'&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pgdata:/var/lib/postgresql/data/'&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3000:3000'&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://postgres:postgres@test-postgres:5432/postgres?connect_timeout=300"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;healthcheck&lt;/code&gt; and &lt;code&gt;depends_on&lt;/code&gt; with &lt;code&gt;condition: service_healthy&lt;/code&gt; ensure the &lt;code&gt;next&lt;/code&gt; container doesn't start until PostgreSQL is actually accepting connections. This eliminates the race condition entirely, even without the timeout parameter. However, keeping &lt;code&gt;connect_timeout=300&lt;/code&gt; adds an extra safety net.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alternative fix: Upgrade Node.js
&lt;/h3&gt;

&lt;p&gt;If the timeout alone doesn't resolve the issue, change the Node.js version in your Dockerfile. Several developers on the Stack Overflow thread reported that moving from Node 16 to 17 or 18 fixed the connection error. Update your Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:16&lt;/span&gt;

&lt;span class="c"&gt;# After&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:18&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rebuild the image with &lt;code&gt;docker-compose build --no-cache&lt;/code&gt; and restart the stack. The newer Node version handles DNS resolution and socket creation differently on ARM, which can sidestep the underlying networking quirk.&lt;/p&gt;

&lt;p&gt;If you're using Next.js, make sure your Prisma client is only instantiated in server components. The &lt;code&gt;DATABASE_URL&lt;/code&gt; should never be exposed to the browser. See the &lt;a href="https://nextjs.org/docs/app/guides/server-and-client-boundary" rel="noopener noreferrer"&gt;Server and Client Boundary&lt;/a&gt; guide for how to keep data-fetching logic on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the fix
&lt;/h2&gt;

&lt;p&gt;After applying the changes, bring the stack up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose down &lt;span class="nt"&gt;-v&lt;/span&gt;   &lt;span class="c"&gt;# clean slate&lt;/span&gt;
docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that PostgreSQL is healthy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose &lt;span class="nb"&gt;exec &lt;/span&gt;postgres pg_isready &lt;span class="nt"&gt;-U&lt;/span&gt; postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/run/postgresql:5432 - accepting connections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now run the Prisma migration from the app container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose &lt;span class="nb"&gt;exec &lt;/span&gt;next npx prisma migrate dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the migration apply without the &lt;code&gt;Can't reach database server&lt;/code&gt; error. The app logs should show a successful database connection.&lt;/p&gt;

&lt;p&gt;If you need to test raw connectivity from the app container, use &lt;code&gt;nc&lt;/code&gt; (netcat):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose &lt;span class="nb"&gt;exec &lt;/span&gt;next sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"nc -zv test-postgres 5432"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test-postgres (172.18.0.2:5432) open
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the connection is established, you can verify by running a simple query using &lt;code&gt;psql&lt;/code&gt; (if you need to exit &lt;code&gt;psql&lt;/code&gt;, see &lt;a href="https://www.iloveblogs.blog/fix/exit-psql-quit-command" rel="noopener noreferrer"&gt;How to Exit psql: \q, exit, quit and Ctrl+D&lt;/a&gt;).&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Pattern 1: Using &lt;code&gt;localhost&lt;/code&gt; instead of the service name
&lt;/h3&gt;

&lt;p&gt;Inside a Docker container, &lt;code&gt;localhost&lt;/code&gt; is the container's own loopback interface. It does not resolve to the host machine or to another container. If your &lt;code&gt;DATABASE_URL&lt;/code&gt; uses &lt;code&gt;localhost&lt;/code&gt;, Prisma will try to connect to a PostgreSQL instance inside the same container, which doesn't exist. Always use the Compose service name (e.g., &lt;code&gt;test-postgres&lt;/code&gt;) and the container port (&lt;code&gt;5432&lt;/code&gt;). The host port mapping (&lt;code&gt;15432:5432&lt;/code&gt;) is only for external access from your Mac.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: Not waiting for PostgreSQL to be ready
&lt;/h3&gt;

&lt;p&gt;Even with &lt;code&gt;connect_timeout=300&lt;/code&gt;, if your app starts before the database container is created, you'll still see the error. The &lt;code&gt;depends_on&lt;/code&gt; with &lt;code&gt;condition: service_healthy&lt;/code&gt; is the most reliable way to sequence startup. Without it, Docker only waits for the container to start, not for the service inside to be ready. Combine the healthcheck with the timeout for a bulletproof setup.&lt;/p&gt;

&lt;p&gt;If you're using Supabase as your database provider, you might also run into connection timeouts; check out &lt;a href="https://www.iloveblogs.blog/post/supabase-slow-queries-fix" rel="noopener noreferrer"&gt;Why Your Supabase Queries Are Slow (And How to Fix)&lt;/a&gt; for performance tuning. When you're ready to run migrations, ensure your schema doesn't have foreign key issues that could cause constraint violations; see &lt;a href="https://www.iloveblogs.blog/post/supabase-foreign-key-constraint-violation-fix" rel="noopener noreferrer"&gt;Fix Foreign Key Constraint Violation in Supabase (23503)&lt;/a&gt; for a common pitfall.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Why does Prisma fail with 'Can't reach database server at database:5432' on M1 Macs?
&lt;/h3&gt;

&lt;p&gt;On Apple Silicon, Docker Desktop runs inside a VM and PostgreSQL initialization can be slower. Prisma attempts to connect before the database is ready, causing a P1001 error. Adding &lt;code&gt;?connect_timeout=300&lt;/code&gt; to the &lt;code&gt;DATABASE_URL&lt;/code&gt; gives Prisma more time to retry and usually resolves the issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does changing the Node.js version fix the Prisma connection error on M1?
&lt;/h3&gt;

&lt;p&gt;Yes, some developers have resolved the error by upgrading from Node 16 to Node 17 or 18. The newer versions handle DNS resolution and networking differently inside Docker on ARM, which can eliminate the connection timeout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use &lt;code&gt;host.docker.internal&lt;/code&gt; instead of the service name?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;host.docker.internal&lt;/code&gt; resolves to the host machine from inside a container. It works on Docker Desktop for Mac, but on M1 it may require Docker Desktop 4.x+ and the &lt;code&gt;--add-host&lt;/code&gt; flag in some configurations. Using the Compose service name is simpler and more portable across environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I test if my database is reachable from inside the container?
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;docker-compose exec &amp;lt;service&amp;gt; pg_isready -U postgres&lt;/code&gt; for PostgreSQL, or &lt;code&gt;nc -zv &amp;lt;host&amp;gt; &amp;lt;port&amp;gt;&lt;/code&gt; for a generic TCP check. Both commands confirm whether the database is listening before Prisma tries to connect.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/fix/exit-psql-quit-command" rel="noopener noreferrer"&gt;How to Exit psql: \q, exit, quit and Ctrl+D&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/fix/postgres-switch-database-psql-connect-command" rel="noopener noreferrer"&gt;Switch Databases in psql: The \connect Command (2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/supabase-slow-queries-fix" rel="noopener noreferrer"&gt;Why Your Supabase Queries Are Slow (And How to Fix)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/supabase-foreign-key-constraint-violation-fix" rel="noopener noreferrer"&gt;Fix Foreign Key Constraint Violation in Supabase (23503)&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/m1-related-prisma-cant-reach-database-server-at-database5432" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>prisma</category>
      <category>docker</category>
      <category>postgres</category>
      <category>m1</category>
    </item>
    <item>
      <title>Why useEffect runs twice in Next.js dev</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Sat, 12 Sep 2026 22:45:00 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/why-useeffect-runs-twice-in-nextjs-dev-3ggp</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/why-useeffect-runs-twice-in-nextjs-dev-3ggp</guid>
      <description>&lt;p&gt;You add a &lt;code&gt;console.log&lt;/code&gt;, start &lt;code&gt;next dev&lt;/code&gt;, and the effect fires twice. Sometimes that means two fetches, two analytics pings, or two subscriptions.&lt;/p&gt;

&lt;p&gt;The root cause is usually not Next.js. React documents this behavior directly: when Strict Mode is on, React runs one extra development-only setup and cleanup cycle before the real setup.&lt;/p&gt;

&lt;p&gt;That means the question is not "how do I stop React from doing that?" The question is "why does my effect break when React stress-tests it?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The broken effect pattern
&lt;/h2&gt;

&lt;p&gt;This is the classic example:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&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;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUsers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;

  &lt;span class="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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&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;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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;setUsers&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&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="mi"&gt;2&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;pre&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;In development, you may see two requests. The effect has no cleanup and no cancellation, so the extra Strict Mode cycle reveals that weakness immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safer effect
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&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;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUsers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;

  &lt;span class="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="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;()&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;loadUsers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="nf"&gt;setUsers&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="nf"&gt;loadUsers&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;error&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;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;AbortError&lt;/span&gt;&lt;span class="dl"&gt;'&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;error&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;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&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;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&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="mi"&gt;2&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;pre&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;Now the cleanup mirrors the setup. That is exactly what the React docs want Strict Mode to test.&lt;/p&gt;

&lt;h2&gt;
  
  
  What React is actually telling you
&lt;/h2&gt;

&lt;p&gt;If a second setup+cleanup cycle causes visible breakage, your effect is probably doing one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;opening a subscription and never unsubscribing&lt;/li&gt;
&lt;li&gt;firing a network request with no cancellation&lt;/li&gt;
&lt;li&gt;mutating global state during setup&lt;/li&gt;
&lt;li&gt;using an effect for something that should happen in response to a user event instead&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The better fix in many Next.js apps: move the fetch to the server
&lt;/h2&gt;

&lt;p&gt;In App Router projects, a lot of client &lt;code&gt;useEffect&lt;/code&gt; fetching should not be client fetching at all.&lt;/p&gt;

&lt;p&gt;If the data is needed to render the page, fetch it in the Server Component page or loader path first. That removes the whole class of duplicate dev-fetch confusion.&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/users/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;Page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/api/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&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="mi"&gt;2&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;pre&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;That is not always the right move, but it is often the right default in modern Next.js.&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to do first
&lt;/h2&gt;

&lt;p&gt;Do not start by disabling Strict Mode just because the effect is noisy. If the extra cycle exposes a bug, production can still hit the same bug through remounts, navigation, interrupted renders, or subscription leaks.&lt;/p&gt;

&lt;p&gt;Use Strict Mode as the warning sign it is meant to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a &lt;code&gt;useRef&lt;/code&gt; guard is acceptable
&lt;/h2&gt;

&lt;p&gt;For genuinely one-off client actions that cannot be moved elsewhere, a guard can be acceptable:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&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;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ClientPing&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;sent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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="nx"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;sent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/ping&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&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="p"&gt;[])&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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 a tactical fix, not the first tool to reach for. Prefer clean setup/cleanup or server-side data flow when possible.&lt;/p&gt;

&lt;p&gt;For related React and App Router debugging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/react-server-components-deep-dive" rel="noopener noreferrer"&gt;React Server Components Deep Dive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/window-is-not-defined-in-nextjs-react-app" rel="noopener noreferrer"&gt;Window is not defined in Next.js – 2026 Fix for React Apps&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;Next.js Hydration Mismatch Error: Exact Fixes for App Router and React&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 for Indie Developers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/72238175/why-useeffect-running-twice-and-how-to-handle-it-well-in-react" rel="noopener noreferrer"&gt;Stack Overflow: Why useEffect running twice and how to handle it well in React?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://react.dev/reference/react/useEffect" rel="noopener noreferrer"&gt;React docs: useEffect troubleshooting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://react.dev/reference/react/StrictMode" rel="noopener noreferrer"&gt;React docs: Strict Mode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/fix/why-useeffect-running-twice-and-how-to-handle-it-well-in-react" rel="noopener noreferrer"&gt;Fix useEffect Running Twice in React 18 — Strict Mode&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/why-useeffect-runs-twice-in-nextjs-dev" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>useeffect</category>
      <category>strictmode</category>
    </item>
    <item>
      <title>Extract date (yyyy/mm/dd) from a timestamp in PostgreSQL</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Sat, 12 Sep 2026 22:44:54 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/extract-date-yyyymmdd-from-a-timestamp-in-postgresql-3lpk</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/extract-date-yyyymmdd-from-a-timestamp-in-postgresql-3lpk</guid>
      <description>&lt;p&gt;The original question asks how to extract the &lt;code&gt;yyyy/mm/dd&lt;/code&gt; date part from a PostgreSQL timestamp. The accepted answer shows a &lt;code&gt;to_char&lt;/code&gt;/&lt;code&gt;to_date&lt;/code&gt; round-trip:&lt;/p&gt;

&lt;p&gt;This write-up is grounded in &lt;a href="https://stackoverflow.com/questions/6133107/extract-date-yyyy-mm-dd-from-a-timestamp-in-postgresql" rel="noopener noreferrer"&gt;the original Stack Overflow question&lt;/a&gt; (434 upvotes, 968,809 views).&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;to_char&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="s1"&gt;'YYYY/MM/DD'&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;to_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_char&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="s1"&gt;'YYYY/MM/DD'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'YYYY/MM/DD'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works, but for a native &lt;code&gt;DATE&lt;/code&gt; value you do not need the text round-trip. Use the cast operator &lt;code&gt;::&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single colon is not valid PostgreSQL. Write &lt;code&gt;timestamp::date&lt;/code&gt;, not &lt;code&gt;timestamp:date&lt;/code&gt;.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Accepted answer:&lt;/strong&gt; &lt;code&gt;to_char(now(), 'YYYY/MM/DD')&lt;/code&gt; formats; &lt;code&gt;to_date(to_char(...), 'YYYY/MM/DD')&lt;/code&gt; parses the text back to &lt;code&gt;date&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct fix:&lt;/strong&gt; &lt;code&gt;timestamp::date&lt;/code&gt; returns a native &lt;code&gt;DATE&lt;/code&gt; without text conversion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Formatting:&lt;/strong&gt; Use &lt;code&gt;to_char(timestamp, 'YYYY/MM/DD')&lt;/code&gt; only when you need text output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification:&lt;/strong&gt; Check the actual type with &lt;code&gt;pg_typeof()&lt;/code&gt; and the column type with &lt;code&gt;\d&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The cast returns the full date, not just the year
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;timestamp::date&lt;/code&gt; and &lt;code&gt;date(timestamp)&lt;/code&gt; both return PostgreSQL's native &lt;code&gt;DATE&lt;/code&gt; type. That type stores year, month, and day. The default text output is &lt;code&gt;YYYY-MM-DD&lt;/code&gt; under the standard &lt;code&gt;DateStyle&lt;/code&gt; setting. If another client displays only &lt;code&gt;2011&lt;/code&gt;, the client is not showing the full value. Verify in &lt;code&gt;psql&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&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="mi"&gt;1&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 plaintext"&gt;&lt;code&gt;         ts          | event_date
---------------------+------------
 2011-05-26 09:00:00 | 2011-05-26
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;SELECT pg_typeof(ts::date);&lt;/code&gt; to confirm the result is &lt;code&gt;date&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The accepted answer and the direct cast
&lt;/h2&gt;

&lt;p&gt;The accepted answer on the original thread uses a text format/parse sequence:&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;to_char&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="s1"&gt;'YYYY/MM/DD'&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;to_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_char&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="s1"&gt;'YYYY/MM/DD'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'YYYY/MM/DD'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first statement returns text in the requested &lt;code&gt;yyyy/mm/dd&lt;/code&gt; format. The second parses that text back into a &lt;code&gt;date&lt;/code&gt; value. This works, but it is an unnecessary round-trip when you only need a native &lt;code&gt;DATE&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use the direct cast for a native DATE
&lt;/h3&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;ts&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also use the SQL-standard function syntax:&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="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both return a &lt;code&gt;DATE&lt;/code&gt; type, ready for insertion into a &lt;code&gt;DATE&lt;/code&gt; column. They truncate the time component without rounding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use &lt;code&gt;to_char&lt;/code&gt; only for display
&lt;/h3&gt;

&lt;p&gt;If you need the exact &lt;code&gt;yyyy/mm/dd&lt;/code&gt; string, use &lt;code&gt;to_char&lt;/code&gt; directly on the timestamp:&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;to_char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'YYYY/MM/DD'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;formatted_date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the column as &lt;code&gt;DATE&lt;/code&gt; for storage and sorting; format only when you display the result.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;date_trunc&lt;/code&gt; is not a date extraction
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;date_trunc('day', ts)&lt;/code&gt; returns a timestamp truncated to midnight, not a &lt;code&gt;DATE&lt;/code&gt;. It preserves the timestamp type and, for &lt;code&gt;timestamptz&lt;/code&gt;, the time zone. Use it only when the target column expects a timestamp and you need to keep the time component at zero. For a plain &lt;code&gt;DATE&lt;/code&gt;, use &lt;code&gt;::date&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the full syntax and behavior, see the &lt;a href="http://www.postgresql.org/docs/9.0/interactive/functions-datetime.html" rel="noopener noreferrer"&gt;PostgreSQL date/time functions documentation&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Time-zone note:&lt;/strong&gt; &lt;code&gt;::date&lt;/code&gt; on a &lt;code&gt;timestamptz&lt;/code&gt; returns the date in the session’s time zone. If the server is UTC but the data represents New York events, the date can shift by one day. Set the time zone first: &lt;code&gt;SET TIME ZONE 'America/New_York';&lt;/code&gt; then run the cast.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Verify the extracted date
&lt;/h2&gt;

&lt;p&gt;After applying the fix, confirmation is a three-step check: type, value, and insertion.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Confirm the column type
&lt;/h3&gt;

&lt;p&gt;Use the &lt;code&gt;\d&lt;/code&gt; command in &lt;code&gt;psql&lt;/code&gt; to inspect the table. If you’re coming from a MySQL background, you may be used to &lt;code&gt;DESCRIBE&lt;/code&gt;. PostgreSQL uses &lt;code&gt;\d&lt;/code&gt; instead; we have a &lt;a href="https://www.iloveblogs.blog/post/postgres-describe-table-equivalent" rel="noopener noreferrer"&gt;full guide on the \d equivalent&lt;/a&gt; if you need it.&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; yourdb &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="s2"&gt; events"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Column     | Type              | Modifiers
------------+-------------------+-----------
 id         | integer           | not null
 ts         | timestamp         |
 event_date | date              |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for the &lt;code&gt;date&lt;/code&gt; type on the new column.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Check the actual value
&lt;/h3&gt;

&lt;p&gt;Query the raw column and force the output with explicit formatting for a sanity check:&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;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;to_char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'YYYY/MM/DD'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;formatted_date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a line like &lt;code&gt;2011-05-26 | 2011/05/26&lt;/code&gt;, proving both the type and the correct day.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Insert the value into a &lt;code&gt;DATE&lt;/code&gt; column
&lt;/h3&gt;

&lt;p&gt;Create a tiny scratch table to simulate the target environment:&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;TEMP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;test_insert&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testd&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;test_insert&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testd&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;ts&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&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="mi"&gt;1&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;test_insert&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 plaintext"&gt;&lt;code&gt;   testd
------------
 2011-05-26
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the insert succeeds, &lt;code&gt;::date&lt;/code&gt; produced a proper &lt;code&gt;DATE&lt;/code&gt; value. When you’re done, you can clean up safely — the approach for dropping test tables without losing production data is covered in &lt;a href="https://www.iloveblogs.blog/post/postgres-drop-all-tables-reset-database-safely" rel="noopener noreferrer"&gt;How to Drop All Tables in PostgreSQL Safely&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three common pitfalls during verification
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client shows only 2011&lt;/strong&gt; – Verify in &lt;code&gt;psql&lt;/code&gt; or run &lt;code&gt;SELECT event_date::text;&lt;/code&gt; to see the full string. The cast itself does not truncate the year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date shift after cast&lt;/strong&gt; – Verify the session time zone (&lt;code&gt;SHOW timezone;&lt;/code&gt;). If it differs from the data’s origin, set it explicitly as shown above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Cannot insert NULL” errors&lt;/strong&gt; – If source timestamps are nullable, filter them: &lt;code&gt;WHERE ts IS NOT NULL&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h3&gt;
  
  
  Why can’t I just do &lt;code&gt;to_date(to_char(ts, 'YYYY/MM/DD'), 'YYYY/MM/DD')&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;That text round-trip converts the timestamp to text, parses the text back into a date, and can never produce a different result than &lt;code&gt;ts::date&lt;/code&gt; — but it costs extra CPU, breaks any plan-time optimisation, and prevents the use of indexes on the expression. Use the direct cast instead.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;EXPLAIN SELECT * FROM events WHERE to_date(to_char(event_ts, 'YYYY/MM/DD'), 'YYYY/MM/DD') = '2025-01-15';&lt;/code&gt; shows a sequential scan, while &lt;code&gt;SELECT * FROM events WHERE event_ts::date = '2025-01-15';&lt;/code&gt; can use an index on the expression &lt;code&gt;(event_ts::date)&lt;/code&gt;. The extra text parsing and function calls also add measurable CPU overhead on large tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does &lt;code&gt;::date&lt;/code&gt; work in all PostgreSQL versions?
&lt;/h3&gt;

&lt;p&gt;Yes, the cast to &lt;code&gt;date&lt;/code&gt; from &lt;code&gt;timestamp&lt;/code&gt; has been available since at least PostgreSQL 9.0. All currently supported versions (14, 15, 16, 17, 18) include it. If you are unsure which version you’re running, check &lt;a href="https://www.iloveblogs.blog/fix/which-version-of-postgresql-am-i-running" rel="noopener noreferrer"&gt;this short guide on finding your PostgreSQL version&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;SELECT version();&lt;/code&gt; in psql — you’ll see output like &lt;code&gt;PostgreSQL 16.3 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 14.2.1, 64-bit&lt;/code&gt;. The &lt;code&gt;::date&lt;/code&gt; cast works identically in every major release back to 9.0, so there’s no compatibility risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I output the date exactly as &lt;code&gt;yyyy/mm/dd&lt;/code&gt; without losing the DATE type?
&lt;/h3&gt;

&lt;p&gt;The output format is a presentation layer concern. Keep the column as &lt;code&gt;DATE&lt;/code&gt; and use &lt;code&gt;to_char(event_date, 'YYYY/MM/DD')&lt;/code&gt; only when you need to display it to a user or generate a report. That way you retain the native type for ordering, indexing, and date arithmetic.&lt;/p&gt;

&lt;p&gt;For instance, define a table with a proper &lt;code&gt;DATE&lt;/code&gt; column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events&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;'2025-02-01'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now query using formatting only for display:&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;to_char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'YYYY/MM/DD'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;formatted_date&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns &lt;code&gt;2025/02/01&lt;/code&gt;, but the underlying column remains a &lt;code&gt;DATE&lt;/code&gt; — so range filters like &lt;code&gt;WHERE event_date BETWEEN '2025-02-01' AND '2025-02-28'&lt;/code&gt; use btree indexes efficiently. This avoids the cost of casting text columns on every scan.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/postgres-describe-table-equivalent" rel="noopener noreferrer"&gt;PostgreSQL DESCRIBE TABLE: The psql \d Equivalent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/fix/which-version-of-postgresql-am-i-running" rel="noopener noreferrer"&gt;Which Version of PostgreSQL Am I Running? 3 Ways to Check&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/postgres-drop-all-tables-reset-database-safely" rel="noopener noreferrer"&gt;How to Drop All Tables in PostgreSQL Safely (2026)&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/extract-date-yyyymmdd-from-a-timestamp-in-postgresql" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>datefunctions</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>Zero-Downtime Supabase Migrations: Expand/Contract (2026)</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Sat, 12 Sep 2026 16:43:54 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/zero-downtime-supabase-migrations-expandcontract-2026-25k7</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/zero-downtime-supabase-migrations-expandcontract-2026-25k7</guid>
      <description>&lt;p&gt;Database migrations feel harmless when your app is small. You add a column, change a type, rename a field, push to production, and everything works.&lt;/p&gt;

&lt;p&gt;Then you get real users.&lt;/p&gt;

&lt;p&gt;Now a migration can break live traffic in three different ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Old Vercel functions are still running while new code is deploying.&lt;/li&gt;
&lt;li&gt;Postgres takes a lock that blocks reads or writes on a hot table.&lt;/li&gt;
&lt;li&gt;Supabase RLS policies, generated TypeScript types, or API assumptions change before the app is ready.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zero downtime means users keep reading and writing while the schema changes underneath them. It does not mean every migration is instant. It means every migration is backward compatible, observable, and reversible enough that a failed deploy does not become an outage.&lt;/p&gt;

&lt;p&gt;This guide gives you a production pattern for Supabase Postgres:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Expand the schema without breaking old code.&lt;/li&gt;
&lt;li&gt;Deploy code that understands both old and new shapes.&lt;/li&gt;
&lt;li&gt;Backfill existing rows in small batches.&lt;/li&gt;
&lt;li&gt;Validate constraints without blocking normal traffic.&lt;/li&gt;
&lt;li&gt;Switch reads and writes to the new shape.&lt;/li&gt;
&lt;li&gt;Contract the old schema in a later release.&lt;/li&gt;
&lt;li&gt;Gate all of it in CI.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you already read the general &lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-migration-strategies" rel="noopener noreferrer"&gt;Supabase migration strategies guide&lt;/a&gt;, this is the operational playbook. It is narrower, more opinionated, and built for production rollouts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mental Model
&lt;/h2&gt;

&lt;p&gt;In a zero-downtime release, database schema and application code move at different speeds.&lt;/p&gt;

&lt;p&gt;On Vercel, a production deploy is a rolling change. Some requests can still hit old serverless functions while other requests hit the new build. Supabase Postgres is shared by both. That means the database must support both versions for a while.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.iloveblogs.blog%2Farticle-assets%2Fzero-downtime-supabase-migrations%2Frolling-deploy-shared-db.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.iloveblogs.blog%2Farticle-assets%2Fzero-downtime-supabase-migrations%2Frolling-deploy-shared-db.svg" alt="Old and new Vercel functions sharing one Supabase Postgres database during a rolling deploy" width="1200" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use this sequence:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Application&lt;/th&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Expand&lt;/td&gt;
&lt;td&gt;Add new nullable columns, tables, policies, indexes&lt;/td&gt;
&lt;td&gt;Old code still works&lt;/td&gt;
&lt;td&gt;Add capacity for the new behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bridge&lt;/td&gt;
&lt;td&gt;Keep old and new fields in sync&lt;/td&gt;
&lt;td&gt;New code writes both, reads with fallback&lt;/td&gt;
&lt;td&gt;Make both versions safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backfill&lt;/td&gt;
&lt;td&gt;Fill old rows gradually&lt;/td&gt;
&lt;td&gt;Code tolerates mixed data&lt;/td&gt;
&lt;td&gt;Move historical data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validate&lt;/td&gt;
&lt;td&gt;Add or validate constraints&lt;/td&gt;
&lt;td&gt;Code handles validation failures&lt;/td&gt;
&lt;td&gt;Prove data is safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Switch&lt;/td&gt;
&lt;td&gt;Read from the new shape&lt;/td&gt;
&lt;td&gt;New code no longer depends on old shape&lt;/td&gt;
&lt;td&gt;Complete behavior change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contract&lt;/td&gt;
&lt;td&gt;Drop old columns, triggers, policies&lt;/td&gt;
&lt;td&gt;Only new code exists&lt;/td&gt;
&lt;td&gt;Clean up debt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.iloveblogs.blog%2Farticle-assets%2Fzero-downtime-supabase-migrations%2Fexpand-contract-timeline.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.iloveblogs.blog%2Farticle-assets%2Fzero-downtime-supabase-migrations%2Fexpand-contract-timeline.svg" alt="Expand contract migration timeline for Supabase Postgres" width="1200" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The important rule: do not expand and contract in the same release.&lt;/p&gt;

&lt;p&gt;A destructive migration is not a migration. It is a coordinated product rollout. Treat it that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supabase Migration Workflow
&lt;/h2&gt;

&lt;p&gt;Supabase migrations are SQL files in &lt;code&gt;supabase/migrations&lt;/code&gt;. Create them with the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx supabase migration new add_account_billing_state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply and test locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx supabase start
npx supabase db reset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For remote projects, Supabase tracks applied migrations in &lt;code&gt;supabase_migrations.schema_migrations&lt;/code&gt;. Use a dry run before production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx supabase db push &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And inspect history when something feels off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx supabase migration list &lt;span class="nt"&gt;--linked&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules keep teams out of trouble:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not change production schema through the Table Editor or SQL Editor once migrations own the schema.&lt;/li&gt;
&lt;li&gt;Do not edit a migration file after it has been applied to a shared environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If remote history and local files drift, use &lt;code&gt;supabase migration repair&lt;/code&gt; deliberately. Do not "fix" drift by deleting migrations until production happens to accept the next push.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set Lock and Statement Timeouts
&lt;/h2&gt;

&lt;p&gt;Every production migration should fail quickly when it cannot get a lock. A blocked migration can block app traffic, and a blocked app can turn into a support incident.&lt;/p&gt;

&lt;p&gt;Start risky migration files with timeouts:&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;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5min'&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;lock_timeout&lt;/code&gt; limits how long Postgres waits to acquire a lock. &lt;code&gt;statement_timeout&lt;/code&gt; limits total execution time for a statement. The exact values depend on the table, but the philosophy is consistent: a migration that cannot acquire a safe lock should fail, not wait behind user traffic forever.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.iloveblogs.blog%2Farticle-assets%2Fzero-downtime-supabase-migrations%2Flock-behavior.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.iloveblogs.blog%2Farticle-assets%2Fzero-downtime-supabase-migrations%2Flock-behavior.svg" alt="Unsafe versus production-safe Postgres migration lock behavior" width="1200" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For large backfills, do not run one huge &lt;code&gt;UPDATE&lt;/code&gt;. Use batches. More on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safe Change 1: Add a New Nullable Column
&lt;/h2&gt;

&lt;p&gt;Adding a nullable column is the safest expand migration.&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;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2min'&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;accounts&lt;/span&gt;
  &lt;span class="k"&gt;add&lt;/span&gt; &lt;span class="k"&gt;column&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;billing_status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Old code ignores the column. New code can start writing it. Existing rows are &lt;code&gt;null&lt;/code&gt;, so reads must tolerate missing values.&lt;/p&gt;

&lt;p&gt;If you need a default for new rows, set it separately:&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;accounts&lt;/span&gt;
  &lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="n"&gt;billing_status&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="s1"&gt;'trialing'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Postgres can add a column with a constant default efficiently on modern versions, but volatile defaults and generated columns can still rewrite the table. When in doubt on a hot table, add the column first, backfill, then set defaults and constraints later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safe Change 2: Rename a Column
&lt;/h2&gt;

&lt;p&gt;The dangerous version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="k"&gt;rename&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That can break old code instantly. A live app may still read &lt;code&gt;name&lt;/code&gt;, while new code reads &lt;code&gt;display_name&lt;/code&gt;. The safer expand/contract pattern takes multiple releases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Release 1: Expand
&lt;/h3&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;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2min'&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="k"&gt;add&lt;/span&gt; &lt;span class="k"&gt;column&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;display_name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Release 2: Bridge in App Code
&lt;/h3&gt;

&lt;p&gt;New writes should fill both columns:&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="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;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayName&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;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;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reads should prefer the new column but tolerate old rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profileName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display_name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;profile&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;Someone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Release 3: Backfill
&lt;/h3&gt;

&lt;p&gt;Create a batch function:&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;backfill_profile_display_names&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_size&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="mi"&gt;1000&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;integer&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;declare&lt;/span&gt;
  &lt;span class="n"&gt;updated_count&lt;/span&gt; &lt;span class="nb"&gt;integer&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;with&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="k"&gt;as&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;id&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;display_name&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
      &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="k"&gt;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="k"&gt;limit&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;skip&lt;/span&gt; &lt;span class="n"&gt;locked&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;update&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;p&lt;/span&gt;
  &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;display_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch&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;get&lt;/span&gt; &lt;span class="k"&gt;diagnostics&lt;/span&gt; &lt;span class="n"&gt;updated_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;row_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;updated_count&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it repeatedly from a script, admin route, or scheduled job until it returns &lt;code&gt;0&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;select&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;backfill_profile_display_names&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&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;FOR UPDATE SKIP LOCKED&lt;/code&gt; lets concurrent workers avoid fighting over the same rows. Keep batches small enough that each transaction is boring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Release 4: Validate
&lt;/h3&gt;

&lt;p&gt;Before you make the new column required, prove the data is ready:&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="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;as&lt;/span&gt; &lt;span class="n"&gt;missing_display_names&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;display_name&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add a check constraint without immediately validating existing rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt;
  &lt;span class="k"&gt;add&lt;/span&gt; &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;profiles_display_name_present&lt;/span&gt;
  &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;display_name&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validate in a separate migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt;
  &lt;span class="n"&gt;validate&lt;/span&gt; &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;profiles_display_name_present&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Postgres still scans the table during validation, but it uses a less aggressive lock than adding the constraint in the fully validated form up front.&lt;/p&gt;

&lt;h3&gt;
  
  
  Release 5: Contract
&lt;/h3&gt;

&lt;p&gt;Only after old code is gone:&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;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2min'&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="k"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;column&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;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This cleanup can wait weeks. Shipping cleanup late is cheaper than causing downtime early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safe Change 3: Add a Required Foreign Key
&lt;/h2&gt;

&lt;p&gt;Foreign keys on large tables can be expensive if you validate them immediately. Use &lt;code&gt;NOT VALID&lt;/code&gt; first.&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;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5min'&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;orders&lt;/span&gt;
  &lt;span class="k"&gt;add&lt;/span&gt; &lt;span class="k"&gt;column&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;customer_id&lt;/span&gt; &lt;span class="n"&gt;uuid&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;orders&lt;/span&gt;
  &lt;span class="k"&gt;add&lt;/span&gt; &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;orders_customer_id_fkey&lt;/span&gt;
  &lt;span class="k"&gt;foreign&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;references&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;customers&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;not&lt;/span&gt; &lt;span class="k"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now backfill &lt;code&gt;customer_id&lt;/code&gt;. Once old rows are clean:&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;orders&lt;/span&gt;
  &lt;span class="n"&gt;validate&lt;/span&gt; &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;orders_customer_id_fkey&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 later need &lt;code&gt;customer_id&lt;/code&gt; to be non-null, use the same staged pattern:&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;orders&lt;/span&gt;
  &lt;span class="k"&gt;add&lt;/span&gt; &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;orders_customer_id_present&lt;/span&gt;
  &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;valid&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;orders&lt;/span&gt;
  &lt;span class="n"&gt;validate&lt;/span&gt; &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;orders_customer_id_present&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then set &lt;code&gt;NOT NULL&lt;/code&gt; in a small final migration after you have measured that it completes quickly:&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;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2min'&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;orders&lt;/span&gt;
  &lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that lock cannot be acquired in production, it should fail. The validated check constraint still protects data while you retry during a lower-traffic window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safe Change 4: Change a Column Type
&lt;/h2&gt;

&lt;p&gt;Changing a column type in place can rewrite the table and rebuild indexes. For hot tables, use a new column.&lt;/p&gt;

&lt;p&gt;Example: moving from &lt;code&gt;text&lt;/code&gt; status values to a stricter enum.&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;type&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;invoice_status&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;enum&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'open'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'void'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'uncollectible'&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;invoices&lt;/span&gt;
  &lt;span class="k"&gt;add&lt;/span&gt; &lt;span class="k"&gt;column&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;status_v2&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;invoice_status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bridge writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statusV2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeInvoiceStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&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;invoices&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;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status_v2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;statusV2&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;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;invoiceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Backfill:&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;backfill_invoice_status_v2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_size&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="mi"&gt;1000&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;integer&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;declare&lt;/span&gt;
  &lt;span class="n"&gt;updated_count&lt;/span&gt; &lt;span class="nb"&gt;integer&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;with&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="k"&gt;as&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;id&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoices&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;status_v2&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&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="k"&gt;limit&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;skip&lt;/span&gt; &lt;span class="n"&gt;locked&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;update&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;invoices&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
  &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;status_v2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="k"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;::&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;invoice_status&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'open'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'open'&lt;/span&gt;&lt;span class="p"&gt;::&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;invoice_status&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;::&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;invoice_status&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'void'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'void'&lt;/span&gt;&lt;span class="p"&gt;::&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;invoice_status&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s1"&gt;'uncollectible'&lt;/span&gt;&lt;span class="p"&gt;::&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;invoice_status&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;i&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;batch&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;get&lt;/span&gt; &lt;span class="k"&gt;diagnostics&lt;/span&gt; &lt;span class="n"&gt;updated_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;row_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;updated_count&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switch reads to &lt;code&gt;status_v2&lt;/code&gt;, keep the old &lt;code&gt;status&lt;/code&gt; column until you are certain no old code writes it, then contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safe Change 5: Create Large Indexes
&lt;/h2&gt;

&lt;p&gt;Postgres regular index creation can block writes. &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt; avoids blocking inserts, updates, and deletes, but PostgreSQL forbids it inside a transaction block.&lt;/p&gt;

&lt;p&gt;That matters because migration runners differ in how they execute SQL files.&lt;/p&gt;

&lt;p&gt;For small tables or new tables, a normal index inside a migration is fine:&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;index&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;orders_tenant_created_at_idx&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;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a large hot table, run the concurrent index through a non-transactional path such as a carefully controlled &lt;code&gt;psql&lt;/code&gt; maintenance step:&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;"&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"set lock_timeout = '5s'"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"set statement_timeout = '30min'"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"create index concurrently if not exists orders_tenant_created_at_idx on public.orders (tenant_id, created_at desc)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep it as its own deployment step. Do not hide it inside an unrelated app deploy. If it fails halfway, inspect invalid indexes before retrying:&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;schemaname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;indexrelname&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;index_name&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_stat_user_indexes&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;pg_index&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;pg_index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pg_stat_user_indexes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;pg_index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indisvalid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then drop the invalid index concurrently and retry:&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;drop&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="n"&gt;concurrently&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;exists&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;orders_tenant_created_at_idx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  RLS Changes Are Schema Changes
&lt;/h2&gt;

&lt;p&gt;RLS policy edits can break production just as hard as column changes. Treat policies as part of the expand/contract rollout.&lt;/p&gt;

&lt;p&gt;Suppose you are moving from user-owned rows to organization-owned rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expand
&lt;/h3&gt;

&lt;p&gt;Add the new column and membership policy path:&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;projects&lt;/span&gt;
  &lt;span class="k"&gt;add&lt;/span&gt; &lt;span class="k"&gt;column&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;organization_id&lt;/span&gt; &lt;span class="n"&gt;uuid&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;"Members can read organization projects"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memberships&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;memberships&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;organization_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;organization_id&lt;/span&gt;
      &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;memberships&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not drop the old policy yet:&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;-- Keep this until all rows have organization_id and app reads use org scope.&lt;/span&gt;
&lt;span class="c1"&gt;-- create policy "Users can read own projects" ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bridge
&lt;/h3&gt;

&lt;p&gt;New code writes &lt;code&gt;organization_id&lt;/code&gt;, but old rows may still rely on &lt;code&gt;user_id&lt;/code&gt;. Reads should scope by organization when available and fall back only where explicitly intended.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contract
&lt;/h3&gt;

&lt;p&gt;After backfill and app rollout:&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;drop&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="nv"&gt;"Users can read own projects"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before contract, run role-impersonation tests. This is a perfect companion to the &lt;a href="https://www.iloveblogs.blog/guides/supabase-rls-policy-design-patterns" rel="noopener noreferrer"&gt;Supabase RLS policy design guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type Generation Timing
&lt;/h2&gt;

&lt;p&gt;Generated types can accidentally force a breaking change too early.&lt;/p&gt;

&lt;p&gt;The safe sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add expand migration.&lt;/li&gt;
&lt;li&gt;Generate types.&lt;/li&gt;
&lt;li&gt;Update app code to accept both old and new fields.&lt;/li&gt;
&lt;li&gt;Deploy.&lt;/li&gt;
&lt;li&gt;Backfill and validate.&lt;/li&gt;
&lt;li&gt;Switch code to new field.&lt;/li&gt;
&lt;li&gt;Contract old schema.&lt;/li&gt;
&lt;li&gt;Generate types again and remove old field references.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Generate Supabase types after migrations are applied to the environment you are targeting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx supabase gen types typescript &lt;span class="nt"&gt;--linked&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; src/types/database.types.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In CI, fail if generated types drift from committed types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx supabase gen types typescript &lt;span class="nt"&gt;--linked&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/database.types.ts
diff &lt;span class="nt"&gt;-u&lt;/span&gt; src/types/database.types.ts /tmp/database.types.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That catches the class of bug where a developer changes schema locally but forgets to commit regenerated types.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions CI
&lt;/h2&gt;

&lt;p&gt;This workflow has three jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lint and test the app.&lt;/li&gt;
&lt;li&gt;Validate local migrations by resetting a local Supabase stack.&lt;/li&gt;
&lt;li&gt;Dry-run and push migrations to production only on &lt;code&gt;main&lt;/code&gt;.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Supabase Production Migrations&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run lint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test -- --runInBand&lt;/span&gt;

  &lt;span class="na"&gt;migration-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase/setup-cli@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase start&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase db reset&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase migration list --local&lt;/span&gt;

  &lt;span class="na"&gt;deploy-migrations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main'&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app-tests&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;migration-tests&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production-supabase-migrations&lt;/span&gt;
      &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase/setup-cli@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Link Supabase project&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase link --project-ref "$SUPABASE_PROJECT_ID"&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SUPABASE_ACCESS_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SUPABASE_ACCESS_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;SUPABASE_DB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SUPABASE_DB_PASSWORD }}&lt;/span&gt;
          &lt;span class="na"&gt;SUPABASE_PROJECT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SUPABASE_PROJECT_ID }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dry-run migrations&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase db push --dry-run --password "$SUPABASE_DB_PASSWORD"&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SUPABASE_ACCESS_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SUPABASE_ACCESS_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;SUPABASE_DB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SUPABASE_DB_PASSWORD }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Push migrations&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase db push --password "$SUPABASE_DB_PASSWORD"&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SUPABASE_ACCESS_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SUPABASE_ACCESS_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;SUPABASE_DB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SUPABASE_DB_PASSWORD }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a GitHub environment approval for production. Schema changes deserve a human checkpoint, especially when they touch hot tables.&lt;/p&gt;

&lt;p&gt;For a full deployment pipeline, pair this with the &lt;a href="https://www.iloveblogs.blog/hubs/nextjs-supabase" rel="noopener noreferrer"&gt;Next.js and Supabase CI/CD guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Downloadable Assets
&lt;/h2&gt;

&lt;p&gt;Use the checklist version when you want this process in a pull request template, runbook, or team migration review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/downloads/zero-downtime-supabase-migration-checklist.md" rel="noopener noreferrer"&gt;Download the zero-downtime Supabase migration checklist&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/examples/zero-downtime-supabase-migrations/expand-contract-profiles.sql" rel="noopener noreferrer"&gt;Copy the complete expand/contract SQL example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/examples/zero-downtime-supabase-migrations/batch-backfill.ts.txt" rel="noopener noreferrer"&gt;Copy the resumable batch backfill script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/examples/zero-downtime-supabase-migrations/github-actions-supabase-migrations.yml" rel="noopener noreferrer"&gt;Copy the GitHub Actions migration workflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/examples/zero-downtime-supabase-migrations/migration-safety-check.js" rel="noopener noreferrer"&gt;Copy the migration safety check script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/examples/zero-downtime-supabase-migrations/README.md" rel="noopener noreferrer"&gt;Browse the full example kit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The article examples are intentionally copy/pasteable: the safe rename rollout, batch backfill function, &lt;code&gt;NOT VALID&lt;/code&gt; constraint flow, concurrent index maintenance command, and GitHub Actions workflow can each stand alone in a real repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a Migration Safety Linter
&lt;/h2&gt;

&lt;p&gt;You can catch dangerous SQL with a small script before it reaches 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="c1"&gt;// scripts/check-migrations.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&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;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&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;node:path&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;migrationsDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&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="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;supabase/migrations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;riskyPatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;drop&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+column&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DROP COLUMN must be a contract migration with a rollback note.&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;rename&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+column&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RENAME COLUMN is not safe for rolling deploys. Use expand/contract.&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;alter&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+column&lt;/span&gt;&lt;span class="se"&gt;\b[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;set&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+not&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+null&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SET NOT NULL needs backfill plus validation proof.&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;create&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+index&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?!&lt;/span&gt;&lt;span class="sr"&gt;concurrently&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Large-table indexes should use a reviewed index plan.&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="p"&gt;]&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="k"&gt;for &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;fileName&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;migrationsDir&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.sql&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fullPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;migrationsDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileName&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;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;for &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;rule&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;riskyPatterns&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;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;failed&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;failed&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="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/check-migrations.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script is intentionally conservative. It should start conversations, not replace review. Add an allowlist comment convention if your team needs exceptions:&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;-- migration-safety-reviewed: contract release 2026-05-23&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="k"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;column&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;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rollback Strategy
&lt;/h2&gt;

&lt;p&gt;Most production database rollbacks should be forward fixes, not time travel.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Rolling back is a decision, not a reflex — the &lt;a href="https://www.iloveblogs.blog/hubs/postgresql" rel="noopener noreferrer"&gt;PostgreSQL migration rollback playbook&lt;/a&gt; walks through the forward-fix-versus-revert call, lock-aware sequencing, and verification, so you pick the safer path instead of the faster-looking one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Use this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Roll back application code first.&lt;/li&gt;
&lt;li&gt;Keep expand migrations in place if they are backward compatible.&lt;/li&gt;
&lt;li&gt;Stop backfill workers if they are causing load.&lt;/li&gt;
&lt;li&gt;Add a forward migration to restore compatibility if needed.&lt;/li&gt;
&lt;li&gt;Use backups or point-in-time recovery only for disaster recovery.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example: a new app release starts reading &lt;code&gt;display_name&lt;/code&gt;, but a subset of rows were not backfilled. Roll back the app to read &lt;code&gt;display_name ?? name&lt;/code&gt;, stop the release, run the missing backfill, then redeploy.&lt;/p&gt;

&lt;p&gt;Do not drop the new column as your first move. Dropping data to roll back code is how a recoverable deploy turns into a data incident.&lt;/p&gt;

&lt;p&gt;Supabase backups are your emergency option. Daily backups and point-in-time recovery are operational safety nets, but restore operations can involve downtime and data loss windows. They are not a normal deploy rollback mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Safety Checklist
&lt;/h2&gt;

&lt;p&gt;Copy this into the pull request description for any production migration, or use the &lt;a href="https://www.iloveblogs.blog/downloads/zero-downtime-supabase-migration-checklist.md" rel="noopener noreferrer"&gt;downloadable checklist&lt;/a&gt; as a reusable PR template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Migration Safety Checklist&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; [ ] This migration has been tested with &lt;span class="sb"&gt;`supabase db reset`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] This migration has been dry-run against the target project.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] The app code is backward compatible with the old and new schema.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] This migration does not rename or drop columns needed by the current deploy.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Hot-table operations use &lt;span class="sb"&gt;`lock_timeout`&lt;/span&gt; and &lt;span class="sb"&gt;`statement_timeout`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Large backfills run in batches, not one giant &lt;span class="sb"&gt;`UPDATE`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] New foreign keys or checks use &lt;span class="sb"&gt;`NOT VALID`&lt;/span&gt; before validation.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Large indexes have a reviewed concurrent index plan.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] RLS policies support both old and new access paths during rollout.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Supabase generated types are updated in the same PR when needed.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Rollback plan is documented.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Monitoring owner is named for the deploy window.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For higher-risk migrations, add two more lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; [ ] We confirmed the latest usable backup or PITR restore point.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] We rehearsed the rollback in staging.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example Rollout: Add Team Billing
&lt;/h2&gt;

&lt;p&gt;Here is the complete expand/contract pattern in one realistic feature.&lt;/p&gt;

&lt;p&gt;You are moving billing from individual users to organizations. Existing tables have &lt;code&gt;user_id&lt;/code&gt;. New billing tables need &lt;code&gt;organization_id&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migration 1: Expand
&lt;/h3&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;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5min'&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;subscriptions&lt;/span&gt;
  &lt;span class="k"&gt;add&lt;/span&gt; &lt;span class="k"&gt;column&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;organization_id&lt;/span&gt; &lt;span class="n"&gt;uuid&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;subscriptions&lt;/span&gt;
  &lt;span class="k"&gt;add&lt;/span&gt; &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;subscriptions_organization_id_fkey&lt;/span&gt;
  &lt;span class="k"&gt;foreign&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;references&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;organizations&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;not&lt;/span&gt; &lt;span class="k"&gt;valid&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;index&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;subscriptions_organization_id_idx&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;subscriptions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploy 1: Bridge
&lt;/h3&gt;

&lt;p&gt;New code writes both:&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="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;subscriptions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;stripe_customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trialing&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;Reads use the new organization scope when present, but old rows still work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&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="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;subscriptions&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;or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`organization_id.eq.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,user_id.eq.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Backfill
&lt;/h3&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;backfill_subscription_organizations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_size&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="mi"&gt;500&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;integer&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;declare&lt;/span&gt;
  &lt;span class="n"&gt;updated_count&lt;/span&gt; &lt;span class="nb"&gt;integer&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;with&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="k"&gt;as&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;s&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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;organization_id&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscriptions&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
    &lt;span class="k"&gt;join&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;memberships&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;organization_id&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="k"&gt;limit&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;skip&lt;/span&gt; &lt;span class="n"&gt;locked&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;update&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;subscriptions&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
  &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;organization_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;organization_id&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;s&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;batch&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;get&lt;/span&gt; &lt;span class="k"&gt;diagnostics&lt;/span&gt; &lt;span class="n"&gt;updated_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;row_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;updated_count&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run:&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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backfill_subscription_organizations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Migration 2: Validate
&lt;/h3&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;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'10min'&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;subscriptions&lt;/span&gt;
  &lt;span class="n"&gt;validate&lt;/span&gt; &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;subscriptions_organization_id_fkey&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;subscriptions&lt;/span&gt;
  &lt;span class="k"&gt;add&lt;/span&gt; &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;subscriptions_organization_id_present&lt;/span&gt;
  &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organization_id&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;valid&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;subscriptions&lt;/span&gt;
  &lt;span class="n"&gt;validate&lt;/span&gt; &lt;span class="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;subscriptions_organization_id_present&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploy 2: Switch
&lt;/h3&gt;

&lt;p&gt;Now reads use only &lt;code&gt;organization_id&lt;/code&gt;. Keep &lt;code&gt;user_id&lt;/code&gt; writes for one more deploy if old code may still exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migration 3: Contract
&lt;/h3&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;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'5s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;statement_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2min'&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;subscriptions&lt;/span&gt;
  &lt;span class="k"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;constraint&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;subscriptions_organization_id_present&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;subscriptions&lt;/span&gt;
  &lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="n"&gt;organization_id&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;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;subscriptions&lt;/span&gt;
  &lt;span class="k"&gt;drop&lt;/span&gt; &lt;span class="k"&gt;column&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;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;This final migration is intentionally boring because all the risky work happened earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Not to Do
&lt;/h2&gt;

&lt;p&gt;Avoid these in live Supabase projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Renaming a column in the same deploy that changes app code.&lt;/li&gt;
&lt;li&gt;Dropping a column because TypeScript no longer references it.&lt;/li&gt;
&lt;li&gt;Running one giant backfill update on a hot table.&lt;/li&gt;
&lt;li&gt;Adding a foreign key to a large table without &lt;code&gt;NOT VALID&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Adding strict RLS policies before data and code are ready.&lt;/li&gt;
&lt;li&gt;Trusting local migration success as proof of production safety.&lt;/li&gt;
&lt;li&gt;Using a backup restore as a normal rollback plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most dangerous migrations are the ones that look clean in a diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference Docs
&lt;/h2&gt;

&lt;p&gt;Keep these open while designing production migrations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/deployment/database-migrations" rel="noopener noreferrer"&gt;Supabase database migrations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/reference/cli/v0/supabase-db-diff" rel="noopener noreferrer"&gt;Supabase CLI reference for db push, db diff, and migration repair&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/deployment/" rel="noopener noreferrer"&gt;Supabase deployment and branching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/platform/backups" rel="noopener noreferrer"&gt;Supabase database backups and PITR&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-altertable.html" rel="noopener noreferrer"&gt;PostgreSQL ALTER TABLE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-createindex.html" rel="noopener noreferrer"&gt;PostgreSQL CREATE INDEX&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/database/postgres/indexes" rel="noopener noreferrer"&gt;Supabase Postgres indexes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Durable Pattern
&lt;/h2&gt;

&lt;p&gt;Zero-downtime migrations are mostly discipline.&lt;/p&gt;

&lt;p&gt;Add before you use. Write both before you read new. Backfill before you require. Validate before you enforce. Drop only after the old code is gone.&lt;/p&gt;

&lt;p&gt;That pattern is slower than a one-line &lt;code&gt;ALTER TABLE&lt;/code&gt;. It is also the difference between a routine release and a production incident.&lt;/p&gt;

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

&lt;p&gt;These production incidents are directly traceable to the migration patterns covered in this guide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/mahdibrr/awesome-nextjs-supabase/blob/main/reference/incident-index/README.md#inc-009-migration-succeeds-in-staging-and-fails-in-prod" rel="noopener noreferrer"&gt;INC-009: Migration succeeds in staging and fails in prod&lt;/a&gt; — production data volume exposed a lock timeout that staging never triggered&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mahdibrr/awesome-nextjs-supabase/blob/main/reference/incident-index/README.md#inc-010-rollback-blocks-writes-for-too-long" rel="noopener noreferrer"&gt;INC-010: Rollback blocks writes for too long&lt;/a&gt; — revert migration acquired a table lock during peak traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Adjacent Guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/hubs/supabase-debugging" rel="noopener noreferrer"&gt;Supabase debugging and DB operations hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/supabase-connection-pooling-vercel" rel="noopener noreferrer"&gt;Supabase Connection Pooling with PgBouncer on Vercel Serverless&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;Database Design and Optimization for Next.js and Supabase Applications&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/zero-downtime-supabase-migrations" 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>migrations</category>
      <category>devops</category>
    </item>
    <item>
      <title>Window is not defined in Next.js – 2026 Fix for React Apps</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Sat, 12 Sep 2026 16:43:44 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/window-is-not-defined-in-nextjs-2026-fix-for-react-apps-4mog</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/window-is-not-defined-in-nextjs-2026-fix-for-react-apps-4mog</guid>
      <description>&lt;p&gt;You import a component that touches &lt;code&gt;window&lt;/code&gt; — a map library, a charting library, a custom script — run &lt;code&gt;npm run dev&lt;/code&gt; or deploy to Vercel, and the build dies 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="go"&gt;ReferenceError: window is not defined
&lt;/span&gt;&lt;span class="gp"&gt;    at Object.&amp;lt;anonymous&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;/src/components/Map.tsx:12:15&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;    at Module._compile (node:internal/modules/cjs/loader:1126:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
    at Module.load (node:internal/modules/cjs/loader:1004:32)
    at Function.Module._load (node:internal/modules/cjs/loader:839:12)
    at next/dist/build/webpack/loaders/next-client-pages-loader.js:274:23
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The behavior is the same across local development, preview deployments, and production builds: the code runs during server‑side rendering, where &lt;code&gt;window&lt;/code&gt; doesn't exist. The fix is to guard the reference or move the code into a client‑only context — this guide covers both patterns, plus the two awkward cases (utility modules and libraries that can't be lazy‑loaded).&lt;/p&gt;

&lt;h2&gt;
  
  
  The crash happens at import time, not render time
&lt;/h2&gt;

&lt;p&gt;Next.js executes every page component on the server for the initial request. During that phase the JavaScript environment is Node.js, which does &lt;strong&gt;not&lt;/strong&gt; provide a &lt;code&gt;window&lt;/code&gt; global. Any top‑level reference to &lt;code&gt;window&lt;/code&gt;—whether you call &lt;code&gt;window.innerWidth&lt;/code&gt;, &lt;code&gt;window.location&lt;/code&gt;, or a library that accesses &lt;code&gt;document&lt;/code&gt;—is evaluated as soon as the module is imported. Because the import happens before the component is rendered, the server throws a &lt;code&gt;ReferenceError&lt;/code&gt; and the build fails.&lt;/p&gt;

&lt;p&gt;The problem is amplified when you use third‑party libraries that assume a browser context. For instance, many map or chart libraries read &lt;code&gt;window.devicePixelRatio&lt;/code&gt; at import time. If you bundle them directly in a page component, Next.js will try to evaluate that code on the server, triggering the error.&lt;/p&gt;

&lt;p&gt;The relevant code path is:&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;// src/components/Map.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;L&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;leaflet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// leaflets reads window at import time&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;Map&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;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;L&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- window is accessed here&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;map&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&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;When the server imports &lt;code&gt;leaflet&lt;/code&gt;, the library immediately accesses &lt;code&gt;window&lt;/code&gt;, causing the crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to keep &lt;code&gt;window&lt;/code&gt; out of the server bundle
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Guard the reference&lt;/strong&gt; with &lt;code&gt;typeof window !== 'undefined'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move the code into a &lt;code&gt;useEffect&lt;/code&gt; hook&lt;/strong&gt; so it only runs after the component mounts in the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load the library dynamically&lt;/strong&gt; with &lt;code&gt;next/dynamic&lt;/code&gt; and disable SSR for that component.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Dynamic import with &lt;code&gt;ssr: false&lt;/code&gt; (the approach I'd pick)
&lt;/h3&gt;

&lt;p&gt;Below is the minimal change that resolves the error for most cases. I chose the dynamic import approach because it isolates the entire component from server rendering, keeping the bundle size small and avoiding any accidental &lt;code&gt;window&lt;/code&gt; access.&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;// src/components/Map.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dynamic&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/dynamic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Dynamically import the map component without SSR&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LeafletMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lib/LeafletMap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ssr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Loading&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&amp;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;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;Map&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;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LeafletMap&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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 the dynamically loaded module:&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;// src/lib/LeafletMap.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;L&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;leaflet&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;leaflet/dist/leaflet.css&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;LeafletMap&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;mapContainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&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="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;mapContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;const&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;L&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;mapContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setView&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;51.505&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.09&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tileLayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;attribution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;copy; OpenStreetMap contributors&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;addTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;map&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;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mapContainer&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&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;That single change addresses the cause because the &lt;code&gt;leaflet&lt;/code&gt; import now happens only in the browser, after the server has already sent the HTML. The &lt;code&gt;ssr: false&lt;/code&gt; flag tells Next.js to skip this component during the server pass, eliminating the &lt;code&gt;window&lt;/code&gt; reference entirely.&lt;/p&gt;

&lt;p&gt;To apply it to your own component:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a new file&lt;/strong&gt; &lt;code&gt;src/lib/LeafletMap.tsx&lt;/code&gt; (or whatever library you’re using) and paste the second snippet above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace the original import&lt;/strong&gt; in &lt;code&gt;src/components/Map.tsx&lt;/code&gt; with the dynamic import shown in the first snippet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove any top‑level &lt;code&gt;window&lt;/code&gt; usage&lt;/strong&gt; from the original component. If you still need a small check, wrap it:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&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="c1"&gt;// browser‑only code&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Save the files&lt;/strong&gt; and restart the dev server (&lt;code&gt;npm run dev&lt;/code&gt;). The error should disappear.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;typeof window&lt;/code&gt; guard inside &lt;code&gt;useEffect&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;If you prefer a guard instead of dynamic import, the pattern looks like 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="c1"&gt;// src/components/Chart.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="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;Chart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;registerables&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;chart.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;Chart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;registerables&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;ChartComponent&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&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="c1"&gt;// guard&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myChart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Chart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;{&lt;/span&gt; &lt;span class="cm"&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="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;myChart&lt;/span&gt;&lt;span class="dl"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both approaches are valid; pick the one that matches your project's style.&lt;/p&gt;

&lt;h2&gt;
  
  
  Restart the dev server and read the log
&lt;/h2&gt;

&lt;p&gt;Run the development server again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the usual Next.js startup log without any ReferenceError:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; next dev
  ▲ Next.js 14.x.x
  - Local:        http://localhost:3000
  - Network:      http://0.0.0.0:3000

 ✓ Starting...
 ✓ Ready in 1234ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:3000&lt;/code&gt; in a browser. The page that previously crashed now renders the map (or chart) correctly. No red error overlay appears in the console, and the network tab shows the dynamic chunk (&lt;code&gt;LeafletMap.js&lt;/code&gt;) loading only after the initial HTML.&lt;/p&gt;

&lt;p&gt;If the error persists, double‑check that &lt;strong&gt;all&lt;/strong&gt; imports of the offending library are now behind a guard or dynamic import. A stray &lt;code&gt;import L from 'leaflet'&lt;/code&gt; in another component will still trigger the same crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the reference hides in a utility module
&lt;/h2&gt;

&lt;p&gt;Sometimes the &lt;code&gt;window&lt;/code&gt; reference lives in a helper file that is imported by many components:&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;// src/utils/browserOnly.ts&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;isMobile&lt;/span&gt; &lt;span class="o"&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In that case, the guard must be inside the utility:&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;// src/utils/browserOnly.ts&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;isMobile&lt;/span&gt; &lt;span class="o"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&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="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;768&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;Or you can export a no‑op fallback for the server:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isMobile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;768&lt;/span&gt;
  &lt;span class="p"&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="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Libraries that read &lt;code&gt;window&lt;/code&gt; the moment they load
&lt;/h2&gt;

&lt;p&gt;Some libraries read &lt;code&gt;window&lt;/code&gt; at module evaluation time and cannot be deferred. A common example is an older analytics or scroll-tracking hook from packages like &lt;code&gt;react-use&lt;/code&gt; (&lt;code&gt;useWindowSize&lt;/code&gt;, &lt;code&gt;useScroll&lt;/code&gt;) that accesses &lt;code&gt;window&lt;/code&gt; as soon as the module is imported in a server context. The fix is to patch the environment with a small shim:&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;// src/shims/windowShim.js&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&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="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;any&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;Add this shim as the first import in &lt;code&gt;next.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;webpack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isServer&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;isServer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &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;entries&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;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;main.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/shims/windowShim.js&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="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;main.js&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="nx"&gt;entries&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;return&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach should be a last resort; the guard or dynamic import is cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guard rails so it doesn't come back
&lt;/h2&gt;

&lt;p&gt;Next.js treats every page as a hybrid of server‑side rendering (SSR) and client‑side hydration. Anything that runs at the top level of a module is evaluated on the server first. Because &lt;code&gt;window&lt;/code&gt;, &lt;code&gt;document&lt;/code&gt;, and &lt;code&gt;navigator&lt;/code&gt; are browser‑only globals, they must be accessed &lt;strong&gt;after&lt;/strong&gt; the component mounts. To keep the problem from resurfacing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add an ESLint rule&lt;/strong&gt; (&lt;code&gt;no-restricted-globals&lt;/code&gt;) that flags direct &lt;code&gt;window&lt;/code&gt; usage outside of a &lt;code&gt;typeof&lt;/code&gt; guard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a unit test&lt;/strong&gt; that renders the component with &lt;code&gt;next/jest&lt;/code&gt; in a Node environment and asserts that it does not throw.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a shared helper&lt;/strong&gt; (&lt;code&gt;src/lib/isBrowser.ts&lt;/code&gt;) that centralizes the guard:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// src/lib/isBrowser.ts&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;isBrowser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use &lt;code&gt;if (isBrowser) { … }&lt;/code&gt; throughout the codebase.&lt;/p&gt;

&lt;p&gt;By making the guard a first‑class citizen, you reduce the cognitive load and avoid accidental server crashes when adding new third‑party UI widgets.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/nextjs-hydration-mismatch-fix" rel="noopener noreferrer"&gt;Next.js Hydration Mismatch Error: Exact Fixes for App Router and React&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/nextjs-app-router-complete-guide" rel="noopener noreferrer"&gt;Next.js App Router Guide: From Basics to Advanced Patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/what-causes-nextjs-warning-extra-attributes-from-the-server-data-ne" rel="noopener noreferrer"&gt;NextJS Warning: Extra attributes from the server – Fix 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/react-server-components-deep-dive" rel="noopener noreferrer"&gt;React Server Components: Complete Deep Dive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/typeerror-cookies-crash-nextjs-route-handler" rel="noopener noreferrer"&gt;TypeError cookies() crash in Next.js route handler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/fix/nextjs-usesearchparams-suspense-static-rendering-docs" rel="noopener noreferrer"&gt;Next.js useSearchParams Suspense: Static Rendering Fix 2026&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/window-is-not-defined-in-nextjs-react-app" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>supabase</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>Fix useEffect Running Twice in React 18 — Strict Mode</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Fri, 11 Sep 2026 23:06:25 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/fix-useeffect-running-twice-in-react-18-strict-mode-d64</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/fix-useeffect-running-twice-in-react-18-strict-mode-d64</guid>
      <description>&lt;p&gt;You add a &lt;code&gt;useEffect&lt;/code&gt; with an empty dependency array, drop a &lt;code&gt;console.log('effect ran')&lt;/code&gt; inside, and on first mount you see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Effect ran
Effect ran
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or in the network tab, the same fetch fires twice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fetching data...
Fetching data...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instinct is to "fix" it so the effect runs once. That instinct is wrong — the double run is React telling you something is about to break. Here is what it is telling you, and the pattern that makes it stop mattering.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not a bug — it's Strict Mode
&lt;/h2&gt;

&lt;p&gt;React 18 Strict Mode, on by default in every dev environment (Create React App, Next.js App Router, Vite), deliberately remounts your component once on initial render to stress-test your cleanup logic. The double log is not a defect in your code or in React — it is a safety probe. It surfaces effects that are not idempotent, so you find them in dev instead of in production. In a production build (&lt;code&gt;npm run build&lt;/code&gt;) Strict Mode is disabled and the effect runs once.&lt;/p&gt;

&lt;p&gt;The probe catches the patterns that hurt most: a &lt;code&gt;fetch&lt;/code&gt; with no cancellation that races when a component unmounts mid-request, a Supabase Realtime &lt;code&gt;subscribe()&lt;/code&gt; with no &lt;code&gt;unsubscribe()&lt;/code&gt; leaking listeners, a timer with no &lt;code&gt;clearInterval&lt;/code&gt;. Naive effects that "work" in dev only because Strict Mode happens to be off would silently corrupt state under real network latency, slow devices, or hot-reload.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mount-unmount-remount cycle
&lt;/h2&gt;

&lt;p&gt;Under the hood, React does three steps on the very first render of a component in dev:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Mounts the component → runs &lt;code&gt;useEffect&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Immediately unmounts it → runs your cleanup (if you returned one).&lt;/li&gt;
&lt;li&gt;Remounts it → runs &lt;code&gt;useEffect&lt;/code&gt; again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the entire mechanism. A typical effect that fails the probe looks like 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="c1"&gt;// src/app/page.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="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;Page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="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;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;Effect ran&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ← logs twice in dev&lt;/span&gt;
    &lt;span class="c1"&gt;// ❌ Non-idempotent: no cleanup, no cancellation&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&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;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Loading...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;&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;&lt;code&gt;fetch&lt;/code&gt; is not cancellable by default, so when Strict Mode unmounts after step 1, the in-flight request keeps going; step 3 starts a second request; both resolve and overwrite state — a race. The same shape breaks Supabase Realtime: two &lt;code&gt;subscribe()&lt;/code&gt; calls without &lt;code&gt;unsubscribe()&lt;/code&gt; in cleanup create duplicate listeners and a memory leak. I cover that case in &lt;a href="https://www.iloveblogs.blog/hubs/nextjs-supabase" rel="noopener noreferrer"&gt;Optimistic UI Patterns with Next.js Server Actions and Supabase Realtime&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idempotent effect pattern
&lt;/h2&gt;

&lt;p&gt;The fix is to make the effect cancellable so the first invocation's work is discarded when Strict Mode unmounts it:&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;// src/app/page.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="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;Page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="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="c1"&gt;// ✅ Local flag — belongs to THIS effect invocation only&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;active&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&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;fetchData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&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;json&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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="nx"&gt;err&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;err&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;AbortError&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Fetch failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="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;fetchData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// ✅ Cleanup cancels the in-flight request and marks this invocation stale&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;active&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;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&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="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&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;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Loading...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;&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;The detail that matters: &lt;code&gt;active&lt;/code&gt; is declared &lt;em&gt;inside&lt;/em&gt; the effect, so each invocation has its own independent flag. When Strict Mode runs cleanup between the first and second mount, it sets the first invocation's &lt;code&gt;active&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; and aborts its request — the second mount starts fresh with &lt;code&gt;active = true&lt;/code&gt;. A shared &lt;code&gt;useRef&lt;/code&gt; across invocations would not work: the first effect's cleanup would set the ref to &lt;code&gt;false&lt;/code&gt; before the second effect's fetch resolves, silently dropping a valid response.&lt;/p&gt;

&lt;p&gt;Steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inside the &lt;code&gt;useEffect&lt;/code&gt; body, declare &lt;code&gt;let active = true&lt;/code&gt; and &lt;code&gt;const controller = new AbortController()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Pass &lt;code&gt;signal: controller.signal&lt;/code&gt; to every &lt;code&gt;fetch&lt;/code&gt; inside the effect.&lt;/li&gt;
&lt;li&gt;Guard all &lt;code&gt;setState&lt;/code&gt; calls with &lt;code&gt;if (active)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Return a cleanup that sets &lt;code&gt;active = false&lt;/code&gt; and calls &lt;code&gt;controller.abort()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Confirm it's gone in production
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the corrected pattern, the first fetch is aborted (&lt;code&gt;AbortError&lt;/code&gt; is swallowed) and only the second mount's fetch completes, setting state exactly once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The double-mount cycle disappears entirely — Strict Mode is off in production, so the effect runs once and the single fetch resolves normally.&lt;/p&gt;

&lt;p&gt;If double logs persist in dev after this, check whether your Supabase client is reinitializing on every render. That happens when &lt;code&gt;createClient&lt;/code&gt; is called inside a component — move it to a module-level constant or wrap it in &lt;code&gt;useMemo&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't disable Strict Mode
&lt;/h2&gt;

&lt;p&gt;You will find blog posts suggesting 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="c1"&gt;// ❌ NEVER do this&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="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StrictMode&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or stripping &lt;code&gt;&amp;lt;React.StrictMode&amp;gt;&lt;/code&gt; from &lt;code&gt;root.render(...)&lt;/code&gt;. This is dangerous. Strict Mode catches real bugs — missing cleanup, race conditions, non-idempotent effects — before they reach production. I have seen teams ship duplicate API calls to production because they disabled Strict Mode to silence the double log.&lt;/p&gt;

&lt;p&gt;Treat the double execution as a feature flag: if your effect runs twice in dev, it &lt;em&gt;will&lt;/em&gt; break under real conditions. Fix it; do not silence it. I go deeper on React 18's stricter behavior in &lt;a href="https://www.iloveblogs.blog/guides/react-server-components-deep-dive" rel="noopener noreferrer"&gt;React Server Components: Complete Deep Dive&lt;/a&gt;, including how Server Components sidestep this entirely by not running effects on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  useLayoutEffect follows the same rule
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;useLayoutEffect&lt;/code&gt; runs synchronously after DOM mutations but before paint, with the same double-execution behavior in development. Because it blocks rendering, it is more prone to layout thrashing — so the cleanup discipline matters even more:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useLayoutEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Same effect, with cleanup&lt;/span&gt;
&lt;span class="nf"&gt;useLayoutEffect&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;handleResize&lt;/span&gt; &lt;span class="o"&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="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;Resized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleResize&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleResize&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;In production both &lt;code&gt;useEffect&lt;/code&gt; and &lt;code&gt;useLayoutEffect&lt;/code&gt; run once. In development both run twice, and both require cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spot leaks with a render counter
&lt;/h2&gt;

&lt;p&gt;To see the cycle explicitly, log render and cleanup counts:&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;// src/app/page.tsx&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;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="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;Page&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;renderCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&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="nx"&gt;renderCount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="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;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;`Effect #&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;renderCount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ran`&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;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;`Cleanup after effect #&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;renderCount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Render&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;renderCount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;&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;In development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Effect #1 ran
Cleanup after effect #1
Effect #2 ran
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Effect #1 ran
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see cleanup logs in production, something is wrong — likely a re-render from state changes, not a remount.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Why does useEffect only run twice in development and not in production?
&lt;/h3&gt;

&lt;p&gt;Strict Mode is disabled in production builds. React runs the double-mount only in development to catch side effects — it is a dev-time probe, not runtime behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  How can I prevent an API call from firing twice on page load?
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;AbortController&lt;/code&gt; to cancel pending requests on cleanup, and guard state updates with a local &lt;code&gt;let active = true&lt;/code&gt; flag declared inside the effect. Never assume the first request will "win".&lt;/p&gt;

&lt;h3&gt;
  
  
  Does useEffect running twice affect performance in production?
&lt;/h3&gt;

&lt;p&gt;No. Strict Mode is off in production; effects run once. The double execution is purely a development-time safety net.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I properly clean up a subscription or timer in useEffect?
&lt;/h3&gt;

&lt;p&gt;Return a cleanup function:&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="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="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&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="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;Tick&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1000&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="nf"&gt;clearInterval&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="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Is using a ref to stop the second execution of useEffect bad practice?
&lt;/h3&gt;

&lt;p&gt;No — &lt;code&gt;useRef&lt;/code&gt; for an &lt;code&gt;isMounted&lt;/code&gt; flag is standard when you cannot cancel the side effect (e.g., third-party libraries without cancellation APIs). Prefer &lt;code&gt;AbortController&lt;/code&gt; when possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is my console.log appearing twice even with an empty dependency array?
&lt;/h3&gt;

&lt;p&gt;Strict Mode remounts the component in development. Empty dependencies only prevent re-runs on &lt;em&gt;subsequent&lt;/em&gt; renders — not the initial double-mount.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I handle state updates that trigger another useEffect run (infinite loops)?
&lt;/h3&gt;

&lt;p&gt;Check for accidental dependencies — e.g., passing a new object/array literal as a dependency. Use &lt;code&gt;useMemo&lt;/code&gt; or &lt;code&gt;useCallback&lt;/code&gt; to stabilize references. I cover this in &lt;a href="https://www.iloveblogs.blog/guides/nextjs-performance-optimization" rel="noopener noreferrer"&gt;Next.js Performance Optimization: 10 Essential Techniques&lt;/a&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/react-server-components-deep-dive" rel="noopener noreferrer"&gt;React Server Components: Complete Deep Dive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/hubs/nextjs-supabase" rel="noopener noreferrer"&gt;Optimistic UI Patterns with Next.js Server Actions and Supabase Realtime&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;Next.js Hydration Mismatch: 8 Fixes for App Router (2026)&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: 10 Essential Techniques&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.iloveblogs.blog/post/react-18-hydration-failed-because-the-initial-ui-does-not-match-what" rel="noopener noreferrer"&gt;Fix React 18 hydration mismatch in Next.js&lt;/a&gt; — the other symptom of the same React 18 change. If the double effect also leaves the first paint disagreeing with the server HTML, fix the mismatch before the effect.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.iloveblogs.blog/post/why-useeffect-running-twice-and-how-to-handle-it-well-in-react" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>Which version of PostgreSQL am I running? 3 ways to check</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Fri, 11 Sep 2026 23:05:44 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/which-version-of-postgresql-am-i-running-3-ways-to-check-pe1</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/which-version-of-postgresql-am-i-running-3-ways-to-check-pe1</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you're staring at a terminal and the only thing on your mind is "Which version of PostgreSQL am I running?", the answer is one command away. The fastest way is &lt;code&gt;psql --version&lt;/code&gt; for the client, or &lt;code&gt;SELECT version();&lt;/code&gt; inside a &lt;code&gt;psql&lt;/code&gt; session for the server. If you don't have &lt;code&gt;psql&lt;/code&gt; installed, &lt;code&gt;pg_config --version&lt;/code&gt; works too.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; You need to know the exact PostgreSQL version to troubleshoot a query, verify feature support, or plan an upgrade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause:&lt;/strong&gt; PostgreSQL doesn't advertise its version in an obvious place—you have to ask for it explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Use &lt;code&gt;psql --version&lt;/code&gt;, &lt;code&gt;SELECT version();&lt;/code&gt;, or &lt;code&gt;pg_config --version&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification:&lt;/strong&gt; Run any of those commands and confirm the output matches the version you expect.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The question, decoded
&lt;/h2&gt;

&lt;p&gt;You typed "Which version of PostgreSQL am I running?" into Google because you're in one of these situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A query that worked on your local machine fails on production, and you suspect a version mismatch.&lt;/li&gt;
&lt;li&gt;You're about to use a feature like &lt;code&gt;GENERATED ALWAYS AS IDENTITY&lt;/code&gt; or &lt;code&gt;MERGE&lt;/code&gt; and need to know if your server supports it.&lt;/li&gt;
&lt;li&gt;A security advisory just dropped and you want to confirm you're on a patched release.&lt;/li&gt;
&lt;li&gt;You inherited a database and have no idea what's running under the hood.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The symptom isn't an error message—it's the absence of information. You're logged into a server, maybe you can connect with &lt;code&gt;psql&lt;/code&gt;, maybe you can't. The version is there, but you need the right incantation to surface it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you need to know the version
&lt;/h2&gt;

&lt;p&gt;PostgreSQL releases a new major version every year, and each one brings syntax changes, new functions, and sometimes breaking behaviour. If you're writing SQL that uses &lt;code&gt;jsonb_path_query_first()&lt;/code&gt; (added in v12) or &lt;code&gt;MERGE&lt;/code&gt; (added in v15), running it against an older server will throw a syntax error. The same goes for extensions like &lt;code&gt;pg_cron&lt;/code&gt; or &lt;code&gt;pg_net&lt;/code&gt;—they have minimum version requirements.&lt;/p&gt;

&lt;p&gt;Beyond feature compatibility, the version tells you about security. PostgreSQL 16.4 includes fixes that 16.3 doesn't. If you're on a managed service like Supabase, the platform handles patching, but you still need to know which major version your project is on to plan migrations.&lt;/p&gt;

&lt;p&gt;The root cause of the confusion is that PostgreSQL doesn't print its version on login by default. The &lt;code&gt;psql&lt;/code&gt; welcome banner shows the version, but only if you connect interactively. If you're scripting or using a GUI, you might never see it. And if &lt;code&gt;psql&lt;/code&gt; isn't installed at all, you need another way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: 3 ways to check your PostgreSQL version
&lt;/h2&gt;

&lt;p&gt;I'll walk through the three methods I use in production, ordered from most common to fallback.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. psql client version (fastest)
&lt;/h3&gt;

&lt;p&gt;If you have the PostgreSQL client tools installed, this is the one-liner:&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;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or the short form:&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;-V&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;psql (PostgreSQL) 16.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tells you the &lt;strong&gt;client&lt;/strong&gt; version, which usually matches the server version if you installed the full PostgreSQL package. But it's not a guarantee—you could have a v16 client talking to a v15 server. If you get &lt;code&gt;psql: command not found&lt;/code&gt;, you need to install the client first. I cover that exact scenario in &lt;a href="https://www.iloveblogs.blog/post/fix-psql-command-not-found-install-postgresql-client" rel="noopener noreferrer"&gt;Fix: psql: command not found (Install PostgreSQL Client)&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SQL query (server version, always accurate)
&lt;/h3&gt;

&lt;p&gt;Connect to the database with &lt;code&gt;psql&lt;/code&gt; (or any SQL client) and run:&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="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output 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;PostgreSQL 16.3 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 13.2.0, 64-bit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the &lt;strong&gt;server&lt;/strong&gt; version—the one that actually matters for query compatibility. It includes the OS, architecture, and compiler details, which can be useful when debugging platform-specific issues.&lt;/p&gt;

&lt;p&gt;If you only need the version number without the extra fluff, use:&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;SHOW&lt;/span&gt; &lt;span class="n"&gt;server_version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns just &lt;code&gt;16.3&lt;/code&gt;. For scripting, you can also query &lt;code&gt;current_setting('server_version_num')&lt;/code&gt; to get an integer like &lt;code&gt;160003&lt;/code&gt; that's easy to compare programmatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. pg_config (when psql isn't available)
&lt;/h3&gt;

&lt;p&gt;If you're on a server that has the PostgreSQL development headers installed but not the client, &lt;code&gt;pg_config&lt;/code&gt; is your friend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pg_config &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL 16.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reports the version of the installed server package. It's especially handy in CI pipelines where you might have &lt;code&gt;libpq-dev&lt;/code&gt; but not the full &lt;code&gt;psql&lt;/code&gt; binary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bonus: check via package manager
&lt;/h3&gt;

&lt;p&gt;On Debian/Ubuntu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dpkg &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On RHEL/CentOS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rpm &lt;span class="nt"&gt;-qa&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These list installed packages, and the version number is right in the package name (e.g., &lt;code&gt;postgresql-16&lt;/code&gt;). This is a quick way to confirm what's installed without needing any PostgreSQL tool at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supabase dashboard
&lt;/h3&gt;

&lt;p&gt;If your database lives on Supabase, you don't need a terminal. Go to your project dashboard → Settings → Database, and the PostgreSQL version is displayed under "Database version". This is the server version your project is running. I use this all the time before testing new features—knowing the version upfront saves me from writing SQL that will fail on deploy. For more on navigating the Supabase SQL editor, see &lt;a href="https://www.iloveblogs.blog/post/how-to-show-tables-in-postgresql-psql-supabase" rel="noopener noreferrer"&gt;PostgreSQL SHOW TABLES / DESCRIBE TABLE (psql + Supabase)&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the version
&lt;/h2&gt;

&lt;p&gt;Pick any method above and run it. You should see a version string that matches what you expect. If you're verifying after an upgrade, compare the output against the target version. For example, after upgrading a Supabase project from v15 to v16, running &lt;code&gt;SELECT version();&lt;/code&gt; should now show &lt;code&gt;PostgreSQL 16.x&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the version doesn't match, double-check that you're connected to the right host and port. It's easy to accidentally run &lt;code&gt;psql&lt;/code&gt; against a local instance when your production database is elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two common pitfalls when checking the version
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pitfall A: client vs. server mismatch
&lt;/h3&gt;

&lt;p&gt;You run &lt;code&gt;psql --version&lt;/code&gt; and see &lt;code&gt;16.3&lt;/code&gt;, but &lt;code&gt;SELECT version();&lt;/code&gt; returns &lt;code&gt;15.7&lt;/code&gt;. This happens when the client tools were upgraded independently of the server. Always trust the server version from &lt;code&gt;SELECT version();&lt;/code&gt; for compatibility decisions. The client version only matters for &lt;code&gt;psql&lt;/code&gt;-specific features like &lt;code&gt;\gexec&lt;/code&gt; or &lt;code&gt;\if&lt;/code&gt; meta-commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall B: multiple PostgreSQL installations
&lt;/h3&gt;

&lt;p&gt;On a dev machine, you might have PostgreSQL 14 from the system package manager and PostgreSQL 16 from a Homebrew or Docker install. Running &lt;code&gt;psql&lt;/code&gt; might pick up whichever binary is first in your &lt;code&gt;PATH&lt;/code&gt;. Use &lt;code&gt;which psql&lt;/code&gt; to see which one you're invoking, and if needed, specify the full path to the correct &lt;code&gt;psql&lt;/code&gt; binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep your version knowledge handy
&lt;/h2&gt;

&lt;p&gt;I make it a habit to pin the PostgreSQL version in my project's README and in infrastructure-as-code configs. That way, when I'm debugging a query six months later, I don't have to re-discover which version the staging database is on. If you're using Docker, tag your images with the exact minor version (&lt;code&gt;postgres:16.3&lt;/code&gt;) instead of &lt;code&gt;latest&lt;/code&gt; to avoid surprise upgrades.&lt;/p&gt;

&lt;p&gt;For teams, add a database migration that inserts the current version into a &lt;code&gt;schema_versions&lt;/code&gt; table. That gives you an audit trail of when upgrades happened and what version was active at any point in time.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  How do I check the PostgreSQL version from the command line?
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;psql --version&lt;/code&gt; or &lt;code&gt;psql -V&lt;/code&gt; to see the client version. To check the server version, connect with &lt;code&gt;psql&lt;/code&gt; and run &lt;code&gt;SELECT version();&lt;/code&gt; or use &lt;code&gt;pg_config --version&lt;/code&gt; if the development headers are installed.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the SQL query to get the PostgreSQL version?
&lt;/h3&gt;

&lt;p&gt;Execute &lt;code&gt;SELECT version();&lt;/code&gt; inside a PostgreSQL session. This returns a string like &lt;code&gt;PostgreSQL 16.3 on x86_64-pc-linux-gnu, compiled by gcc...&lt;/code&gt; that includes the full version number and build details.&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-psql-command-not-found-install-postgresql-client" rel="noopener noreferrer"&gt;Fix: psql: command not found (Install PostgreSQL Client)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/how-to-show-tables-in-postgresql-psql-supabase" rel="noopener noreferrer"&gt;PostgreSQL SHOW TABLES / DESCRIBE TABLE (psql + Supabase)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/postgres-drop-all-tables-reset-database-safely" rel="noopener noreferrer"&gt;How to Drop All Tables in PostgreSQL Safely (2026)&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/which-version-of-postgresql-am-i-running" 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>troubleshooting</category>
    </item>
    <item>
      <title>NextJS Warning: Extra attributes from the server – Fix 2026</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Fri, 11 Sep 2026 23:05:37 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/nextjs-warning-extra-attributes-from-the-server-fix-2026-50jk</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/nextjs-warning-extra-attributes-from-the-server-fix-2026-50jk</guid>
      <description>&lt;p&gt;In my own Next.js + Supabase project this line started showing up in the console after I added a third‑party analytics snippet directly inside a component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warning: Extra attributes from the server: data-new-gr-c-s-check-loaded="true"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page is rendered on the server, sent to the browser, and then React tries to hydrate the markup on the client. The server‑side HTML contains a &lt;code&gt;data-new-gr-c-s-check-loaded&lt;/code&gt; attribute that the client‑side React tree never creates, so React logs the warning and continues. The behavior is reproducible on local development (&lt;code&gt;npm run dev&lt;/code&gt;) as well as on Vercel preview deployments, but only when the offending script is present.&lt;/p&gt;

&lt;p&gt;The warning itself is harmless — the page still works — but it signals a hidden source of nondeterminism that can cause subtle bugs, especially when you rely on server‑only data (e.g., Supabase session cookies) that might be overwritten. Here is where the attribute comes from and three ways to make it go away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who injects &lt;code&gt;data-new-gr-c-s-check-loaded&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The warning is a direct result of a hydration mismatch. During the initial render Next.js streams HTML generated by React on the server. After the browser receives that HTML, React mounts the same component tree on the client and expects the DOM to match exactly. If any attribute appears in the server markup that React never renders, it flags the difference as “extra attributes from the server”.&lt;/p&gt;

&lt;p&gt;Two common culprits inject the &lt;code&gt;data-new-gr-c-s-check-loaded&lt;/code&gt; attribute:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Browser extensions&lt;/strong&gt; – Grammarly, for example, injects a &lt;code&gt;data-gr-ext-installed&lt;/code&gt; attribute onto the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; element. The server HTML never contains this attribute; the extension adds it client‑side after the page loads. React then sees an attribute in the live DOM that was not in the server markup and logs the hydration warning. Because the injection target is &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;, adding &lt;code&gt;suppressHydrationWarning&lt;/code&gt; to the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; tag (not &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;) is the correct way to silence it in Next.js App Router layouts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Third‑party scripts added via plain &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags&lt;/strong&gt; – Many analytics or widget providers ask you to paste a &lt;code&gt;&amp;lt;script src="…"&amp;gt;&lt;/code&gt; snippet directly into your JSX. If that script writes &lt;code&gt;data-*&lt;/code&gt; attributes onto the root element (or any element) during its load phase, the server‑side HTML will miss those attributes, and React will warn.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both scenarios break the invariant that server and client markup must be identical before hydration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What React actually compares during hydration
&lt;/h2&gt;

&lt;p&gt;The relevant code path is the client-side hydration reconciler in &lt;code&gt;react-dom&lt;/code&gt;. During hydration the client calls &lt;code&gt;warnForExtraAttributes&lt;/code&gt; (in &lt;code&gt;ReactDOMComponent.js&lt;/code&gt;) which compares the attribute list of each DOM element against what React rendered. Any attribute present in the live DOM that React did not produce triggers the warning you see.&lt;/p&gt;

&lt;p&gt;That means the fix has two parts: (1) stop third‑party scripts from mutating the markup, and (2) optionally strip any stray attributes that extensions might add during the initial render.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1 — load external scripts with &lt;code&gt;next/script&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;next/script&lt;/code&gt; defers script execution until after React has hydrated, preventing the script from touching the server‑generated HTML.&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;// pages/_app.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppProps&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/app&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;Script&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/script&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;MyApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageProps&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;AppProps&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;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Example: Google Analytics */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"&lt;/span&gt;
        &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"afterInteractive"&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;Script&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"ga-init"&lt;/span&gt; &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"afterInteractive"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', 'G-XXXXXXX');
        `&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Script&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;Component&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;pageProps&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;/&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;That single change ensures the analytics script runs &lt;strong&gt;after&lt;/strong&gt; the React tree is already attached, so any &lt;code&gt;data-*&lt;/code&gt; attributes it adds will appear only on the client side &lt;em&gt;after&lt;/em&gt; hydration, and React will not compare them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2 — strip &lt;code&gt;data-gr-*&lt;/code&gt; attributes in a custom &lt;code&gt;_document&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you cannot control a third‑party script (or you need to support users with extensions that inject attributes), you can strip those attributes during the server render. Create a custom &lt;code&gt;_document&lt;/code&gt; that removes any &lt;code&gt;data-gr-*&lt;/code&gt; attributes from the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element before sending HTML to the browser.&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;// pages/_document.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Head&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Main&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextScript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DocumentContext&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/document&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;class&lt;/span&gt; &lt;span class="nc"&gt;MyDocument&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getInitialProps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DocumentContext&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;initialProps&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;Document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInitialProps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Clone the HTML to manipulate it safely&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;initialProps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="sr"&gt;/ data-gr-&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+="&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*"/g&lt;/span&gt;&lt;span class="p"&gt;,&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;...&lt;/span&gt;&lt;span class="nx"&gt;initialProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;render&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="nc"&gt;Html&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;Head&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;body&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;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="nc"&gt;NextScript&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;body&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;Html&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before / after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- &amp;lt;Html data-gr-ext-installed="true" data-new-gr-c-s-check-loaded="true"&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ &amp;lt;Html&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The regex removes any attribute that starts with &lt;code&gt;data-gr-&lt;/code&gt; or &lt;code&gt;data-new-gr-c-s-check-loaded&lt;/code&gt;. This approach is safe because those attributes are never used by your application logic; they are purely injected by external tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3 — defer your own DOM writes to &lt;code&gt;useEffect&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you have custom &lt;code&gt;useEffect&lt;/code&gt; code that writes attributes, wrap it in a check that runs only after the component is mounted.&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;// components/AnalyticsProvider.tsx&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;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="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;AnalyticsProvider&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&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;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/widget.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;script&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="k"&gt;return&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By ensuring the script runs after the first paint, you avoid contaminating the server markup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Triage checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identify the offending script&lt;/strong&gt; – Look at the warning’s attribute name. If it contains &lt;code&gt;gr-&lt;/code&gt;, suspect Grammarly or a similar extension. If it’s a custom analytics name, locate the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag you added.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace raw &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags&lt;/strong&gt; with the &lt;code&gt;next/script&lt;/code&gt; component as shown above. Use &lt;code&gt;strategy="afterInteractive"&lt;/code&gt; for most cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a custom &lt;code&gt;_document&lt;/code&gt;&lt;/strong&gt; if you need a blanket removal of &lt;code&gt;data-gr-*&lt;/code&gt; attributes. Paste the code exactly; the regex will strip any matching attribute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the dev server&lt;/strong&gt; (&lt;code&gt;npm run dev&lt;/code&gt;). The warning should disappear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy&lt;/strong&gt; and verify on Vercel preview; the warning should stay gone because the server‑side HTML no longer contains the extra attributes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Confirming the hydration is clean
&lt;/h2&gt;

&lt;p&gt;Run the development server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:3000&lt;/code&gt; in a clean browser (disable extensions). You should see no warning in the terminal or browser console.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; next dev
ready - started server on http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the warning still appears, open the page source (&lt;code&gt;Ctrl+U&lt;/code&gt;) and search for &lt;code&gt;data-new-gr-c-s-check-loaded&lt;/code&gt;. If it is absent, the server markup is clean. Then open the DevTools Elements panel and look for the same attribute; if it appears only after the page loads, it means a client‑side script is still injecting it, and you need to adjust the script’s loading strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the source is out of your hands
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Extension‑only injection.&lt;/strong&gt; If you cannot control the user’s browser extensions, the &lt;code&gt;_document&lt;/code&gt; sanitisation is the reliable fix. The regex will strip the attribute regardless of which extension added it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inline scripts that write attributes early.&lt;/strong&gt; If you have an inline script that runs before React hydrates (e.g., a legacy analytics snippet placed in &lt;code&gt;pages/index.tsx&lt;/code&gt;), move it to &lt;code&gt;next/script&lt;/code&gt; with &lt;code&gt;strategy="afterInteractive"&lt;/code&gt; or wrap it in a &lt;code&gt;useEffect&lt;/code&gt; as shown earlier. This prevents the attribute from existing in the server HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping server markup deterministic
&lt;/h2&gt;

&lt;p&gt;React’s hydration algorithm assumes a &lt;strong&gt;deterministic&lt;/strong&gt; render: given the same props and state, the server and client must produce identical DOM trees. Anything that mutates the DOM between the server response and the client render breaks that assumption. The safest pattern is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never embed raw &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags&lt;/strong&gt; that execute synchronously. Use &lt;code&gt;next/script&lt;/code&gt; with an explicit loading strategy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid DOM manipulation in the top‑level component body&lt;/strong&gt;; always place it inside &lt;code&gt;useEffect&lt;/code&gt; or &lt;code&gt;useLayoutEffect&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with extensions disabled&lt;/strong&gt; or use a headless browser (e.g., Playwright) to catch unexpected attribute injection before shipping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a lint rule&lt;/strong&gt; (via ESLint) that flags &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; JSX elements without &lt;code&gt;next/script&lt;/code&gt;. Example rule configuration can be found in the official Next.js ESLint plugin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By keeping the server‑generated markup pure and deferring any side‑effects until after hydration, you eliminate the “Extra attributes from the server” warning and keep your Next.js + Supabase app stable.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/react-server-components-deep-dive" rel="noopener noreferrer"&gt;React Server Components: Complete Deep Dive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/react-18-hydration-failed-because-the-initial-ui-does-not-match-what" rel="noopener noreferrer"&gt;Fix React 18 Hydration Mismatch in Next.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/typeerror-cookies-crash-nextjs-route-handler" rel="noopener noreferrer"&gt;TypeError cookies() crash in Next.js route handler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/fix-lcp-not-working-in-production" rel="noopener noreferrer"&gt;Fix "lcp" not working in production&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/what-causes-nextjs-warning-extra-attributes-from-the-server-data-ne" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>supabase</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>TypeScript 6.0 Migration: What Actually Breaks in Next.js/Supabase</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Fri, 11 Sep 2026 23:04:56 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/typescript-60-migration-what-actually-breaks-in-nextjssupabase-n4p</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/typescript-60-migration-what-actually-breaks-in-nextjssupabase-n4p</guid>
      <description>&lt;p&gt;TypeScript 6.0 shipped on March 23, 2026, and it's the last version of the compiler written in JavaScript before the team's full-speed move to the Go-based TypeScript 7 (&lt;code&gt;tsgo&lt;/code&gt;). Because of that, 6.0 is a genuine "spring cleaning" release: several long-deprecated options are gone, strict mode is on by default, and legacy targets are removed outright rather than just discouraged.&lt;/p&gt;

&lt;p&gt;Most of the writeups floating around right now are abstract changelogs. This is what those changes actually do to a real Next.js App Router + Supabase project — the errors you'll see, and the order that minimizes wasted work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strict mode is on by default.&lt;/strong&gt; If your &lt;code&gt;tsconfig.json&lt;/code&gt; never explicitly set &lt;code&gt;"strict": false&lt;/code&gt;, you're now opted into &lt;code&gt;strictNullChecks&lt;/code&gt;, &lt;code&gt;noImplicitAny&lt;/code&gt;, and the rest of the bundle, even if you never asked for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;target: "es5"&lt;/code&gt; is deprecated&lt;/strong&gt; (ES2015 is now the effective floor), and &lt;strong&gt;&lt;code&gt;moduleResolution: "classic"&lt;/code&gt; is removed entirely.&lt;/strong&gt; You must use &lt;code&gt;"node16"&lt;/code&gt;, &lt;code&gt;"nodenext"&lt;/code&gt;, or &lt;code&gt;"bundler"&lt;/code&gt; (the one Next.js projects almost always want). Per the official &lt;a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-6-0.html" rel="noopener noreferrer"&gt;TypeScript 6.0 release notes&lt;/a&gt;, deprecated options still compile in 6.0 (silence the warning with &lt;code&gt;"ignoreDeprecations": "6.0"&lt;/code&gt; if you need breathing room) but are slated for hard removal in TypeScript 7.0 — treat the deprecation warning as a deadline, not background noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;module: "amd" | "umd" | "systemjs" | "none"&lt;/code&gt; are deprecated&lt;/strong&gt;, not yet hard errors. These predate the ESM ecosystem and have no place in an App Router project, but you may have inherited one from a dependency's shared tsconfig — fix it now rather than waiting for TypeScript 7.0 to force the issue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All code is now assumed to be in strict-mode JavaScript syntax.&lt;/strong&gt; If legacy or generated code uses reserved words (&lt;code&gt;await&lt;/code&gt;, &lt;code&gt;static&lt;/code&gt;, &lt;code&gt;private&lt;/code&gt;, &lt;code&gt;public&lt;/code&gt;, etc.) as plain identifiers, that now fails to parse. This mostly affects legacy scripts or generated code, not typical Next.js source.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fix order for a Next.js + Supabase project
&lt;/h2&gt;

&lt;p&gt;Trying to fix everything at once produces a wall of hundreds of errors with no sense of progress. This order clears the noisy, mechanical errors first so what's left is the genuine type-safety issues worth your attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Fix config-level errors first.&lt;/strong&gt; Run &lt;code&gt;tsc --noEmit&lt;/code&gt; and look only at errors that reference &lt;code&gt;tsconfig.json&lt;/code&gt; itself — &lt;code&gt;moduleResolution&lt;/code&gt;, &lt;code&gt;target&lt;/code&gt;, &lt;code&gt;module&lt;/code&gt; values. These are one-line fixes and they unblock everything downstream. For a standard Next.js App Router project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"es2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bundler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"esnext"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Fix &lt;code&gt;strictNullChecks&lt;/code&gt; errors in your data layer next&lt;/strong&gt;, specifically anywhere you touch a Supabase client response. Supabase's generated types already model nullability accurately (a &lt;code&gt;.single()&lt;/code&gt; query can return &lt;code&gt;null&lt;/code&gt;), but code written under lenient mode often accessed &lt;code&gt;.data.someField&lt;/code&gt; without a null check because it "worked" at runtime. This is where strict mode earns its keep — and where you'll find the highest-value real bugs, not just noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Fix &lt;code&gt;noImplicitAny&lt;/code&gt; in Server Actions and API route handlers last.&lt;/strong&gt; These tend to be the highest volume of trivial errors (untyped &lt;code&gt;formData.get()&lt;/code&gt; results, untyped request bodies) and the lowest risk — mechanical typing, not logic changes. Doing this last means your context budget goes to the data-layer bugs that matter, not to typing 200 form handlers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Re-run your test suite, not just the type checker.&lt;/strong&gt; Strict null checks catching a real bug means a code path changes behavior, not just its types — a &lt;code&gt;.single()&lt;/code&gt; result that was silently &lt;code&gt;null&lt;/code&gt; and got treated as an object might now throw where it used to fail silently downstream. That's the fix working as intended, but it needs a test pass to confirm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The error you'll hit that isn't really about TypeScript 6.0
&lt;/h2&gt;

&lt;p&gt;If you see &lt;code&gt;Cannot find module 'X' or its corresponding type declarations&lt;/code&gt; appearing for packages that worked fine before, check whether it's actually the &lt;code&gt;moduleResolution&lt;/code&gt; change surfacing a package that never shipped proper ESM exports — not a new TypeScript 6.0 bug. See &lt;a href="https://www.iloveblogs.blog/post/typescript-could-not-find-declaration-file-module-fix" rel="noopener noreferrer"&gt;TypeScript: could not find declaration file for module — fix&lt;/a&gt; for the general diagnostic steps, which still apply.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/typescript-javascript-migration-guide-2026" rel="noopener noreferrer"&gt;JavaScript to TypeScript migration guide 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/typescript-module-has-no-exported-member-fix" rel="noopener noreferrer"&gt;TypeScript: "Module has no exported member" — fix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/typescript-property-has-no-initializer-fix" rel="noopener noreferrer"&gt;TypeScript: property has no initializer — fix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/interfaces-vs-types-in-typescript" rel="noopener noreferrer"&gt;Interfaces vs. types in TypeScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-type-safety-guide" rel="noopener noreferrer"&gt;Next.js + Supabase type safety guide&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/typescript-6-migration-nextjs-supabase-breaking-changes" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>typescript6</category>
      <category>migration</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Fix An index signature parameter type cannot be a union</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Fri, 11 Sep 2026 23:04:49 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/fix-an-index-signature-parameter-type-cannot-be-a-union-5c7o</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/fix-an-index-signature-parameter-type-cannot-be-a-union-5c7o</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you're seeing &lt;code&gt;An index signature parameter type cannot be a union type. Consider using a mapped object type instead&lt;/code&gt;, the cause is that TypeScript index signatures only accept &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, or &lt;code&gt;symbol&lt;/code&gt; as the key type. Fix it by replacing the index signature with a mapped object type using the &lt;code&gt;in&lt;/code&gt; operator, or the &lt;code&gt;Record&lt;/code&gt; utility type.&lt;/p&gt;

&lt;p&gt;If that doesn't work, scroll to verify the fix — there are two common variants this guide also covers.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; TypeScript compiler error "An index signature parameter type cannot be a union type. Consider using a mapped object type instead."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause:&lt;/strong&gt; Index signatures describe &lt;em&gt;all possible&lt;/em&gt; string/number keys; a union type restricts keys to a specific set, which is a contradiction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Replace &lt;code&gt;[key: UnionType]: ValueType&lt;/code&gt; with &lt;code&gt;[key in UnionType]: ValueType&lt;/code&gt; inside a &lt;code&gt;type&lt;/code&gt; alias, or use &lt;code&gt;Record&amp;lt;UnionType, ValueType&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification:&lt;/strong&gt; Run &lt;code&gt;tsc --noEmit&lt;/code&gt; and confirm zero errors related to the index signature.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you'll see
&lt;/h2&gt;

&lt;p&gt;A developer on Stack Overflow &lt;a href="https://stackoverflow.com/questions/54438012/an-index-signature-parameter-type-cannot-be-a-union-type-consider-using-a-mappe" rel="noopener noreferrer"&gt;reported this exact error&lt;/a&gt; when trying to use a TypeScript enum as the key type in an interface index signature. The compiler output is unambiguous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;An index signature parameter type cannot be a union type. Consider using a mapped object type instead.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It happens when you write something like this:&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="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;Option&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ONE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;one&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;TWO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;two&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;THREE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;three&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;someBool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;someString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirements&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;//  ~~~~~~~~~&lt;/span&gt;
  &lt;span class="c1"&gt;//  An index signature parameter type cannot be a union type.&lt;/span&gt;
  &lt;span class="c1"&gt;//  Consider using a mapped object type instead.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The behavior is the same across all TypeScript versions that support mapped types. It fires whether you use an enum, a union of string literals, or any type that isn't exactly &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, or &lt;code&gt;symbol&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root cause
&lt;/h2&gt;

&lt;p&gt;An index signature in TypeScript describes an object that can have &lt;em&gt;any number&lt;/em&gt; of properties, as long as every property's key matches the index signature parameter type and every property's value matches the value type. The language specification restricts the key type to &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, or &lt;code&gt;symbol&lt;/code&gt; — and for good reason.&lt;/p&gt;

&lt;p&gt;When you write &lt;code&gt;[key: string]: SomeType&lt;/code&gt;, you're telling the compiler "this object may have any string key, and every value under any string key must be &lt;code&gt;SomeType&lt;/code&gt;." That's an open-ended contract. A union type like &lt;code&gt;Option.ONE | Option.TWO | Option.THREE&lt;/code&gt; is a closed set — it says "only these three specific strings are allowed as keys." Those two ideas are fundamentally incompatible. An index signature cannot both be open-ended (any string) and closed (only these three strings) at the same time.&lt;/p&gt;

&lt;p&gt;The compiler detects this contradiction and suggests the correct alternative: a mapped object type. A mapped type iterates over a specific set of keys and produces a type with exactly those keys, which is what you actually want when you use an enum or a union of string literals as the key set.&lt;/p&gt;

&lt;p&gt;The relevant code path that triggers the error is any &lt;code&gt;interface&lt;/code&gt; or &lt;code&gt;type&lt;/code&gt; declaration containing an index signature whose parameter type is not &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, or &lt;code&gt;symbol&lt;/code&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;// Both of these trigger the error:&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Broken&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// union type&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AlsoBroken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MyEnum&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// enum (which is a union under the hood)&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The fix: replace the index signature with a mapped type
&lt;/h2&gt;

&lt;p&gt;The solution is to use the &lt;code&gt;in&lt;/code&gt; operator inside a &lt;code&gt;type&lt;/code&gt; alias. The &lt;code&gt;in&lt;/code&gt; operator iterates over each member of a union type and creates a property for each one:&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="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;Option&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ONE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;one&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;TWO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;two&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;THREE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;three&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;someBool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;someString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirement&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;That single change addresses the cause because &lt;code&gt;key in Option&lt;/code&gt; tells TypeScript "for each individual member of the &lt;code&gt;Option&lt;/code&gt; union, create a property with that exact key and type &lt;code&gt;OptionRequirement&lt;/code&gt;." The result is an object type with exactly three known keys — &lt;code&gt;one&lt;/code&gt;, &lt;code&gt;two&lt;/code&gt;, and &lt;code&gt;three&lt;/code&gt; — each mapped to &lt;code&gt;OptionRequirement&lt;/code&gt;. This is a closed, specific shape, not an open-ended index signature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step by step
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open the file containing the broken &lt;code&gt;interface&lt;/code&gt; or &lt;code&gt;type&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Locate the index signature that uses a union type or enum as the key — it will have the form &lt;code&gt;[key: SomeUnion]: SomeType&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Change the &lt;code&gt;interface&lt;/code&gt; to a &lt;code&gt;type&lt;/code&gt; alias (mapped types cannot appear inside &lt;code&gt;interface&lt;/code&gt; declarations).&lt;/li&gt;
&lt;li&gt;Replace the colon (&lt;code&gt;:&lt;/code&gt;) after &lt;code&gt;key&lt;/code&gt; with the &lt;code&gt;in&lt;/code&gt; operator.&lt;/li&gt;
&lt;li&gt;Save and run &lt;code&gt;tsc --noEmit&lt;/code&gt; to verify.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The fix: use the &lt;code&gt;Record&lt;/code&gt; utility type as a shorthand
&lt;/h2&gt;

&lt;p&gt;TypeScript ships with a built-in utility type called &lt;code&gt;Record&amp;lt;K, V&amp;gt;&lt;/code&gt; that does exactly what the mapped type above does, but with less syntax:&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="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;Option&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ONE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;one&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;TWO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;two&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;THREE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;three&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;someBool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;someString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirement&lt;/span&gt;&lt;span class="o"&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;&lt;code&gt;Record&amp;lt;Option, OptionRequirement&amp;gt;&lt;/code&gt; expands to the same mapped type &lt;code&gt;{ [key in Option]: OptionRequirement }&lt;/code&gt;. It's a direct, readable shortcut. Use it when you don't need to add additional properties or modifiers to the mapped type — &lt;code&gt;Record&lt;/code&gt; gives you a plain object type with all keys required and all values of the same type.&lt;/p&gt;

&lt;p&gt;If you need to make some keys optional or add readonly modifiers, fall back to the explicit mapped type syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;]?:&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// all keys optional&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ReadonlyOptionRequirements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// all keys readonly&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Important: mapped types must be inside a &lt;code&gt;type&lt;/code&gt; alias, not an &lt;code&gt;interface&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A common mistake after learning the fix is to try to use the &lt;code&gt;in&lt;/code&gt; operator inside an &lt;code&gt;interface&lt;/code&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;// This does NOT compile:&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirements&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;//     ~~&lt;/span&gt;
  &lt;span class="c1"&gt;//     'in' operator is not allowed in an interface.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript interfaces do not support mapped type syntax. The &lt;code&gt;in&lt;/code&gt; operator, along with &lt;code&gt;keyof&lt;/code&gt;, &lt;code&gt;as&lt;/code&gt; (key remapping), and other mapped type features, are exclusive to &lt;code&gt;type&lt;/code&gt; aliases. If your type needs to be extended by other interfaces or used with &lt;code&gt;implements&lt;/code&gt; on a class, you can still use a &lt;code&gt;type&lt;/code&gt; alias — classes can implement object types defined with &lt;code&gt;type&lt;/code&gt; as long as the shape is a plain object type.&lt;/p&gt;

&lt;p&gt;This distinction between &lt;code&gt;interface&lt;/code&gt; and &lt;code&gt;type&lt;/code&gt; comes up frequently in production TypeScript codebases. I cover the full decision framework in the &lt;a href="https://www.iloveblogs.blog/guides/interfaces-vs-types-in-typescript" rel="noopener noreferrer"&gt;Interfaces vs Types in TypeScript guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the fix
&lt;/h2&gt;

&lt;p&gt;Run the TypeScript compiler in check-only mode to confirm the error is gone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsc &lt;span class="nt"&gt;--noEmit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see no output (zero errors). If the error persists, check for these two common variants.&lt;/p&gt;

&lt;h3&gt;
  
  
  Variant A — you used &lt;code&gt;:&lt;/code&gt; instead of &lt;code&gt;in&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The most frequent mistake after learning about mapped types is writing &lt;code&gt;[key: Options]&lt;/code&gt; instead of &lt;code&gt;[key in Options]&lt;/code&gt;. The colon tells TypeScript you're still trying to write an index signature, and the error fires again. The fix is to replace the colon with &lt;code&gt;in&lt;/code&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;// Wrong — still an index signature:&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ProviderProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;items&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="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PossibleKeysType&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SectionItemsType&lt;/span&gt;&lt;span class="o"&gt;&amp;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;// Correct — mapped type:&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ProviderProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;items&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;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;PossibleKeysType&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SectionItemsType&lt;/span&gt;&lt;span class="o"&gt;&amp;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;h3&gt;
  
  
  Variant B — you defined the mapped type inside an &lt;code&gt;interface&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;If you see an error about the &lt;code&gt;in&lt;/code&gt; operator not being allowed, you've placed the mapped type inside an &lt;code&gt;interface&lt;/code&gt; body. Convert the &lt;code&gt;interface&lt;/code&gt; to a &lt;code&gt;type&lt;/code&gt; alias:&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;// Wrong — interface doesn't support mapped types:&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirements&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Correct — type alias does:&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;OptionRequirement&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;h2&gt;
  
  
  Why this happens (and how to avoid it next time)
&lt;/h2&gt;

&lt;p&gt;The invariant is simple: index signatures describe &lt;em&gt;all possible&lt;/em&gt; keys of a given category (&lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;symbol&lt;/code&gt;). A union type describes a &lt;em&gt;specific, finite&lt;/em&gt; set of keys. When you need an object type with a specific set of keys derived from a union or enum, you want a mapped type — not an index signature. The compiler's error message already tells you this, but the distinction between the two concepts is what trips people up.&lt;/p&gt;

&lt;p&gt;To prevent this error from appearing in your codebase, enable the &lt;code&gt;@typescript-eslint/consistent-indexed-object-style&lt;/code&gt; rule if you're using ESLint with TypeScript. It can enforce using &lt;code&gt;Record&lt;/code&gt; or mapped type syntax consistently and will flag index signatures that could be expressed more precisely. For a broader approach to catching type-level mistakes before they reach production, the &lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-type-safety-guide" rel="noopener noreferrer"&gt;Type Safety Guide for Next.js + Supabase in TypeScript&lt;/a&gt; walks through setting up strict TypeScript configurations that catch these issues at compile time.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Why can't I use a union type as an index signature parameter in TypeScript?
&lt;/h3&gt;

&lt;p&gt;TypeScript index signatures require the key type to be &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, or &lt;code&gt;symbol&lt;/code&gt; because they describe &lt;em&gt;all possible&lt;/em&gt; keys of an object. A union type restricts the keys to a specific set, which contradicts the open-ended nature of an index signature. Use a mapped object type with the &lt;code&gt;in&lt;/code&gt; operator instead — it iterates over the union members and creates a type with exactly those keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I use an enum as keys in a TypeScript object type?
&lt;/h3&gt;

&lt;p&gt;You cannot use an enum directly in an index signature. Instead, define a mapped type using &lt;code&gt;[key in MyEnum]: ValueType&lt;/code&gt; inside a &lt;code&gt;type&lt;/code&gt; alias, or use the &lt;code&gt;Record&amp;lt;MyEnum, ValueType&amp;gt;&lt;/code&gt; utility type. Both approaches iterate over the enum members and create a type with those specific keys. Mapped types must be defined in a &lt;code&gt;type&lt;/code&gt; alias, not an &lt;code&gt;interface&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between an index signature and a mapped type?
&lt;/h3&gt;

&lt;p&gt;An index signature (&lt;code&gt;[key: string]: T&lt;/code&gt;) says "this object can have any string key, and every value must be &lt;code&gt;T&lt;/code&gt;." It's open-ended. A mapped type (&lt;code&gt;[key in SomeUnion]: T&lt;/code&gt;) says "this object has exactly the keys in &lt;code&gt;SomeUnion&lt;/code&gt;, and each value is &lt;code&gt;T&lt;/code&gt;." It's closed and specific. Use index signatures when the set of keys is truly unbounded; use mapped types when you know the exact keys ahead of time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use a union of string literals as keys in an interface?
&lt;/h3&gt;

&lt;p&gt;Not with an index signature. If you need an interface with a specific set of keys from a union, you have two options: either write the keys out explicitly in the interface, or use a &lt;code&gt;type&lt;/code&gt; alias with a mapped type. Interfaces do not support the &lt;code&gt;in&lt;/code&gt; operator or mapped type syntax. If your type doesn't need to be merged via declaration merging, a &lt;code&gt;type&lt;/code&gt; alias is the more flexible choice.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/interfaces-vs-types-in-typescript" rel="noopener noreferrer"&gt;Interfaces vs Types in TypeScript: 2026 Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-type-safety-guide" rel="noopener noreferrer"&gt;Type Safety Guide for Next.js + Supabase in TypeScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/nextjs-tsconfig-paths-not-working-fix" rel="noopener noreferrer"&gt;Fix tsconfig Paths Not Working in Next.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/how-to-force-tsc-to-ignore-nodemodules-folder" rel="noopener noreferrer"&gt;Force tsc to Ignore node_modules: Fix TS Errors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/get-and-set-in-typescript" rel="noopener noreferrer"&gt;TypeScript Getter Setter Errors: TS1056, TS1028, TS2378 Fix&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/an-index-signature-parameter-type-cannot-be-a-union-type-consider-usi" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>troubleshooting</category>
      <category>mappedtypes</category>
    </item>
    <item>
      <title>TypeError cookies() crash in Next.js route handler</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Fri, 11 Sep 2026 17:25:35 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/typeerror-cookies-crash-in-nextjs-route-handler-2bp1</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/typeerror-cookies-crash-in-nextjs-route-handler-2bp1</guid>
      <description>&lt;p&gt;A route handler that reads or writes cookies starts returning 500s right after a deploy to Vercel, and the logs show this stack trace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: TypeError: cookies() is not a function
    at async /app/api/auth/login/route.ts:12:25
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The short version: in Next.js 15+ the &lt;code&gt;cookies()&lt;/code&gt; helper is asynchronous. Call it without &lt;code&gt;await&lt;/code&gt; and the variable you think holds a cookie store actually holds a Promise — the next &lt;code&gt;.get()&lt;/code&gt; or &lt;code&gt;.set()&lt;/code&gt; blows up with this TypeError. Adding &lt;code&gt;await&lt;/code&gt; in front of &lt;code&gt;cookies()&lt;/code&gt; (and updating any downstream code that expects a cookie store) resolves it.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;TypeError: cookies() is not a function&lt;/code&gt; in a route handler&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause:&lt;/strong&gt; &lt;code&gt;cookies()&lt;/code&gt; is async in Next.js 15/16 and must be awaited&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Add &lt;code&gt;await&lt;/code&gt; to the call and update dependent code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification:&lt;/strong&gt; Run the handler locally or with &lt;code&gt;curl&lt;/code&gt; and see a 200 response instead of 500
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The stack trace blames your file, not the framework
&lt;/h2&gt;

&lt;p&gt;The error surfaces when a POST request hits a route handler that reads or writes cookies after a recent deployment to Vercel. The behavior is the same across staging, production, and local &lt;code&gt;npm run dev&lt;/code&gt; environments, which makes it easy to reproduce with a single &lt;code&gt;curl&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;I first noticed this after merging a feature branch that introduced a new authentication endpoint. The GitHub issue that sparked this investigation is &lt;a href="https://github.com/vercel/next.js/issues/51156" rel="noopener noreferrer"&gt;vercel/next.js#51156&lt;/a&gt;, where multiple teams reported intermittent 500 errors caused by the same stack trace. The error only appears after the code is built for production because the serverless runtime enforces the async contract of &lt;code&gt;cookies()&lt;/code&gt; more strictly than the dev server.&lt;/p&gt;

&lt;p&gt;The relevant code path lives in the Next.js core, but the part that touches your code is the call site inside your route file. When you look at the stack trace, the topmost frame points to your file, not the framework, which is why the issue appears to be “your code”.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed: &lt;code&gt;cookies()&lt;/code&gt; now returns a Promise
&lt;/h2&gt;

&lt;p&gt;In Next.js 15 the &lt;code&gt;cookies()&lt;/code&gt; helper was refactored to be asynchronous. The function now returns a &lt;code&gt;Promise&amp;lt;CookieStore&amp;gt;&lt;/code&gt; instead of a plain object. When you call &lt;code&gt;cookies()&lt;/code&gt; without &lt;code&gt;await&lt;/code&gt;, the variable you think holds a cookie store actually holds a Promise. The next line usually tries to read a cookie like &lt;code&gt;cookieStore.get('session')&lt;/code&gt;, but because &lt;code&gt;cookieStore&lt;/code&gt; is a Promise, JavaScript throws &lt;code&gt;TypeError: cookies() is not a function&lt;/code&gt;. The error propagates up to the route handler, causing the entire request to fail with a 500 status.&lt;/p&gt;

&lt;p&gt;Two things make this bug especially sneaky:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The TypeScript compiler does not always flag the missing &lt;code&gt;await&lt;/code&gt; because the return type of &lt;code&gt;cookies()&lt;/code&gt; is &lt;code&gt;any&lt;/code&gt; in older type definitions. That means the code compiles cleanly, but at runtime the mismatch surfaces.&lt;/li&gt;
&lt;li&gt;In the Vercel edge runtime, the handler is executed in a separate isolate where the Promise is never automatically resolved, so the error is not swallowed as it sometimes is in a Node.js dev server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is what the failing call site looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/auth/login/route.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cookieStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// ← missing await&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secret&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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;httpOnly&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Logged in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&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;Notice how &lt;code&gt;cookies()&lt;/code&gt; is called synchronously. The framework expects an async call, so the returned Promise is never resolved, and the subsequent &lt;code&gt;set&lt;/code&gt; call triggers the TypeError.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line change: &lt;code&gt;await cookies()&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&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="c1"&gt;// app/api/auth/login/route.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Await the async cookies helper&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secret&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="c1"&gt;// Now cookieStore is a proper CookieStore instance&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;httpOnly&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Logged in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&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;That single change addresses the cause because it respects the async contract introduced in Next.js 15. The &lt;code&gt;await&lt;/code&gt; forces the Promise to resolve before we interact with the cookie store, eliminating the &lt;code&gt;TypeError&lt;/code&gt;. If you have multiple route handlers that use &lt;code&gt;cookies()&lt;/code&gt;, apply the same pattern to each:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the file that contains the failing route handler, e.g., &lt;code&gt;app/api/auth/login/route.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Locate the line that reads &lt;code&gt;const cookieStore = cookies();&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Prefix the call with &lt;code&gt;await&lt;/code&gt; so the line becomes &lt;code&gt;const cookieStore = await cookies();&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Ensure the surrounding function is declared &lt;code&gt;async&lt;/code&gt; (it already is in most cases). Save the file and run &lt;code&gt;npm run dev&lt;/code&gt; or redeploy to Vercel.&lt;/li&gt;
&lt;li&gt;If you use TypeScript, you may also want to update the import: &lt;code&gt;import { cookies } from 'next/headers';&lt;/code&gt; – this stays the same, but the type definitions will now correctly show a &lt;code&gt;Promise&amp;lt;CookieStore&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wrote a companion guide that walks through the same pattern for the App Router in Next.js 15: &lt;a href="https://www.iloveblogs.blog/post/nextjs-15-cookies-async-error-fix" rel="noopener noreferrer"&gt;"Fix \"cookies() should be awaited\" Error in Next.js 15 App Router (Complete Migration Fix 2026)"&lt;/a&gt;. That article also explains why the lint rule &lt;code&gt;next/no-unawaited-cookies&lt;/code&gt; is useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove it with a curl request
&lt;/h2&gt;

&lt;p&gt;Run the following command against your local dev server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/api/auth/login &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"password":"secret"}'&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see output similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;set-cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;session=abc123; Path=/; HttpOnly&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;text/plain; charset=UTF-8&lt;/span&gt;
&lt;span class="na"&gt;Content-Length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;9&lt;/span&gt;

Logged in
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;set-cookie&lt;/code&gt; header is present and the status code is &lt;code&gt;200&lt;/code&gt; instead of &lt;code&gt;500&lt;/code&gt;. If you still receive a 500 response, double‑check that every &lt;code&gt;cookies()&lt;/code&gt; call in the file (and any imported helper) is awaited.&lt;/p&gt;

&lt;h2&gt;
  
  
  Still crashing? Two look-alike causes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;cookies&lt;/code&gt; imported from the wrong package
&lt;/h3&gt;

&lt;p&gt;Some projects import &lt;code&gt;cookies&lt;/code&gt; from &lt;code&gt;next/headers&lt;/code&gt; while others mistakenly use &lt;code&gt;next/cookies&lt;/code&gt;. The wrong import returns a stub that is not async, leading to a different error (&lt;code&gt;cookies is not a function&lt;/code&gt;). Ensure your import looks exactly like 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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see &lt;code&gt;import { cookies } from 'next/cookies';&lt;/code&gt;, replace it with the correct path and re‑run the verification command.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;cookies()&lt;/code&gt; called from middleware instead of a route handler
&lt;/h3&gt;

&lt;p&gt;Middleware runs in a different runtime where &lt;code&gt;cookies()&lt;/code&gt; is still synchronous. If you copy the same code into &lt;code&gt;middleware.ts&lt;/code&gt;, the &lt;code&gt;await&lt;/code&gt; will cause a compile error. In that case, keep the call synchronous and use &lt;code&gt;request.cookies&lt;/code&gt; directly. The fix for route handlers does not apply to middleware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Next.js made header helpers async
&lt;/h2&gt;

&lt;p&gt;Next.js 15 introduced an async API surface for many header helpers to make them compatible with edge runtimes. The underlying invariant is that any helper that may need to read from a streaming request must be async. When the framework changed, the type definitions lagged, so developers kept using the old synchronous pattern. To avoid the regression:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the ESLint rule &lt;code&gt;next/no-unawaited-cookies&lt;/code&gt; to your &lt;code&gt;.eslintrc.json&lt;/code&gt;. It will flag any &lt;code&gt;cookies()&lt;/code&gt; call that lacks &lt;code&gt;await&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Write a unit test for each route handler that asserts the response contains the expected &lt;code&gt;set-cookie&lt;/code&gt; header. A failing test will surface the missing &lt;code&gt;await&lt;/code&gt; before you ship.&lt;/li&gt;
&lt;li&gt;Pin your Next.js version to a minor release that you have verified, and read the migration guide for each major release. My guide on partial prerendering covers similar migration pitfalls: &lt;a href="https://www.iloveblogs.blog/guides/nextjs-15-partial-prerendering-complete-guide" rel="noopener noreferrer"&gt;"Next.js 15 Partial Prerendering: The Complete Production Guide"&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By treating the async nature of &lt;code&gt;cookies()&lt;/code&gt; as a first‑class citizen, you prevent the TypeError from resurfacing after future upgrades.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/fix/cookies-should-be-awaited-nextjs-fix" rel="noopener noreferrer"&gt;cookies() Should Be Awaited in Next.js 15/16: Exact Production Fix Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/nextjs-15-cookies-async-error-fix" rel="noopener noreferrer"&gt;Fix "cookies() should be awaited" Error in Next.js 15 App Router (Complete Migration Fix 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/nextjs-15-partial-prerendering-complete-guide" rel="noopener noreferrer"&gt;Next.js 15 Partial Prerendering: The Complete Production Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/window-is-not-defined-in-nextjs-react-app" rel="noopener noreferrer"&gt;Window is not defined in Next.js – 2026 Fix for React Apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/what-causes-nextjs-warning-extra-attributes-from-the-server-data-ne" rel="noopener noreferrer"&gt;NextJS Warning: Extra attributes from the server – Fix 2026&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/typeerror-cookies-crash-nextjs-route-handler" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>supabase</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>Supabase bucket RLS policy for table objects fix</title>
      <dc:creator>Mahdi BEN RHOUMA</dc:creator>
      <pubDate>Fri, 11 Sep 2026 17:24:53 +0000</pubDate>
      <link>https://dev.to/mahdi_benrhouma_fe1c6005/supabase-bucket-rls-policy-for-table-objects-fix-3f6</link>
      <guid>https://dev.to/mahdi_benrhouma_fe1c6005/supabase-bucket-rls-policy-for-table-objects-fix-3f6</guid>
      <description>&lt;p&gt;The exact error 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;new row violates row-level security policy for table "objects"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The root cause is usually one of three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;you have no &lt;code&gt;INSERT&lt;/code&gt; policy on &lt;code&gt;storage.objects&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;you are using &lt;code&gt;upsert: true&lt;/code&gt;, which needs more than just &lt;code&gt;INSERT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;you passed the bucket and file path in the wrong places&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is why this error shows up even when the user is already signed in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first thing to fix: bucket name vs file path
&lt;/h2&gt;

&lt;p&gt;This is the broken shape from the real Stack Overflow thread:&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;// Wrong&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;storage&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;/public/avatars&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;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;upsert&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supabase's own upload docs show the correct split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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;storage&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;avatars&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;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public/avatar1.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;cacheControl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3600&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;error&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;error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;from()&lt;/code&gt; takes the &lt;strong&gt;bucket&lt;/strong&gt;. &lt;code&gt;upload()&lt;/code&gt; takes the &lt;strong&gt;path inside that bucket&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The minimal policy for a plain upload
&lt;/h2&gt;

&lt;p&gt;Supabase documents that the only policy required for uploading objects is an &lt;code&gt;INSERT&lt;/code&gt; policy on &lt;code&gt;storage.objects&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A minimal bucket-scoped policy looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Allow authenticated uploads to avatars"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'avatars'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your uploads should go only into a specific folder, add a folder constraint too:&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;"Allow authenticated uploads to avatars/private"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'avatars'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foldername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'private'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The &lt;code&gt;upsert: true&lt;/code&gt; trap
&lt;/h2&gt;

&lt;p&gt;This is the part many articles miss. Supabase's Storage access-control docs explicitly say that overwriting files with &lt;code&gt;upsert&lt;/code&gt; needs &lt;strong&gt;&lt;code&gt;SELECT&lt;/code&gt; and &lt;code&gt;UPDATE&lt;/code&gt;&lt;/strong&gt; permissions in addition to the upload policy.&lt;/p&gt;

&lt;p&gt;So this code:&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="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;storage&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;avatars&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;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`private/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/avatar.png`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;needs more than just &lt;code&gt;for insert&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you do not actually need overwrite behavior, make the fix smaller:&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="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;storage&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;avatars&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;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`private/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/avatar.png`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;That alone resolves a surprising number of production bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you really do need overwrite behavior
&lt;/h2&gt;

&lt;p&gt;Keep the insert policy, then add read/update policies that match the same object scope.&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;"Allow authenticated reads on avatars/private"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'avatars'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foldername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'private'&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;"Allow authenticated updates on avatars/private"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'avatars'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foldername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'private'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'avatars'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foldername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'private'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A safe client upload example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`private/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/avatar.png`&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;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&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;avatars&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;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;cacheControl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3600&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&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 right place to start. Only add overwrite behavior once the basic insert path works.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the service key changes everything
&lt;/h2&gt;

&lt;p&gt;Supabase's Storage docs also note that service keys bypass Storage RLS entirely. That can be useful for trusted server-side jobs, but it is not a fix for a browser upload bug. If a client upload needs the service key to work, the policy is still wrong.&lt;/p&gt;

&lt;p&gt;The better debugging order is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;fix the bucket/path split&lt;/li&gt;
&lt;li&gt;test with &lt;code&gt;upsert: false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;add the exact &lt;code&gt;INSERT&lt;/code&gt; policy&lt;/li&gt;
&lt;li&gt;only then add &lt;code&gt;SELECT&lt;/code&gt; and &lt;code&gt;UPDATE&lt;/code&gt; if overwrite is required&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Scoping uploads to the signed-in user
&lt;/h2&gt;

&lt;p&gt;The folder constraint above used a literal (&lt;code&gt;'private'&lt;/code&gt;). To restrict every user to &lt;em&gt;their own&lt;/em&gt; folder, match the first path segment against the user id. This is the pattern Supabase's own docs recommend, and it survives multi-tenant scale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users upload to their own folder"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'avatars'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foldername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upload to a path whose first segment is the user id, and the check passes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&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="s2"&gt;/avatar.png`&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;storage&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;avatars&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;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;auth.uid()&lt;/code&gt; returns a UUID; the cast to &lt;code&gt;text&lt;/code&gt; matters because &lt;code&gt;storage.foldername(name)&lt;/code&gt; returns text segments. Wrapping it in &lt;code&gt;(select ...)&lt;/code&gt; lets Postgres evaluate it once per statement instead of once per row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which role and which &lt;code&gt;FOR&lt;/code&gt; clause
&lt;/h2&gt;

&lt;p&gt;Two details decide whether your policy ever fires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Role (&lt;code&gt;TO&lt;/code&gt; clause).&lt;/strong&gt; A signed-in user's request runs as &lt;code&gt;authenticated&lt;/code&gt;, not &lt;code&gt;anon&lt;/code&gt;. A policy scoped &lt;code&gt;to anon&lt;/code&gt; will never apply to a logged-in upload, and vice-versa. The &lt;code&gt;service_role&lt;/code&gt; key bypasses RLS entirely — useful for trusted server jobs, never a fix for a browser upload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operation (&lt;code&gt;FOR&lt;/code&gt; clause).&lt;/strong&gt; &lt;code&gt;for insert&lt;/code&gt; covers only the upload. If the same per-user predicate should also gate reads, updates, and deletes, use &lt;code&gt;for all&lt;/code&gt; with both &lt;code&gt;using&lt;/code&gt; (read/delete predicate) and &lt;code&gt;with check&lt;/code&gt; (write predicate):
&lt;/li&gt;
&lt;/ul&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;"Full access to own files"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foldername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foldername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;code&gt;owner&lt;/code&gt; vs &lt;code&gt;owner_id&lt;/code&gt;: don't gate on the wrong column
&lt;/h2&gt;

&lt;p&gt;A common dead end is writing the policy against an ownership column instead of the path. If you go that route, use the &lt;strong&gt;right&lt;/strong&gt; column: modern Supabase stores the uploader in &lt;strong&gt;&lt;code&gt;owner_id&lt;/code&gt;&lt;/strong&gt; (text), and the older &lt;strong&gt;&lt;code&gt;owner&lt;/code&gt;&lt;/strong&gt; (uuid) column is deprecated. A policy like &lt;code&gt;with check (owner_id = auth.uid())&lt;/code&gt; also needs a cast (&lt;code&gt;owner_id&lt;/code&gt; is text, &lt;code&gt;auth.uid()&lt;/code&gt; is uuid) — &lt;code&gt;with check (owner_id = (select auth.uid()::text))&lt;/code&gt;. In practice the folder-path pattern above is simpler and less error-prone than relying on the auto-set owner column, so prefer it unless you have a reason not to.&lt;/p&gt;

&lt;p&gt;For the surrounding security pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-file-storage-media-handling" rel="noopener noreferrer"&gt;Supabase Storage: Guide to File Uploads and Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/guides/nextjs-supabase-file-storage-media-handling" rel="noopener noreferrer"&gt;File Storage and Media Handling with Next.js and Supabase&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.iloveblogs.blog/post/supabase-rls-silent-failures-debug" rel="noopener noreferrer"&gt;Why Your Supabase RLS Policies Are Silently Failing (And How to Debug Them)&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: RLS, Secrets, and the Mistakes That Leak Data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/74302341/supabase-bucket-new-row-violates-row-level-security-policy-for-table-objects" rel="noopener noreferrer"&gt;Stack Overflow: Supabase bucket - new row violates row-level security policy for table "objects"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/storage/security/access-control" rel="noopener noreferrer"&gt;Supabase docs: Storage access control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/reference/javascript/v1/storage-from-upload" rel="noopener noreferrer"&gt;Supabase docs: Upload a file&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/supabase-storage-new-row-violates-rls-policy-objects" rel="noopener noreferrer"&gt;https://www.iloveblogs.blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>storage</category>
      <category>rls</category>
      <category>security</category>
    </item>
  </channel>
</rss>
