<?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: u11d</title>
    <description>The latest articles on DEV Community by u11d (u11d).</description>
    <link>https://dev.to/u11d</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%2Forganization%2Fprofile_image%2F11016%2F3aaba768-bbba-47eb-999a-9c314cf428f1.png</url>
      <title>DEV Community: u11d</title>
      <link>https://dev.to/u11d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/u11d"/>
    <language>en</language>
    <item>
      <title>Next.js 16 Cache Components: Where Should You Place 'use cache'?</title>
      <dc:creator>Paweł Sobolewski</dc:creator>
      <pubDate>Fri, 06 Nov 2026 16:20:40 +0000</pubDate>
      <link>https://dev.to/u11d/nextjs-16-cache-components-what-use-cache-actually-caches-a-hands-on-comparison-8hn</link>
      <guid>https://dev.to/u11d/nextjs-16-cache-components-what-use-cache-actually-caches-a-hands-on-comparison-8hn</guid>
      <description>&lt;p&gt;Next.js 16 ships &lt;a href="https://nextjs.org/docs/app/getting-started/caching" rel="noopener noreferrer"&gt;Cache Components&lt;/a&gt; as a first-class caching model: a single &lt;code&gt;'use cache'&lt;/code&gt; directive, paired with &lt;code&gt;cacheLife&lt;/code&gt;, that you can drop on a route, a component, or a plain data-fetching function. The pitch is appealing: pick your own granularity, mix static and dynamic content on the same page, and let Partial Prerendering figure out the rest.&lt;/p&gt;

&lt;p&gt;That pitch also raises a question the docs don't spend much time on: does it actually matter &lt;em&gt;where&lt;/em&gt; you put &lt;code&gt;'use cache'&lt;/code&gt;? If you wrap an entire component versus just the fetch call inside it, what changes, for the reader and for the server doing the work?&lt;/p&gt;

&lt;p&gt;To find out, I built a small dashboard with three crypto price cards (Bitcoin, Ethereum, Solana), all pulling the same kind of data from the same public API, each caching it a different way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bitcoin&lt;/strong&gt; caches the fetch &lt;em&gt;and&lt;/em&gt; the render together, at the component level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ethereum&lt;/strong&gt; caches only the fetch; the chart underneath is recomputed on every request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solana&lt;/strong&gt; caches nothing at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three run under the &lt;em&gt;exact same&lt;/em&gt; &lt;code&gt;cacheLife&lt;/code&gt; profile. The only variable is what each card chooses to cache, not how long it's cached for. That constraint turned out to matter: it's what forced some genuinely non-obvious behavior into the open, the kind you won't hit until you've deployed something and watched it run for a few minutes under real traffic.&lt;/p&gt;

&lt;p&gt;This is a walkthrough of what I found, with the actual numbers, not just the theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;

&lt;p&gt;All three cards share the exact same &lt;code&gt;cacheLife("demo")&lt;/code&gt; profile (&lt;code&gt;stale: 60&lt;/code&gt;, &lt;code&gt;revalidate: 10&lt;/code&gt;, &lt;code&gt;expire: 300&lt;/code&gt;). The only variable is what each one chooses to cache.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Card&lt;/th&gt;
&lt;th&gt;What's cached&lt;/th&gt;
&lt;th&gt;First-load time (warm cache)&lt;/th&gt;
&lt;th&gt;Re-runs on every request?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bitcoin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fetch + chart computation + JSX, via &lt;code&gt;'use cache'&lt;/code&gt; on the component&lt;/td&gt;
&lt;td&gt;~40ms&lt;/td&gt;
&lt;td&gt;No — only after the 10s &lt;code&gt;revalidate&lt;/code&gt; window passes and a request triggers the refresh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ethereum&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fetch only, via &lt;code&gt;'use cache'&lt;/code&gt; on the data function&lt;/td&gt;
&lt;td&gt;~445ms&lt;/td&gt;
&lt;td&gt;Chart: yes, every request. Fetch: no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Solana&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;td&gt;~1084ms&lt;/td&gt;
&lt;td&gt;Yes — fetch and chart, every request&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;cacheLife&lt;/code&gt;'s prerendering rules force a cache scope into a dynamic hole (excluded from the static shell) once any of these thresholds is crossed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;
&lt;code&gt;cacheLife&lt;/code&gt; field&lt;/th&gt;
&lt;th&gt;This demo's value&lt;/th&gt;
&lt;th&gt;Forces dynamic below/at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stale&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;60s&lt;/td&gt;
&lt;td&gt;&amp;lt; 30s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;revalidate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10s&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expire&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;300s (5m)&lt;/td&gt;
&lt;td&gt;&amp;lt; 5m&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The demo
&lt;/h2&gt;

&lt;p&gt;The app itself is deliberately small: one route, three cards, no database. Each card fetches live price data for its coin from Binance's public REST API (no key required), renders a price, a 24h change, and a small chart. Under the hood, each card also prints two things most demos hide: an execution counter and a timestamp, both of which only update when the card's data-fetching function &lt;em&gt;actually runs&lt;/em&gt;, not on every page load. That's the whole trick this article leans on: instead of guessing whether something was cached, you can watch the counter and just... check.&lt;/p&gt;

&lt;p&gt;Stack-wise it's nothing exotic: Next.js 16.3.2, App Router, TypeScript, Tailwind. The full source is on &lt;a href="https://github.com/u11d-com/blog_-nextjs-cached-components" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; if you want to run it yourself; every number quoted below came from that exact code, built and served with &lt;code&gt;next build &amp;amp;&amp;amp; next start&lt;/code&gt;. That last part matters more than it sounds: &lt;code&gt;'use cache'&lt;/code&gt; behaves differently under &lt;code&gt;next dev&lt;/code&gt;, so &lt;code&gt;next dev&lt;/code&gt; is not where you want to be verifying any of this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Component-level: cache everything, render included
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://nextjs.org/docs/app/api-reference/directives/use-cache" rel="noopener noreferrer"&gt;&lt;code&gt;'use cache'&lt;/code&gt; directive&lt;/a&gt; can sit at the top of a function &lt;em&gt;or&lt;/em&gt; a component. Put it on a component, and the entire thing (data fetching, computation, the JSX it returns) gets cached as one unit. That's the Bitcoin card:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cacheLife&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/cache&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;computeExpensiveChart&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/chart&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;COINS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetchBinancePrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recordExecution&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/prices&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;CryptoCard&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./crypto-card/CryptoCard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BitcoinCard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;demo&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;change24h&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="nf"&gt;fetchBinancePrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bitcoin&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;executionCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetchedAt&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;recordExecution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bitcoin&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sparklinePath&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="nf"&gt;computeExpensiveChart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;change24h&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;CryptoCard&lt;/span&gt;
      &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;change24h&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;change24h&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;sparklinePath&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;sparklinePath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;executionCount&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;executionCount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;fetchedAt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;fetchedAt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="c1"&gt;// ...tier, name, symbol&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things happen inside that cached scope: fetching the price from Binance, recording that a real execution happened, and computing the chart's SVG path. &lt;code&gt;computeExpensiveChart&lt;/code&gt; stands in for something you'd actually see in production: a heavier chart library, a technical-indicator calculation, a template render. So I gave it a real, measurable cost (a few hundred milliseconds) instead of pretending it's free.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://nextjs.org/docs/app/api-reference/functions/cacheLife" rel="noopener noreferrer"&gt;&lt;code&gt;cacheLife&lt;/code&gt;&lt;/a&gt; call is what gives this cache entry a lifetime. &lt;code&gt;"demo"&lt;/code&gt; is a custom profile defined in &lt;code&gt;next.config.ts&lt;/code&gt; rather than one of the built-ins (&lt;code&gt;"minutes"&lt;/code&gt;, &lt;code&gt;"hours"&lt;/code&gt;, and so on); more on why, and on the exact numbers, in a couple of sections. For now, the point is simpler: on a cache hit, &lt;em&gt;none&lt;/em&gt; of that work happens. Not the fetch, not the chart computation. The component just returns whatever was computed and stored the last time this cache entry was filled.&lt;/p&gt;

&lt;p&gt;What actually gets stored isn't HTML, and it isn't a plain object of &lt;code&gt;{ price, change24h, ... }&lt;/code&gt; either. It's a serialized &lt;a href="https://nextjs.org/docs/app/getting-started/server-and-client-components#on-the-server" rel="noopener noreferrer"&gt;&lt;strong&gt;RSC payload&lt;/strong&gt;&lt;/a&gt;: the same wire format React Server Components normally stream to the client, just persisted instead of sent once and discarded. That's also why &lt;code&gt;'use cache'&lt;/code&gt; is allowed to return JSX at all: &lt;a href="https://nextjs.org/docs/app/api-reference/directives/use-cache#serialization" rel="noopener noreferrer"&gt;return values follow client-serialization rules&lt;/a&gt;, which are more permissive than the rules for a cached function's &lt;em&gt;arguments&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;From that one stored payload, Next.js can produce whatever a given request needs: HTML for the prerendered static shell, the same payload replayed as-is for another server hit, or shipped straight to the browser so a client-side navigation can reuse it without a round trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetch-level: cache the data, not the render
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;'use cache'&lt;/code&gt; doesn't have to sit on a component. Put it on a plain async function instead, and only &lt;em&gt;that function's&lt;/em&gt; return value gets cached; nothing about the component that calls it changes:&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;// lib/prices.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;getEthereumSnapshot&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PriceSnapshot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;demo&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;change24h&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="nf"&gt;fetchBinancePrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ethereum&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;executionCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetchedAt&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;recordExecution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ethereum&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="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;change24h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;executionCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetchedAt&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// EthereumCard.tsx&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;EthereumCard&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;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;change24h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;executionCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetchedAt&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="nf"&gt;getEthereumSnapshot&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;sparklinePath&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="nf"&gt;computeExpensiveChart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;change24h&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;CryptoCard&lt;/span&gt;
      &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;change24h&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;change24h&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;sparklinePath&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;sparklinePath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;executionCount&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;executionCount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;fetchedAt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;fetchedAt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="c1"&gt;// ...tier, name, symbol&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;EthereumCard&lt;/code&gt; itself has no &lt;code&gt;'use cache'&lt;/code&gt; at all. It calls a cached function to get pricing data, then unconditionally recomputes the chart from that data on every single request: same &lt;code&gt;computeExpensiveChart&lt;/code&gt;, same artificial cost, as Bitcoin's card. Same &lt;code&gt;cacheLife("demo")&lt;/code&gt;, too: the fetch is cached on the &lt;em&gt;exact&lt;/em&gt; same schedule as Bitcoin's whole component. The only difference between these two cards is what's inside the cached scope versus what's outside it.&lt;/p&gt;

&lt;p&gt;Here's the part that isn't obvious going in: that difference alone is &lt;em&gt;not&lt;/em&gt; what makes Ethereum's chart recompute every time. If &lt;code&gt;EthereumCard&lt;/code&gt; had no reason to be treated as dynamic, Next would happily bake the entire thing (cached fetch, chart computation and all) into the same kind of static shell Bitcoin gets, on the theory that nothing about it depends on the incoming request. Cache placement controls what gets &lt;em&gt;cached&lt;/em&gt;; it doesn't, by itself, control how often a component gets &lt;em&gt;re-evaluated&lt;/em&gt;. Those are two separate questions, and it's easy to assume the first answers the second.&lt;/p&gt;

&lt;p&gt;What actually forces &lt;code&gt;EthereumCard&lt;/code&gt; to run on every request is &lt;code&gt;computeExpensiveChart&lt;/code&gt; itself: an &lt;code&gt;await&lt;/code&gt; with no &lt;code&gt;'use cache'&lt;/code&gt; around it, sitting in a component that also has no &lt;code&gt;'use cache'&lt;/code&gt; around it. Next can't treat that as predictable output it's safe to freeze at build time, so the component becomes a genuine dynamic hole: resolved fresh, on the server, for every single request. The cached fetch inside it is just along for the ride: fast when it's warm, but that speed has no bearing on whether the component around it reruns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero cache: the baseline
&lt;/h2&gt;

&lt;p&gt;Solana's card doesn't try to be clever:&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="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;SolanaCard&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;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;change24h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;executionCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetchedAt&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="nf"&gt;getSolanaSnapshot&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;sparklinePath&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="nf"&gt;computeExpensiveChart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;change24h&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;CryptoCard&lt;/span&gt;
      &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;change24h&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;change24h&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;sparklinePath&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;sparklinePath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;executionCount&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;executionCount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;fetchedAt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;fetchedAt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;live&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;'use cache'&lt;/code&gt; anywhere in the chain: not on the component, not on &lt;code&gt;getSolanaSnapshot&lt;/code&gt;. The fetch alone is enough to make this a dynamic hole, for the same reason as Ethereum's chart: an uncached async operation with nothing caching it. Every request pays for both the network round trip to Binance &lt;em&gt;and&lt;/em&gt; the chart computation, in full, every time. That's the baseline the other two cards get compared against.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to measure this for real
&lt;/h2&gt;

&lt;p&gt;Explaining what &lt;em&gt;should&lt;/em&gt; be cached is one thing. Watching it happen is more convincing, so each card carries two independent signals, and neither is a number the code invents.&lt;/p&gt;

&lt;p&gt;The first is server-side: an execution counter plus a timestamp, both updated only inside the function that does the real work: the fetch for Ethereum and Solana, the whole component for Bitcoin. A cache hit means that code doesn't run, which means the counter doesn't move. No inference required, just read the number.&lt;/p&gt;

&lt;p&gt;The second is client-side, and answers a different question: not "was this cached," but "how long did the reader actually wait for it." Each card mounts a small client component the instant its content reaches 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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;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="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AppearedAtBadge&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLSpanElement&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ref&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`appeared after &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;&lt;span class="s2"&gt;ms`&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;…&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&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;&lt;code&gt;performance.now()&lt;/code&gt; inside a &lt;code&gt;useEffect&lt;/code&gt; fires the moment this specific node mounts. For a card behind its own &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary, that's the moment its streamed chunk actually arrived, not when the initial HTML shell did. (It writes to the DOM directly instead of calling &lt;code&gt;setState&lt;/code&gt;, on purpose: a &lt;code&gt;setState&lt;/code&gt; here would just trigger an extra render for a value nothing else depends on.) Since Bitcoin, Ethereum, and Solana each stream independently, this produces three different numbers on the same page load. On a warm run, measured against this exact code:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Card&lt;/th&gt;
&lt;th&gt;Appeared after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bitcoin&lt;/td&gt;
&lt;td&gt;~40ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ethereum&lt;/td&gt;
&lt;td&gt;~445ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solana&lt;/td&gt;
&lt;td&gt;~1084ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the whole argument in one table. Bitcoin's number is just network latency: the cache hit means there's nothing else to wait for. Ethereum pays for the chart, once, and skips the fetch. Solana pays for both, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things about &lt;code&gt;stale&lt;/code&gt;, &lt;code&gt;revalidate&lt;/code&gt;, and &lt;code&gt;expire&lt;/code&gt; worth knowing before you hit them in production
&lt;/h2&gt;

&lt;p&gt;Bitcoin and Ethereum share one &lt;code&gt;cacheLife&lt;/code&gt; profile (see Quick Reference above), precisely so any difference between them comes from what's cached, not how long. Getting that shared profile right surfaced a few behaviors that aren't obvious from a first read of the docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A short lifetime can silently force a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary you didn't ask for.&lt;/strong&gt; &lt;a href="https://nextjs.org/docs/app/api-reference/functions/cacheLife#prerendering-behavior" rel="noopener noreferrer"&gt;&lt;code&gt;cacheLife&lt;/code&gt;'s prerendering rules&lt;/a&gt; exclude a cache scope from the static shell, turning it into a dynamic hole, under three conditions: &lt;code&gt;revalidate: 0&lt;/code&gt; or &lt;code&gt;expire&lt;/code&gt; under 5 minutes, or &lt;code&gt;stale&lt;/code&gt; under 30 seconds. Hit any of them on a component you expected to be static, like Bitcoin, and &lt;code&gt;next build&lt;/code&gt; fails outright, pointing at that component with an error about uncached data outside a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;revalidate&lt;/code&gt; never blocks a request, and it never fires without one.&lt;/strong&gt; The &lt;a href="https://nextjs.org/docs/app/guides/how-revalidation-works#the-revalidation-model" rel="noopener noreferrer"&gt;background regeneration model&lt;/a&gt; is classic stale-while-revalidate: once a cache entry's age passes &lt;code&gt;revalidate&lt;/code&gt;, the server keeps serving the existing value immediately while refreshing it in the background; nobody waits. If nothing asks, nothing refreshes, no matter how much wall-clock time passes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A fresh build and a fresh process don't share state, even though they share a build.&lt;/strong&gt; This one's specific to caching at the component level, and it's the kind of thing you only notice if you're watching closely. &lt;code&gt;next build&lt;/code&gt; genuinely executes Bitcoin's cached component once, to produce the static shell, including our counter, which ticks up to 1 inside that short-lived build process. When &lt;code&gt;next start&lt;/code&gt; boots, it's a new process with its own fresh copy of that counter, starting back at 0. So the &lt;em&gt;first&lt;/em&gt; real execution inside the running server also lands on "1", the same number the build already showed, just with a new price behind it. Only the second live execution correctly shows "2". If you're using an in-memory counter as a "did this actually run" signal, this is the one moment it lies to you; a timestamp doesn't have the same problem, since it never resets.&lt;/p&gt;

&lt;p&gt;None of these are bugs. They're the kind of detail that only shows up once you deploy something and watch it for a few minutes instead of trusting the mental model you built from the docs alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this compares to classic ISR
&lt;/h2&gt;

&lt;p&gt;If you've used &lt;a href="https://nextjs.org/docs/app/guides/incremental-static-regeneration" rel="noopener noreferrer"&gt;Incremental Static Regeneration&lt;/a&gt; before (&lt;code&gt;getStaticProps&lt;/code&gt; with a &lt;code&gt;revalidate&lt;/code&gt; option, or &lt;code&gt;export const revalidate = 60&lt;/code&gt; in the App Router), the stale-while-revalidate mechanics in this article will feel familiar, because it's the same underlying model. What's different is the unit it applies to. Classic ISR revalidates a &lt;em&gt;page&lt;/em&gt;: one lifetime, one cache entry, for everything that page renders. Getting Bitcoin-style and Ethereum-style behavior on the same route used to mean either splitting them into separate routes or reaching for something like &lt;code&gt;unstable_cache&lt;/code&gt; to hand-roll a second cache boundary inside the page.&lt;/p&gt;

&lt;p&gt;Cache Components collapses that gap by making the &lt;em&gt;component or function&lt;/em&gt; the unit instead of the route. Bitcoin and Ethereum sit on the same page, share the same &lt;code&gt;cacheLife&lt;/code&gt;, and still behave completely differently, because the boundary moved, not because the underlying revalidation logic changed. If you're already comfortable with ISR's mental model, most of what's new here is granularity, not a new set of rules to learn from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking a strategy
&lt;/h2&gt;

&lt;p&gt;Three cards, three defensible choices, and the right one depends on what's actually expensive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache the component&lt;/strong&gt; (Bitcoin) when the render itself costs real time (a chart library, a heavy transform, anything downstream of the fetch) and that render doesn't need to be fresher than the data feeding it. Bundling both together means a cache hit skips all of it, not just the network call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache just the fetch&lt;/strong&gt; (Ethereum) when the data can tolerate being a little stale but what you build from it can't: per-request personalization, something reading a cookie, anything that has to reflect &lt;em&gt;this&lt;/em&gt; request even though the numbers behind it don't change every second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache nothing&lt;/strong&gt; (Solana) when the data genuinely has to be live, and go in knowing you're paying for the fetch and everything downstream of it, every single time, deliberately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The instinct worth keeping is to verify, not assume. &lt;code&gt;'use cache'&lt;/code&gt; placement determines &lt;em&gt;what's&lt;/em&gt; frozen; it doesn't determine &lt;em&gt;when&lt;/em&gt; your code runs. That's still governed by whether anything nearby gives Next a reason to treat the surrounding component as dynamic. As with most caching decisions, there's no universal answer here; it depends entirely on the use case in front of you. What does generalize is the method: a counter and a timestamp that only move on real work turn that decision into something you can check, not something you have to guess at.&lt;/p&gt;

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

&lt;p&gt;Cache Components don't change the mental model of stale-while-revalidate caching — they change its unit, from the whole route down to a single component or function. That extra granularity is worth having, but it comes with a sharp edge: placement controls what's frozen, not how often the surrounding code reruns, and the two are easy to conflate until you've measured them separately, as the Bitcoin/Ethereum/Solana comparison above does. If you're building on Next.js and want this kind of caching behavior verified against real numbers before it ships, that's the same kind of work u11d's &lt;a href="https://u11d.com/services/web-development/" rel="noopener noreferrer"&gt;web development&lt;/a&gt; team does on React/Next.js projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs/app/getting-started/caching" rel="noopener noreferrer"&gt;Cache Components&lt;/a&gt; — Next.js docs&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/api-reference/directives/use-cache" rel="noopener noreferrer"&gt;&lt;code&gt;'use cache'&lt;/code&gt; directive reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/api-reference/functions/cacheLife" rel="noopener noreferrer"&gt;&lt;code&gt;cacheLife&lt;/code&gt; function reference and prerendering rules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/guides/how-revalidation-works" rel="noopener noreferrer"&gt;How revalidation works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/guides/incremental-static-regeneration" rel="noopener noreferrer"&gt;Incremental Static Regeneration guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/u11d-com/blog_-nextjs-cached-components" rel="noopener noreferrer"&gt;Demo source code on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://u11d.com/blog/implementing-postgres-row-level-security-drizzle-nextjs/" rel="noopener noreferrer"&gt;Implementing Postgres Row-Level Security in Next.js: The Drizzle Multi-Tenant Pattern&lt;/a&gt; — another Next.js production deep-dive on this blog&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://u11d.com/blog/multi-language-shop-nextjs/" rel="noopener noreferrer"&gt;Building a Multi-Language Shop with Next.js App Router&lt;/a&gt; — another Next.js App Router deep-dive on this blog&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;What does Next.js's &lt;code&gt;'use cache'&lt;/code&gt; directive actually cache?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;'use cache'&lt;/code&gt; stores whatever the function or component it wraps returns, serialized as an RSC (React Server Components) payload — not raw HTML and not a plain JavaScript object. On a component, that means the data fetch, any computation inside it, and the JSX it returns are all cached together as one unit; on a plain function, only that function's return value is cached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it matter whether I put &lt;code&gt;'use cache'&lt;/code&gt; on a component or on the fetch function?&lt;/strong&gt;&lt;br&gt;
Yes. Caching a whole component in Next.js caches the fetch, any computation, and the rendered output together, so a cache hit skips all of it. Caching only the data-fetching function leaves everything else in the component — like a chart calculation — to run on every request, even though the data behind it came from cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does my Next.js component still re-run on every request even though I cached the data fetch with &lt;code&gt;'use cache'&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
Because an uncached &lt;code&gt;await&lt;/code&gt; anywhere else in that component — a chart computation, a transform, another fetch — forces Next.js to treat the whole component as a dynamic hole that gets re-evaluated per request. Caching a function only freezes that function's own output; it doesn't stop Next.js from re-running everything around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did &lt;code&gt;next build&lt;/code&gt; fail with an error about uncached data outside a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary?&lt;/strong&gt;&lt;br&gt;
Next.js's &lt;code&gt;cacheLife&lt;/code&gt; prerendering rules exclude a cache scope from the static shell — turning it into a required dynamic hole — whenever &lt;code&gt;revalidate&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;expire&lt;/code&gt; is under 5 minutes, or &lt;code&gt;stale&lt;/code&gt; is under 30 seconds. Hitting one of these thresholds on a component you expected to prerender statically makes &lt;code&gt;next build&lt;/code&gt; fail unless that component is wrapped in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;cacheLife&lt;/code&gt;'s &lt;code&gt;revalidate&lt;/code&gt; option refresh the cache automatically in the background?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;revalidate&lt;/code&gt; only marks a Next.js cache entry as stale once its age passes that threshold — nothing refreshes it until the next real request arrives, at which point Next.js serves the existing stale value immediately and refreshes it in the background. If no request comes in, the cache never refreshes on its own, no matter how much time passes.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>nextjs</category>
      <category>performance</category>
    </item>
    <item>
      <title>A Complete Guide to AWS CloudFront Origin Request Policies</title>
      <dc:creator>Maciej Łopalewski</dc:creator>
      <pubDate>Wed, 04 Nov 2026 14:26:33 +0000</pubDate>
      <link>https://dev.to/u11d/a-complete-guide-to-aws-cloudfront-origin-request-policies-dg9</link>
      <guid>https://dev.to/u11d/a-complete-guide-to-aws-cloudfront-origin-request-policies-dg9</guid>
      <description>&lt;p&gt;If your origin needs data the cache key shouldn't include, an origin request policy is the right tool. It controls what CloudFront forwards to your backend on a cache miss, separately from what makes a request "unique" for caching. Use a managed policy unless you have a specific reason not to — AWS ships eight of them, and most distributions can pick one off the shelf.&lt;/p&gt;

&lt;p&gt;The short version, before the rest of the article:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Managed policy&lt;/th&gt;
&lt;th&gt;Use when your origin is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AllViewer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A backend that needs every viewer header, cookie, and query string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AllViewerAndCloudFrontHeaders-2022-06&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The same, plus geolocation, device-type, or TLS metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AllViewerExceptHostHeader&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;API Gateway or a Lambda function URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CORS-S3Origin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An S3 bucket serving cross-origin assets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CORS-CustomOrigin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A non-S3 web server handling CORS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HostHeaderOnly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A backend that needs only the original &lt;code&gt;Host&lt;/code&gt; header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UserAgentRefererHeaders&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Doing analytics, hotlink protection, or basic device detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Elemental-MediaTailor-PersonalizedManifests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An AWS Elemental MediaTailor endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rest of this article covers what these policies actually do, how they interact with the cache key (where most bugs live), and when to skip the managed list and write a custom one.&lt;/p&gt;

&lt;p&gt;This is a follow-up to an earlier post that covered cache, origin request, and response policies as a trio — read that first if you want the broader context.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an origin request policy actually does
&lt;/h2&gt;

&lt;p&gt;CloudFront builds the request it sends to your origin from two sources. The first is everything in the cache key, controlled by your cache policy. The second is whatever your origin request policy adds on top. Without an origin request policy attached, the only headers your origin sees are the ones already in the cache key, plus a small set CloudFront automatically adds (&lt;code&gt;Host&lt;/code&gt;, &lt;code&gt;User-Agent&lt;/code&gt;, &lt;code&gt;X-Amz-Cf-Id&lt;/code&gt;, and a few others).&lt;/p&gt;

&lt;p&gt;That default is intentional. If a value isn't in the cache key, CloudFront treats requests with different versions of that value as the same cached object. Forwarding the value to the origin would make the response depend on something the cache doesn't see — and you'd get inconsistent results because the first response wins and gets served to everyone else. Origin request policies let you forward extra data to the origin while keeping the cache blind to it. That's only safe when the origin uses that data for things that don't change the response body: logging, analytics, downstream auth, or branching that returns the same content regardless.&lt;/p&gt;

&lt;p&gt;I've seen teams reach for an origin request policy when what they actually needed was to add a value to the cache key. If your application reads &lt;code&gt;User-Agent&lt;/code&gt; to decide between a mobile and a desktop response, forwarding it via an origin request policy isn't enough — the cached response is shared across both. The fix is to put &lt;code&gt;User-Agent&lt;/code&gt; in the cache key, or to use a CloudFront-supplied header like &lt;code&gt;CloudFront-Is-Mobile-Viewer&lt;/code&gt;, which is designed exactly for that case.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it interacts with the cache policy
&lt;/h3&gt;

&lt;p&gt;Anything in the cache key is automatically forwarded to the origin. The origin request policy can only &lt;em&gt;add&lt;/em&gt; to that — it can't subtract. If your cache policy includes &lt;code&gt;Authorization&lt;/code&gt; in the cache key, the origin will see it on a cache miss whether or not your origin request policy mentions it. This often surprises people debugging "why is this header reaching my origin when I didn't list it?"&lt;/p&gt;

&lt;p&gt;There's also a hard requirement worth knowing up front: a cache behavior cannot have an origin request policy attached without also having a cache policy. AWS rejects the configuration. So in practice, every distribution that uses origin request policies pairs each one with a cache policy on the same behavior.&lt;/p&gt;

&lt;p&gt;The working rule: put values in the cache key when the response actually varies on them. Put values in the origin request policy when the origin needs them but the response is the same regardless.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's inside an origin request policy
&lt;/h2&gt;

&lt;p&gt;Three knobs: headers, cookies, and query strings. Each one accepts a behavior, plus an optional list when the behavior calls for one.&lt;/p&gt;

&lt;p&gt;For headers, the available behaviors are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;none&lt;/code&gt; — no viewer headers forwarded (cache-key headers still go through, plus the small CloudFront-added set)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;whitelist&lt;/code&gt; — only the listed headers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allViewer&lt;/code&gt; — every viewer header&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allViewerAndWhitelistCloudFront&lt;/code&gt; — every viewer header plus specific CloudFront headers (geolocation, device-type, TLS info)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allExcept&lt;/code&gt; — every viewer header except a listed set&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cookies and query strings have a similar shape but a smaller set: &lt;code&gt;none&lt;/code&gt;, &lt;code&gt;all&lt;/code&gt;, &lt;code&gt;whitelist&lt;/code&gt;, or &lt;code&gt;allExcept&lt;/code&gt;. There's no &lt;code&gt;allViewerAndWhitelistCloudFront&lt;/code&gt; equivalent because CloudFront doesn't add its own cookies or query strings.&lt;/p&gt;

&lt;p&gt;A subtlety worth knowing before you read the managed policies below: when a policy uses &lt;code&gt;allExcept&lt;/code&gt; to remove the viewer's &lt;code&gt;Host&lt;/code&gt; header, CloudFront automatically replaces it with a new &lt;code&gt;Host&lt;/code&gt; header containing the origin's domain name. That's exactly what makes &lt;code&gt;AllViewerExceptHostHeader&lt;/code&gt; work for API Gateway — the gateway sees its own domain in the &lt;code&gt;Host&lt;/code&gt; header and routes correctly, instead of seeing the CloudFront distribution's domain and rejecting the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eight managed policies
&lt;/h2&gt;

&lt;p&gt;Each managed policy has a stable ID that you reference from CloudFormation, the AWS CLI, the SDKs, or your IaC tool of choice. The IDs don't change, so it's safe to hardcode them.&lt;/p&gt;

&lt;h3&gt;
  
  
  AllViewer
&lt;/h3&gt;

&lt;p&gt;Forwards every header, cookie, and query string from the viewer request to the origin.&lt;/p&gt;

&lt;p&gt;ID: &lt;code&gt;216adef6-5c7f-47e4-b989-5492eafa07d3&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Use this when your backend genuinely needs the full request — typically for legacy applications, complex authenticated apps, or anything where you can't easily enumerate which headers and cookies matter. The trade-off is that it forwards the viewer's &lt;code&gt;Host&lt;/code&gt; header too, which means it doesn't work with API Gateway or Lambda function URLs (use &lt;code&gt;AllViewerExceptHostHeader&lt;/code&gt; for those). It also forwards anything sensitive that the client happens to send, so think about whether your origin should see, for instance, a long-lived authentication cookie that some unrelated part of your app sets.&lt;/p&gt;

&lt;h3&gt;
  
  
  AllViewerAndCloudFrontHeaders-2022-06
&lt;/h3&gt;

&lt;p&gt;Same as &lt;code&gt;AllViewer&lt;/code&gt; plus a fixed set of CloudFront-added headers: &lt;code&gt;CloudFront-Forwarded-Proto&lt;/code&gt;, the device-type series (&lt;code&gt;-Is-Android-Viewer&lt;/code&gt;, &lt;code&gt;-Is-Mobile-Viewer&lt;/code&gt;, &lt;code&gt;-Is-Tablet-Viewer&lt;/code&gt;, etc.), the geolocation series (&lt;code&gt;-Viewer-Country&lt;/code&gt;, &lt;code&gt;-Viewer-City&lt;/code&gt;, &lt;code&gt;-Viewer-Latitude&lt;/code&gt;, &lt;code&gt;-Viewer-Longitude&lt;/code&gt;, &lt;code&gt;-Viewer-Time-Zone&lt;/code&gt;, &lt;code&gt;-Viewer-Postal-Code&lt;/code&gt;, &lt;code&gt;-Viewer-ASN&lt;/code&gt;, and a few more), &lt;code&gt;CloudFront-Viewer-Http-Version&lt;/code&gt;, and &lt;code&gt;CloudFront-Viewer-TLS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;ID: &lt;code&gt;33f36d7e-f396-46d9-90e0-52428a34d9dc&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-2022-06&lt;/code&gt; suffix is significant. The set of headers this policy forwards is frozen as of June 2022 — any CloudFront viewer header added after that date isn't included. AWS does this so existing distributions don't change behavior when new headers ship. If you want a newer CloudFront header in your origin requests, write a custom policy.&lt;/p&gt;

&lt;p&gt;Use this when your origin uses CloudFront's enriched headers for personalization, analytics, or routing. Geolocation-based content selection is the common case: your backend reads &lt;code&gt;CloudFront-Viewer-Country&lt;/code&gt; and serves a localized response, without doing GeoIP lookups itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  AllViewerExceptHostHeader
&lt;/h3&gt;

&lt;p&gt;Forwards everything except the &lt;code&gt;Host&lt;/code&gt; header. CloudFront then injects the origin's domain name as the new &lt;code&gt;Host&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;ID: &lt;code&gt;b689b0a8-53d0-40ab-baf2-68738e2966ac&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is the policy you almost certainly want when your origin is API Gateway, a Lambda function URL, or any AWS-managed endpoint that does virtual hosting based on the &lt;code&gt;Host&lt;/code&gt; header. These services check &lt;code&gt;Host&lt;/code&gt; against their own domain. If CloudFront forwards the viewer's &lt;code&gt;Host&lt;/code&gt; (which is your CloudFront distribution's domain or your custom domain), they don't recognize it and respond with a &lt;code&gt;403&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It also includes the same set of CloudFront viewer headers as &lt;code&gt;AllViewerAndCloudFrontHeaders-2022-06&lt;/code&gt;, which means you get geolocation and device-type metadata for free. For most API Gateway-backed CloudFront distributions, this is a one-line answer to a problem that used to require fiddling with cache behaviors and Lambda@Edge.&lt;/p&gt;

&lt;h3&gt;
  
  
  CORS-CustomOrigin
&lt;/h3&gt;

&lt;p&gt;Forwards only the &lt;code&gt;Origin&lt;/code&gt; header. No cookies, no query strings.&lt;/p&gt;

&lt;p&gt;ID: &lt;code&gt;59781a5b-3903-41f3-afcb-af62929ccde1&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Use this when your origin is anything other than S3 — an ALB, an EC2 instance, a containerized service, or a third-party HTTP server — and it serves cross-origin requests. The &lt;code&gt;Origin&lt;/code&gt; header is what the server inspects to decide whether to return CORS-allowing response headers. Anything more just bloats the origin request without any CORS benefit, and risks forwarding data the origin doesn't need to see.&lt;/p&gt;

&lt;p&gt;This policy pairs naturally with the &lt;code&gt;CORS-with-preflight-and-SecurityHeadersPolicy&lt;/code&gt; response headers policy when your backend doesn't reliably set CORS headers itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  CORS-S3Origin
&lt;/h3&gt;

&lt;p&gt;Forwards &lt;code&gt;Origin&lt;/code&gt;, &lt;code&gt;Access-Control-Request-Headers&lt;/code&gt;, and &lt;code&gt;Access-Control-Request-Method&lt;/code&gt;. No cookies, no query strings.&lt;/p&gt;

&lt;p&gt;ID: &lt;code&gt;88a5eaf4-2fd4-4709-b370-b4c650ea3fcf&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;S3 is fussier about CORS than a custom server. To return the right CORS response headers, S3 needs to see all three of these request headers — not just &lt;code&gt;Origin&lt;/code&gt;. The standard CORS forwarding policy (&lt;code&gt;CORS-CustomOrigin&lt;/code&gt;) doesn't work because it omits the other two. If you've ever spent time debugging "CORS works locally but not through CloudFront" with an S3 origin, this is almost certainly the missing piece.&lt;/p&gt;

&lt;p&gt;Note that this policy is for S3-as-origin scenarios where you've also configured a CORS rule on the bucket itself. Without the bucket-side CORS configuration, S3 won't return the &lt;code&gt;Access-Control-Allow-*&lt;/code&gt; headers regardless of what gets forwarded.&lt;/p&gt;

&lt;h3&gt;
  
  
  HostHeaderOnly
&lt;/h3&gt;

&lt;p&gt;Forwards just the &lt;code&gt;Host&lt;/code&gt; header. Nothing else.&lt;/p&gt;

&lt;p&gt;ID: &lt;code&gt;bf0718e1-ba1e-49d1-88b1-f726733018ae&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A narrow policy with a specific use case: multi-tenant origins that route based on the original viewer &lt;code&gt;Host&lt;/code&gt;. If your backend serves multiple custom domains and uses the &lt;code&gt;Host&lt;/code&gt; header to figure out which tenant a request belongs to, you need that header to come through unchanged. Most other managed policies either rewrite &lt;code&gt;Host&lt;/code&gt; or don't forward enough information to make it useful.&lt;/p&gt;

&lt;p&gt;If you're using this with anything other than a multi-tenant routing scenario, double-check whether you actually want it. For API Gateway origins, &lt;code&gt;HostHeaderOnly&lt;/code&gt; will &lt;em&gt;not&lt;/em&gt; work — API Gateway needs the rewritten &lt;code&gt;Host&lt;/code&gt;, which is what &lt;code&gt;AllViewerExceptHostHeader&lt;/code&gt; gives you instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  UserAgentRefererHeaders
&lt;/h3&gt;

&lt;p&gt;Forwards only &lt;code&gt;User-Agent&lt;/code&gt; and &lt;code&gt;Referer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;ID: &lt;code&gt;acba4595-bd28-49b8-b9fe-13317c0390fa&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is the policy for cases where the origin needs to know "what kind of client is this?" or "where did the request come from?" without seeing the rest of the headers, cookies, or query strings. Two common uses: hotlink protection (block requests where &lt;code&gt;Referer&lt;/code&gt; doesn't match an allowed list) and analytics that bucket traffic by browser or operating system.&lt;/p&gt;

&lt;p&gt;It pairs well with a tight cache policy — typically &lt;code&gt;CachingOptimized&lt;/code&gt; — because neither header is in the cache key. The cache stays efficient and the origin still gets enough data to do its filtering or counting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Elemental-MediaTailor-PersonalizedManifests
&lt;/h3&gt;

&lt;p&gt;Forwards &lt;code&gt;Origin&lt;/code&gt;, &lt;code&gt;Access-Control-Request-Headers&lt;/code&gt;, &lt;code&gt;Access-Control-Request-Method&lt;/code&gt;, &lt;code&gt;User-Agent&lt;/code&gt;, &lt;code&gt;X-Forwarded-For&lt;/code&gt;, and all query strings.&lt;/p&gt;

&lt;p&gt;ID: &lt;code&gt;775133bc-15f2-49f9-abea-afb2e0bf67d2&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A specialized policy for AWS Elemental MediaTailor, which serves personalized HLS/DASH manifests for ad insertion in video streaming. MediaTailor needs the CORS triplet for browser playback, &lt;code&gt;User-Agent&lt;/code&gt; and &lt;code&gt;X-Forwarded-For&lt;/code&gt; for ad-decisioning logic, and the full query string because session and personalization tokens are encoded there. If you're not using MediaTailor, you don't need this policy — but if you are, it's the difference between manifests that work and manifests that don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison: what each managed policy forwards
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Headers&lt;/th&gt;
&lt;th&gt;Cookies&lt;/th&gt;
&lt;th&gt;Query strings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AllViewer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All viewer&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AllViewerAndCloudFrontHeaders-2022-06&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All viewer + frozen CF set&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AllViewerExceptHostHeader&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All viewer except &lt;code&gt;Host&lt;/code&gt; + CF set&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CORS-CustomOrigin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Origin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CORS-S3Origin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Origin&lt;/code&gt;, &lt;code&gt;Access-Control-Request-Headers&lt;/code&gt;, &lt;code&gt;Access-Control-Request-Method&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HostHeaderOnly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Host&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UserAgentRefererHeaders&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;User-Agent&lt;/code&gt;, &lt;code&gt;Referer&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Elemental-MediaTailor-PersonalizedManifests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CORS triplet + &lt;code&gt;User-Agent&lt;/code&gt; + &lt;code&gt;X-Forwarded-For&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When to write a custom origin request policy
&lt;/h2&gt;

&lt;p&gt;The managed policies cover most cases. A custom policy is needed when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your origin needs a specific application header (like &lt;code&gt;X-Tenant-ID&lt;/code&gt; or &lt;code&gt;X-Request-Trace&lt;/code&gt;) that isn't in any managed policy&lt;/li&gt;
&lt;li&gt;You want CloudFront viewer headers added after June 2022 — the managed &lt;code&gt;AllViewerAndCloudFrontHeaders-2022-06&lt;/code&gt; is frozen at that date&lt;/li&gt;
&lt;li&gt;A specific cookie or query string needs to pass through while others stay blocked (no managed policy uses the &lt;code&gt;whitelist&lt;/code&gt; behavior for cookies or query strings)&lt;/li&gt;
&lt;li&gt;The requirements get oddly mixed — e.g., "everything except &lt;code&gt;Authorization&lt;/code&gt;, plus a few CloudFront geolocation headers"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a Terraform example for a custom policy that forwards a tenant header, geolocation, and a session cookie, while excluding everything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudfront_origin_request_policy"&lt;/span&gt; &lt;span class="s2"&gt;"tenant_api"&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="s2"&gt;"tenant-api-policy"&lt;/span&gt;
  &lt;span class="nx"&gt;comment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Forwards tenant routing data and CF geolocation"&lt;/span&gt;

  &lt;span class="nx"&gt;headers_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;header_behavior&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"whitelist"&lt;/span&gt;
    &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"X-Tenant-ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"CloudFront-Viewer-Country"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"CloudFront-Viewer-City"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;cookies_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cookie_behavior&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"whitelist"&lt;/span&gt;
    &lt;span class="nx"&gt;cookies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"session_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;span class="nx"&gt;query_strings_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;query_string_behavior&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"none"&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;A subtle gotcha worth knowing: under &lt;code&gt;allViewerAndWhitelistCloudFront&lt;/code&gt;, the &lt;code&gt;headers.items&lt;/code&gt; list isn't the &lt;em&gt;only&lt;/em&gt; set of headers forwarded — every viewer header is forwarded too, and the list just adds CloudFront-specific headers on top. The behavior name is more literal than it looks. To restrict to a specific allowlist, use &lt;code&gt;whitelist&lt;/code&gt; (as in the example above) and list both the viewer headers and any CloudFront headers you want.&lt;/p&gt;

&lt;p&gt;To list all available origin request policies (managed and custom) from the AWS CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Returns the 8 managed policies with their IDs&lt;/span&gt;
aws cloudfront list-origin-request-policies &lt;span class="nt"&gt;--type&lt;/span&gt; managed

&lt;span class="c"&gt;# Or list only your custom policies&lt;/span&gt;
aws cloudfront list-origin-request-policies &lt;span class="nt"&gt;--type&lt;/span&gt; custom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To attach a managed policy by ID, set &lt;code&gt;OriginRequestPolicyId&lt;/code&gt; on the relevant cache behavior in your distribution config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudfront update-distribution &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--id&lt;/span&gt; E1A2B3C4D5E6F7 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--distribution-config&lt;/span&gt; file://config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A header isn't reaching the origin in the form you expect
&lt;/h3&gt;

&lt;p&gt;Check whether the same header is configured under &lt;code&gt;Origin Custom Headers&lt;/code&gt; on the distribution. AWS specifically warns against setting the same header in both places. Origin custom headers are applied before CloudFront forwards the request, and if the header is already present in the viewer request, CloudFront overwrites the value. The origin sees one deterministic value, but it may not be the one you set in the origin request policy. Pick one mechanism per header and you avoid the ambiguity.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Gateway returns 403 even though &lt;code&gt;Host&lt;/code&gt; isn't in the allowlist
&lt;/h3&gt;

&lt;p&gt;If you wrote a custom policy and used &lt;code&gt;whitelist&lt;/code&gt; for headers without including &lt;code&gt;Host&lt;/code&gt; in the allowlist, the viewer's &lt;code&gt;Host&lt;/code&gt; header isn't forwarded — but it also isn't &lt;em&gt;replaced&lt;/em&gt;. CloudFront's automatic &lt;code&gt;Host&lt;/code&gt; rewrite only kicks in with the &lt;code&gt;allExcept&lt;/code&gt; behavior when &lt;code&gt;Host&lt;/code&gt; is in the excluded list. For API Gateway, either use &lt;code&gt;AllViewerExceptHostHeader&lt;/code&gt; (the easiest path), or use &lt;code&gt;allExcept&lt;/code&gt; with &lt;code&gt;Host&lt;/code&gt; in the exclusion list.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Authorization&lt;/code&gt; reaches the origin even though it's not in the policy
&lt;/h3&gt;

&lt;p&gt;Your cache policy is including it. Cache-key headers are automatically forwarded; the origin request policy can only add to that. Open the cache policy attached to the same cache behavior and check whether &lt;code&gt;Authorization&lt;/code&gt; is listed under headers there. If you actually want the header to stay out of the origin requests, you'll need to remove it from the cache key — and accept the implications for cache hit ratio, since responses will no longer be keyed on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  A CloudFront-added header isn't arriving
&lt;/h3&gt;

&lt;p&gt;Two different causes look the same from the origin side. First, the header may simply not be in your policy: a bare &lt;code&gt;AllViewer&lt;/code&gt; doesn't include any CloudFront-added headers, and &lt;code&gt;AllViewerAndCloudFrontHeaders-2022-06&lt;/code&gt; is frozen at the set of headers AWS released through June 2022. Anything added later — &lt;code&gt;CloudFront-Viewer-JA3-Fingerprint&lt;/code&gt; and &lt;code&gt;CloudFront-Viewer-JA4-Fingerprint&lt;/code&gt; for TLS fingerprinting, &lt;code&gt;CloudFront-Viewer-Header-Order&lt;/code&gt; and &lt;code&gt;CloudFront-Viewer-Header-Count&lt;/code&gt; for bot detection, &lt;code&gt;CloudFront-Error-Uri&lt;/code&gt; and &lt;code&gt;CloudFront-Error-Args&lt;/code&gt; for error-handling flows — is not in the managed &lt;code&gt;-2022-06&lt;/code&gt; policy. To get those, write a custom origin request policy and list them explicitly.&lt;/p&gt;

&lt;p&gt;Second, even when the header is included, AWS notes that &lt;code&gt;CloudFront-Viewer-City&lt;/code&gt;, &lt;code&gt;CloudFront-Viewer-Metro-Code&lt;/code&gt;, and &lt;code&gt;CloudFront-Viewer-Postal-Code&lt;/code&gt; may not be available for every IP address — some addresses don't geolocate with enough precision. If those fields are blank for some traffic but populated for others, the policy is fine; the geolocation data just isn't there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cookies aren't reaching a Lambda function URL even with &lt;code&gt;AllViewerExceptHostHeader&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Three things to check, in order. First, confirm the browser actually sent the cookie to your CloudFront domain — &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;SameSite=Strict&lt;/code&gt;, and &lt;code&gt;Domain&lt;/code&gt; mismatches all stop browsers from including the cookie in the first place, and no policy can rescue you if the cookie never crossed the wire. Second, confirm the policy you chose includes cookies: &lt;code&gt;AllViewerExceptHostHeader&lt;/code&gt; does forward all cookies, but if you wrote a custom policy and set &lt;code&gt;cookie_behavior = "none"&lt;/code&gt;, you'll see no cookies at the origin regardless of what was in the request. Third, when you do receive the request at the function, remember that Lambda function URLs use the API Gateway v2.0 payload format — cookies arrive in the event's &lt;code&gt;cookies&lt;/code&gt; array, not in &lt;code&gt;headers["Cookie"]&lt;/code&gt;. Code that reads the &lt;code&gt;Cookie&lt;/code&gt; header directly will appear to receive nothing even when the cookies are present in the event.&lt;/p&gt;

&lt;p&gt;If the issue is specific to cross-origin browser requests, the function URL's CORS configuration matters too — specifically the &lt;code&gt;AllowCredentials&lt;/code&gt; setting, which has to be &lt;code&gt;true&lt;/code&gt; for credentialed requests to be allowed at all. But that's only one of three things that need to align: the cookie has to permit cross-origin sending (typically &lt;code&gt;SameSite=None; Secure&lt;/code&gt;), the client-side fetch has to opt into credentials (&lt;code&gt;credentials: 'include'&lt;/code&gt;), and the function URL has to allow credentials. Miss any one and the cookie won't reach the function — and none of that is something CloudFront can fix from the edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking the right policy
&lt;/h2&gt;

&lt;p&gt;A short decision tree:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is your origin API Gateway or a Lambda function URL? → &lt;code&gt;AllViewerExceptHostHeader&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Is your origin S3 and you need CORS? → &lt;code&gt;CORS-S3Origin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Is your origin a custom server (ALB, EC2, ECS) with CORS? → &lt;code&gt;CORS-CustomOrigin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Are you using AWS Elemental MediaTailor? → &lt;code&gt;Elemental-MediaTailor-PersonalizedManifests&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Do you only need to identify the client? → &lt;code&gt;UserAgentRefererHeaders&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Are you running multi-tenant routing on &lt;code&gt;Host&lt;/code&gt;? → &lt;code&gt;HostHeaderOnly&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Does your origin need everything? → &lt;code&gt;AllViewer&lt;/code&gt;, or &lt;code&gt;AllViewerAndCloudFrontHeaders-2022-06&lt;/code&gt; if you want geolocation too&lt;/li&gt;
&lt;li&gt;None of the above? → Write a custom policy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If none of the managed policies match exactly but you're close, custom policies are cheap to create and easy to version. There's no API quota concern for normal use, and you can test changes by attaching the new policy to a non-production cache behavior first.&lt;/p&gt;

&lt;p&gt;The next post in this series will go through CloudFront response headers policies in the same depth — what each managed policy adds, when the browser actually honors it, and where custom policies are worth the maintenance cost.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudfront</category>
      <category>cdn</category>
    </item>
    <item>
      <title>Boosting GPU Performance by Reordering Unstructured Mesh Cells</title>
      <dc:creator>Aleksy Bohdziul</dc:creator>
      <pubDate>Wed, 04 Nov 2026 14:06:30 +0000</pubDate>
      <link>https://dev.to/u11d/boosting-gpu-performance-by-reordering-unstructured-mesh-cells-1fod</link>
      <guid>https://dev.to/u11d/boosting-gpu-performance-by-reordering-unstructured-mesh-cells-1fod</guid>
      <description>&lt;p&gt;We're building a GPU flood solver for unstructured 2D meshes. On every step, each cell reads its neighbors' state and turns those values into fluxes across its edges. Before optimizing the solver, we wanted to answer one question: how much does the order of cells in memory affect the speed of those neighbor reads?&lt;/p&gt;

&lt;p&gt;To find out, we swapped the solver for Game of Life on the same kind of mesh. The memory access pattern is the same (read your neighbors, write yourself), but there's almost no math, so the result mostly measures memory performance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fktbglhlzuc1fvc3ym9bs.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fktbglhlzuc1fvc3ym9bs.gif" alt="Game of Life on the 768x768 hex mesh, Hilbert order" width="560" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Storing cells along a space-filling curve instead of in random order made the benchmark 1.9-3x faster. We didn't change the kernel or the mesh. The simulation computes the same result, only the cell numbering changed.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foa7kvclj14txmylusld6.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foa7kvclj14txmylusld6.png" alt="Throughput by ordering, three meshes" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cell reordering assigns nearby mesh cells nearby positions in memory so GPU neighbor reads can reuse cache lines more effectively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cell Ordering Affects GPU Performance
&lt;/h2&gt;

&lt;p&gt;A GPU memory system fetches data in cache-line-sized transactions. If a cell's neighbors live next to it in memory, one fetch can cover many of them. If they're scattered, neighbor reads can require separate trips to device memory.&lt;/p&gt;

&lt;p&gt;Meshes don't always come with a useful order. Our Voronoi mesh numbers its cells in the order the random sites were generated, so random order is our baseline: it's what you get if you do nothing. The hex grids naturally come out row by row, which already has decent locality, so we shuffled them to create the same baseline.&lt;/p&gt;

&lt;p&gt;A space-filling curve—Hilbert, Morton, or Peano—snakes through the 2D domain. Cells that are near each other in space then tend to be near each other in memory.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw7jxtrp6oef3d9gp8ul7.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw7jxtrp6oef3d9gp8ul7.png" alt="Random vs Hilbert ordering on a small hex mesh" width="800" height="246"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each dot is colored by the cell's position in memory. With random order (left), you get noise. With Hilbert order (right), memory sweeps through the domain in compact blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hardware:&lt;/strong&gt; an M1 Pro running Taichi 1.7.4, a Python framework that compiles kernels to the GPU, on the Vulkan backend. On a Mac, MoltenVK translates Vulkan to Metal. Every timed run reported &lt;code&gt;backend: vulkan&lt;/code&gt;, so nothing quietly fell back to the CPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meshes:&lt;/strong&gt; stored as CSR adjacency graphs. A 768x768 hex grid (589,824 cells, 3.53M directed edges), a 512x512 hex grid (262,144 cells), and a Voronoi mesh built from 200,000 random sites with scipy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orderings:&lt;/strong&gt; each one is a permutation applied to the cells before the run. Random uses &lt;code&gt;rng.permutation&lt;/code&gt;. For the curves, we snap cell centers to a grid, compute each cell's position along the curve, and sort with &lt;code&gt;argsort&lt;/code&gt;. Neighbor indices get remapped to match. Geometry, initial state, and rules stay the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kernel:&lt;/strong&gt; two &lt;code&gt;i32&lt;/code&gt; buffers, one for the current state and one for the next. One kernel counts live neighbors along each CSR row and looks up birth/survive tables, then a second one copies the new state back. Standard B3/S23 rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing:&lt;/strong&gt; an untimed warmup for each ordering, &lt;code&gt;ti.sync()&lt;/code&gt; on both sides of the timed region, and no reads back to the host inside that region. Each number is the median of 5 runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  GPU Performance Results
&lt;/h2&gt;

&lt;p&gt;Hex 768x768, 1000 steps, 100 warmup steps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ordering&lt;/th&gt;
&lt;th&gt;steps/s&lt;/th&gt;
&lt;th&gt;range over 5 runs&lt;/th&gt;
&lt;th&gt;µs/step&lt;/th&gt;
&lt;th&gt;G cell-updates/s&lt;/th&gt;
&lt;th&gt;vs random&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;peano&lt;/td&gt;
&lt;td&gt;4273&lt;/td&gt;
&lt;td&gt;4252-4274&lt;/td&gt;
&lt;td&gt;234&lt;/td&gt;
&lt;td&gt;2.52&lt;/td&gt;
&lt;td&gt;2.96x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hilbert&lt;/td&gt;
&lt;td&gt;4033&lt;/td&gt;
&lt;td&gt;4000-4058&lt;/td&gt;
&lt;td&gt;248&lt;/td&gt;
&lt;td&gt;2.38&lt;/td&gt;
&lt;td&gt;2.80x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;morton&lt;/td&gt;
&lt;td&gt;4009&lt;/td&gt;
&lt;td&gt;3950-4102&lt;/td&gt;
&lt;td&gt;249&lt;/td&gt;
&lt;td&gt;2.36&lt;/td&gt;
&lt;td&gt;2.78x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;random&lt;/td&gt;
&lt;td&gt;1441&lt;/td&gt;
&lt;td&gt;1437-1444&lt;/td&gt;
&lt;td&gt;694&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Smaller meshes, 500 steps, 50 warmup steps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;mesh&lt;/th&gt;
&lt;th&gt;best curve&lt;/th&gt;
&lt;th&gt;worst curve&lt;/th&gt;
&lt;th&gt;random&lt;/th&gt;
&lt;th&gt;curve vs random&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;hex 512x512&lt;/td&gt;
&lt;td&gt;peano 8840&lt;/td&gt;
&lt;td&gt;hilbert 8358&lt;/td&gt;
&lt;td&gt;3566&lt;/td&gt;
&lt;td&gt;2.34-2.48x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voronoi 200k&lt;/td&gt;
&lt;td&gt;hilbert 9491&lt;/td&gt;
&lt;td&gt;morton 8934&lt;/td&gt;
&lt;td&gt;4693&lt;/td&gt;
&lt;td&gt;1.90-2.02x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The choice of curve has little effect
&lt;/h3&gt;

&lt;p&gt;Peano came out on top for both hex meshes in this run, while Hilbert won on Voronoi. In a repeat run, Morton took first place on the 512-cell hex mesh. The curves stayed within 9% of one another, and each beat random order by about 1.9-3x. All three were effective. Our solver uses Hilbert, but Morton is the simplest to implement, so the example below uses it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Random ordering limits throughput
&lt;/h3&gt;

&lt;p&gt;Under random order, every mesh runs at about 0.9 billion cell updates per second, whatever its size or shape. Random ordering causes poor cache reuse, so mesh layout has little effect. With a curve, the same meshes reach 1.8-2.5 billion updates per second because neighbors are stored closer together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory is the bottleneck
&lt;/h3&gt;

&lt;p&gt;If you count only the bytes each step has to move (edge list, row offsets, state reads, and writes), the curve orders move about 105-111 GB/s on the big hex mesh. That's more than half of the M1 Pro's 200 GB/s spec. Random order manages 37 GB/s of useful traffic and spends the rest of its time waiting on cache misses.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Reorder Cells in Your Own Mesh
&lt;/h2&gt;

&lt;p&gt;You need two NumPy functions. &lt;code&gt;morton_order&lt;/code&gt; takes cell centers and returns a permutation, and &lt;code&gt;reorder_csr&lt;/code&gt; renumbers a CSR adjacency graph to match it. Here, &lt;code&gt;order&lt;/code&gt; maps each new cell position to its old cell ID; &lt;code&gt;new_id&lt;/code&gt; is the inverse mapping used to rewrite neighbor indices. Apply the same permutation to every other per-cell array you have (state, geometry, boundary flags).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;morton_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Permutation that sorts cells along a Z-order (Morton) curve.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&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="n"&gt;xy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;xy&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lo&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="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;bits&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;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xy&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bits&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;g&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="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;g&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;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&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;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argsort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reorder_csr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Renumber cells so that new cell k is old cell order[k].&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;new_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new_id&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;deg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indptr&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;new_indptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concatenate&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="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cumsum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deg&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indptr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;new_indptr&lt;/span&gt;&lt;span class="p"&gt;[:&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="n"&gt;deg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_indptr&lt;/span&gt;&lt;span class="p"&gt;[&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;return&lt;/span&gt; &lt;span class="n"&gt;new_indptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_id&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;

&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;morton_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;indptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reorder_csr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# int64; cast with .astype(np.int32) for i32 GPU buffers
&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;bits=10&lt;/code&gt;, the domain is split into a 1024x1024 grid. Cells that land in the same grid square keep their original relative order, so if your mesh is much finer than that, increase &lt;code&gt;bits&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reordering is a one-time cost.&lt;/strong&gt; On the 768 hex mesh, the code above runs in about 110 ms: 74 ms to compute the keys and sort, 33 ms to rebuild the CSR graph. Each step saves about 450 µs, so it pays for itself after roughly 240 steps. You only do it when you load the mesh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading state back to the CPU is slow.&lt;/strong&gt; If we copy the full state to the host after every step, the 768 hex mesh with Hilbert order drops from 4033 to 565 steps/s (median of 5 runs of 1000 steps, ranging from 510 to 826). Each readback costs about 1.5 ms, six times the step time. A flood run still needs periodic output, so how often you read back matters about as much as how you order the cells.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;All of these numbers come from a single M1 Pro going through MoltenVK. A discrete GPU with a different cache setup will give different ratios. Game of Life also does almost no math per neighbor, while a real flux kernel does plenty, so the gap should shrink. Treat 2-3x as the most you can expect from reordering on this hardware. Your own kernel will probably gain less.&lt;/p&gt;

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

&lt;p&gt;If your GPU code gathers neighbor data on an unstructured mesh, check the order of your cells. If the mesher or loader hands them over in arbitrary order, you're probably leaving speed on the table. Sorting them along a Morton curve takes about 20 lines of NumPy and a tenth of a second when the mesh loads, and on this benchmark it made every step 2-3x faster.&lt;/p&gt;

&lt;p&gt;For us, the real test is the flood solver itself. It already orders its cells along a Hilbert curve, and next we'll time its flux kernel the same way to see how much of that speedup survives once the kernel does real math.&lt;/p&gt;

</description>
      <category>gpu</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Beyond Prompts: Enforcing Repository Rules With Git Hooks</title>
      <dc:creator>Daniel Kraszewski</dc:creator>
      <pubDate>Fri, 30 Oct 2026 09:03:29 +0000</pubDate>
      <link>https://dev.to/u11d/beyond-prompts-enforcing-repository-rules-with-git-hooks-a6k</link>
      <guid>https://dev.to/u11d/beyond-prompts-enforcing-repository-rules-with-git-hooks-a6k</guid>
      <description>&lt;p&gt;You have probably already tried the obvious thing. You wrote the rules down - in &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;.cursor/rules&lt;/code&gt;, or whatever file your agent reads first. Do not commit without tests. Do not leave a component without a story. Run the type checker before you say you are done. The agent agreed, enthusiastically, and then went ahead and did something else anyway.&lt;/p&gt;

&lt;p&gt;This is not a prompt-quality problem, and it is not specific to any one model. Large language models optimize for the next plausible token, not for your repository's conventions. In a small codebase you can review every diff and catch the drift by hand. In a large one you cannot: the agent touches a dozen files across a monorepo, the formatting is off, a component is missing its story, a test that used to pass now does not, and the commit lands anyway because nobody is running the checks at the moment the change is written.&lt;/p&gt;

&lt;p&gt;The fix is not a better prompt; it is a gate. A Git hook runs your checks on every change, at &lt;code&gt;git commit&lt;/code&gt; and &lt;code&gt;git push&lt;/code&gt;, whoever - or whatever - wrote it. The tool I use to manage them is &lt;a href="https://github.com/evilmartians/lefthook" rel="noopener noreferrer"&gt;Lefthook&lt;/a&gt;: a single, dependency-free Go binary, driven by one YAML file, able to run jobs in parallel and re-stage the files it fixes. It reaches past Git, too: an &lt;code&gt;ai:&lt;/code&gt; block (currently beta) lets Claude Code, Cursor, Codex, and Copilot run Lefthook hooks on their own events, so your checks fire when the agent stops working - before anything is ever committed. What follows is a working setup: the Git hooks first, then the same checks wired into the agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;

&lt;p&gt;If you want the short answer before the walkthrough, this is the whole tool at a glance:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it is&lt;/td&gt;
&lt;td&gt;A hooks manager for Git and for coding agents; one binary, no runtime dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config file&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;lefthook.yml&lt;/code&gt;, or a &lt;code&gt;.lefthook.yaml&lt;/code&gt; / &lt;code&gt;.toml&lt;/code&gt; / &lt;code&gt;.json&lt;/code&gt; variant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install the hooks&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lefthook install&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run a hook by hand&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lefthook run pre-commit&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validate the config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lefthook validate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skip once&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LEFTHOOK=0 git commit&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent hooks (beta)&lt;/td&gt;
&lt;td&gt;Run your Lefthook hooks on Claude Code, Cursor, Codex, or Copilot events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current version&lt;/td&gt;
&lt;td&gt;v2.1.17 (October 2026)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The config we build in the next sections wires up five checks on commit and two on push; later, the same checks are attached to your coding agent's events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Instructions Are Not Enforcement
&lt;/h2&gt;

&lt;p&gt;A written rule is advisory. The agent may follow it, may partially follow it, or may follow it for the first three files and forget by the fourth - and you will not know which until you read the diff. A hook is deterministic. It runs the same command on the same files every single time, it fails closed, and it does not care who authored the change. If the check fails, the commit does not happen.&lt;/p&gt;

&lt;p&gt;That difference matters more, not less, as codebases grow. Instructions have to be read, remembered, and applied; enforcement is automatic. It also gives you a single place to put the rules that are specific to your project - the ones no linter ships with, like "every component must have a story" - instead of repeating them across agent instruction files and hoping.&lt;/p&gt;

&lt;p&gt;Lefthook fits here because it is small. It is a Go binary with no runtime to install, and it reads &lt;code&gt;lefthook.yml&lt;/code&gt; on every hook run, so you never reinstall after editing the config - for Git hooks or for the agent hooks we add later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install Lefthook
&lt;/h2&gt;

&lt;p&gt;The most common route is the package manager you already use. For a Node project that is the &lt;code&gt;lefthook&lt;/code&gt; npm package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; lefthook     &lt;span class="c"&gt;# or: npm install --save-dev lefthook / yarn add --dev lefthook&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are equivalents for almost every ecosystem, all documented in the &lt;a href="https://lefthook.dev/install/" rel="noopener noreferrer"&gt;installation guide&lt;/a&gt;: &lt;code&gt;brew install lefthook&lt;/code&gt; on macOS and Linux, &lt;code&gt;go install github.com/evilmartians/lefthook/v2@v2.1.17&lt;/code&gt;, &lt;code&gt;pipx install lefthook&lt;/code&gt;, &lt;code&gt;gem install lefthook&lt;/code&gt;, plus &lt;code&gt;apt&lt;/code&gt;, &lt;code&gt;rpm&lt;/code&gt;, &lt;code&gt;apk&lt;/code&gt;, &lt;code&gt;winget&lt;/code&gt;, &lt;code&gt;scoop&lt;/code&gt;, &lt;code&gt;snap&lt;/code&gt;, and &lt;code&gt;mise&lt;/code&gt;. If you would rather not use a package manager at all, download the binary from the &lt;a href="https://github.com/evilmartians/lefthook/releases" rel="noopener noreferrer"&gt;latest release&lt;/a&gt; and put it on your &lt;code&gt;PATH&lt;/code&gt;; &lt;code&gt;lefthook self-update&lt;/code&gt; keeps it current.&lt;/p&gt;

&lt;p&gt;For CI the mechanics are slightly different, because Git hooks are a local, client-side feature. Nothing runs your &lt;code&gt;pre-commit&lt;/code&gt; hook in a container that just cloned the repo - there is no &lt;code&gt;.git/hooks/pre-commit&lt;/code&gt; doing work unless you install it. So the CI job installs Lefthook and then calls the hook explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; lefthook
pnpm &lt;span class="nb"&gt;exec &lt;/span&gt;lefthook run pre-commit &lt;span class="nt"&gt;--all-files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--all-files&lt;/code&gt; replaces &lt;code&gt;{staged_files}&lt;/code&gt; with every tracked file, which is what you want when there is no index to speak of. There is no official Lefthook GitHub Action and no official Docker image, so in CI you install the binary yourself - &lt;code&gt;brew&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;npm&lt;/code&gt;, or your base image's package manager - and call the hook explicitly. One detail to know: when the npm package is installed with &lt;code&gt;CI=true&lt;/code&gt; set, its &lt;code&gt;postinstall&lt;/code&gt; script deliberately skips hook installation, so a CI run will not try to wire up &lt;code&gt;.git/hooks&lt;/code&gt; that nothing will execute. Your CI step calling &lt;code&gt;lefthook run&lt;/code&gt; is what does the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Write the Config
&lt;/h2&gt;

&lt;p&gt;The whole configuration is one file at the repository root. This is the shape I use on a TypeScript/React project, and it is the config the runs below use:&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="c1"&gt;# lefthook.yml&lt;/span&gt;
&lt;span class="na"&gt;min_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.1.10&lt;/span&gt;
&lt;span class="na"&gt;lefthook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm exec lefthook&lt;/span&gt;
&lt;span class="na"&gt;glob_matcher&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;doublestar&lt;/span&gt;

&lt;span class="na"&gt;pre-commit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&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;format staged files&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.{js,jsx,ts,tsx,json,md,yml,yaml,css}"&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;pnpm format {staged_files}&lt;/span&gt;
      &lt;span class="na"&gt;stage_fixed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;lint staged files&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.{js,jsx,ts,tsx}"&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;pnpm lint {staged_files}&lt;/span&gt;
      &lt;span class="na"&gt;stage_fixed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;typecheck&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;pnpm check:types&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;stories-present&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/components/**/*.{ts,tsx}"&lt;/span&gt;
      &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.stories.{ts,tsx}"&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;pnpm check:stories {staged_files}&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;unit tests&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;pnpm test:unit&lt;/span&gt;

&lt;span class="na"&gt;pre-push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parallel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&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;full verification&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;pnpm verify&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;storybook tests&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;pnpm test:storybook&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven jobs in total, split across the two hooks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Hook&lt;/th&gt;
&lt;th&gt;What it enforces&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;format staged files&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-commit&lt;/td&gt;
&lt;td&gt;Prettier rewrites staged files, then Lefthook re-stages them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lint staged files&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-commit&lt;/td&gt;
&lt;td&gt;ESLint &lt;code&gt;--fix&lt;/code&gt; rewrites staged files, then re-stages them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;typecheck&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-commit&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tsc --noEmit&lt;/code&gt; over the whole project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stories-present&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-commit&lt;/td&gt;
&lt;td&gt;A custom rule: every component has a story file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unit tests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-commit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;node --test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;full verification&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-push&lt;/td&gt;
&lt;td&gt;Typecheck, tests, and the custom checks together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;storybook tests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-push&lt;/td&gt;
&lt;td&gt;Component-level tests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few keys carry most of the weight. The &lt;code&gt;format&lt;/code&gt;, &lt;code&gt;lint&lt;/code&gt;, &lt;code&gt;check:*&lt;/code&gt;, and &lt;code&gt;test:*&lt;/code&gt; targets are your own &lt;code&gt;package.json&lt;/code&gt; scripts, so the config stays tool-agnostic: replacing Prettier with Biome is a change to one script body, not to seven hook entries. &lt;code&gt;jobs&lt;/code&gt; is the modern list form (the older &lt;code&gt;commands:&lt;/code&gt; map still works, but jobs also support scripts and groups). &lt;code&gt;glob&lt;/code&gt; filters the file list a job receives, and &lt;code&gt;exclude&lt;/code&gt; removes files from that filtered list. &lt;code&gt;run&lt;/code&gt; is the command, executed with &lt;code&gt;sh&lt;/code&gt;. The &lt;code&gt;{staged_files}&lt;/code&gt; template expands to the files staged for the current commit - for a &lt;code&gt;pre-push&lt;/code&gt; job you would use &lt;code&gt;{push_files}&lt;/code&gt;, and &lt;code&gt;{all_files}&lt;/code&gt; gives you everything tracked.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;stage_fixed: true&lt;/code&gt; is the one that changes how it feels to use. When a job rewrites its files - Prettier reformatting, ESLint applying an autofix - Lefthook runs &lt;code&gt;git add&lt;/code&gt; on them afterward, so the fixes land in the same commit instead of leaving a dirty working tree behind. It only works on &lt;code&gt;pre-commit&lt;/code&gt;, and because each of those jobs mutates the index, I keep the auto-fixing jobs sequential by leaving &lt;code&gt;parallel&lt;/code&gt; at its default (&lt;code&gt;false&lt;/code&gt;). If the &lt;code&gt;git add&lt;/code&gt; fails, Lefthook fails the hook rather than let the commit go through with unfixed content. If you would rather fail than let a hook rewrite files, invert the two auto-fixing jobs: run &lt;code&gt;prettier --check&lt;/code&gt; and &lt;code&gt;eslint&lt;/code&gt; without &lt;code&gt;--fix&lt;/code&gt;, drop &lt;code&gt;stage_fixed&lt;/code&gt;, and the &lt;code&gt;pre-commit&lt;/code&gt; hook becomes a pure, parallel-safe gate.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;typecheck&lt;/code&gt;, &lt;code&gt;stories-present&lt;/code&gt;, and &lt;code&gt;unit tests&lt;/code&gt; jobs do not use &lt;code&gt;{staged_files}&lt;/code&gt;; they run against the whole project, because a change in one file can break a type or a test somewhere else. The &lt;code&gt;stories-present&lt;/code&gt; job is the interesting one for the agent narrative: it is a rule that exists only in this repository. Its glob is the part that matters, and it is also the part that is easy to get wrong - see The Glob That Silently Disables Your Hook.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;pre-push&lt;/code&gt; hook is deliberately broader and parallel. It runs &lt;code&gt;pnpm verify&lt;/code&gt; (types, unit tests, and the custom story check) alongside the component tests. This is the layer that catches what a distracted &lt;code&gt;--no-verify&lt;/code&gt; commit slipped past, and it is the right place for the slower checks you do not want on every keystroke.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Install and Run the Hooks
&lt;/h2&gt;

&lt;p&gt;Install the hooks once per clone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;exec &lt;/span&gt;lefthook &lt;span class="nb"&gt;install&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;sync hooks: ✔️(pre-push, pre-commit)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command writes small shell scripts into &lt;code&gt;.git/hooks/&lt;/code&gt; for each configured hook; each script simply calls &lt;code&gt;lefthook run &amp;lt;hook&amp;gt;&lt;/code&gt;. Because the config is read fresh every time a hook runs, editing &lt;code&gt;lefthook.yml&lt;/code&gt; never requires reinstalling. You can confirm the hooks are present and synchronized with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;exec &lt;/span&gt;lefthook validate     &lt;span class="c"&gt;# config is well-formed&lt;/span&gt;
pnpm &lt;span class="nb"&gt;exec &lt;/span&gt;lefthook check-install &lt;span class="c"&gt;# exit 0 if hooks are installed and in sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run a hook without committing, &lt;code&gt;lefthook run&lt;/code&gt; is the entry point. &lt;code&gt;lefthook run pre-commit&lt;/code&gt; runs the hook as-is, &lt;code&gt;lefthook run pre-commit --all-files&lt;/code&gt; forces every tracked file through it, &lt;code&gt;--file path/to/file&lt;/code&gt; forces a specific list, and &lt;code&gt;--job&lt;/code&gt; or &lt;code&gt;--tag&lt;/code&gt; narrows it to specific jobs. Any hook name is a valid target, so a custom block such as &lt;code&gt;verify:&lt;/code&gt; becomes &lt;code&gt;lefthook run verify&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two escape hatches are worth knowing. &lt;code&gt;LEFTHOOK=0 git commit&lt;/code&gt; disables hooks for a single command, and &lt;code&gt;git commit --no-verify&lt;/code&gt; bypasses hooks at the Git level. I mention them because they are necessary - but they are also exactly why the push hook and CI matter: a rule enforced only at commit time is a rule that a tired developer or a confident agent can skip.&lt;/p&gt;

&lt;p&gt;Finally, teams rarely want identical hooks on every machine. A &lt;code&gt;lefthook-local.yml&lt;/code&gt; file (git-ignored) merges over &lt;code&gt;lefthook.yml&lt;/code&gt;, so an individual can set &lt;code&gt;skip: true&lt;/code&gt; on a slow job or re-tag a command without touching the shared config.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Looks Like When a Commit Is Blocked
&lt;/h2&gt;

&lt;p&gt;Here is a real run, on a small demo repository, of an agent finishing a change and committing it. The change adds a &lt;code&gt;greet&lt;/code&gt; helper written in double quotes with a &lt;code&gt;let&lt;/code&gt; that is never reassigned, and a &lt;code&gt;Modal&lt;/code&gt; component with no story file. The first attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git commit -m "feat: add greeting helper and modal component"
╭───────────────────────────────────────╮
│ lefthook  v2.1.17   hook:  pre-commit │
╰───────────────────────────────────────╯
┃  format staged files ❯ 
$ prettier --write lefthook.yml src/components/Modal.ts src/greeting.ts
lefthook.yml 12ms
src/components/Modal.ts 18ms (unchanged)
src/greeting.ts 3ms

┃  lint staged files ❯ 
$ eslint --fix src/components/Modal.ts src/greeting.ts

┃  typecheck ❯ 
$ tsc --noEmit

┃  stories-present ❯ 
$ node scripts/check-stories.mjs src/components/Modal.ts
Missing story files for:
  src/components/Modal.ts
[ELIFECYCLE] Command failed with exit code 1.

exit status 1
...

  ────────────────────────────────────
summary: (done in 1.24 seconds)
✓ format staged files (0.26 seconds)
✓ lint staged files (0.44 seconds)
✓ typecheck (0.37 seconds)
✓ unit tests (0.11 seconds)
✗ stories-present (0.05 seconds)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commit does not happen. Two different things occurred in that run, and they are the two halves of why this works. First, &lt;code&gt;format&lt;/code&gt; and &lt;code&gt;lint&lt;/code&gt; silently fixed the sloppy code: Prettier rewrote the quotes and style, ESLint turned the &lt;code&gt;let&lt;/code&gt; into a &lt;code&gt;const&lt;/code&gt;, and &lt;code&gt;stage_fixed&lt;/code&gt; re-staged both. You can prove the fixed content is what is staged, not just present in the working tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git show :src/greeting.ts
export function greet(name: string) {
  const message = 'Hello, ' + name + '!'
  return message
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, &lt;code&gt;stories-present&lt;/code&gt; refused the change. The component exists, the story does not, and no amount of instruction-following on the agent's part makes the commit land. Add the missing story and the same commit passes, every job green:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git add src/components/Modal.stories.ts
$ git commit -m "feat: add greeting helper and modal component"
...

  ────────────────────────────────────
summary: (done in 1.17 seconds)
✓ format staged files (0.14 seconds)
✓ lint staged files (0.47 seconds)
✓ typecheck (0.38 seconds)
✓ stories-present (0.05 seconds)
✓ unit tests (0.12 seconds)
[main de227fe] feat: add greeting helper and modal component
 4 files changed, 17 insertions(+), 4 deletions(-)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git push&lt;/code&gt; runs the second gate. Both jobs run in parallel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git push origin main
╭─────────────────────────────────────╮
│ lefthook  v2.1.17   hook:  pre-push │
╰─────────────────────────────────────╯
┃  storybook tests ❯ 
$ node scripts/check-stories.mjs
Story check passed for 2 component file(s).

┃  full verification ❯ 
$ pnpm check:types &amp;amp;&amp;amp; pnpm test:unit &amp;amp;&amp;amp; pnpm check:stories
$ tsc --noEmit
$ node --test "src/**/*.test.ts"
✔ adds two numbers (0.467667ms)
ℹ pass 1
ℹ fail 0
$ node scripts/check-stories.mjs
Story check passed for 2 component file(s).

  ────────────────────────────────────
summary: (done in 0.65 seconds)
✓ storybook tests (0.06 seconds)
✓ full verification (0.55 seconds)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Glob That Silently Disables Your Hook
&lt;/h2&gt;

&lt;p&gt;The config above includes &lt;code&gt;glob_matcher: doublestar&lt;/code&gt;, and that one line is doing more than it looks. A pattern like &lt;code&gt;src/components/**/*.{ts,tsx}&lt;/code&gt; reads as correct, but without &lt;code&gt;doublestar&lt;/code&gt; it never matches a file sitting directly in &lt;code&gt;src/components/&lt;/code&gt;. Here is what that looks like on a commit that adds &lt;code&gt;src/components/Modal.ts&lt;/code&gt; with no story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git commit -m "feat: add modal component"
...
│  stories-present (skip) no files for inspection
  ────────────────────────────────────
summary: (done in 1.10 seconds)
✓ format staged files (0.11 seconds)
✓ lint staged files (0.49 seconds)
✓ typecheck (0.38 seconds)
✓ unit tests (0.12 seconds)
[main 511f599] feat: add modal component
 1 file changed, 3 insertions(+)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The check did not fail. It did not run at all. In Lefthook's default matcher (&lt;code&gt;gobwas&lt;/code&gt;), &lt;code&gt;**&lt;/code&gt; matches &lt;strong&gt;one or more&lt;/strong&gt; directories, not zero or more, so &lt;code&gt;src/components/**/*.ts&lt;/code&gt; requires at least one directory between &lt;code&gt;src/components/&lt;/code&gt; and the file - it matches &lt;code&gt;src/components/Button/Button.ts&lt;/code&gt; but not &lt;code&gt;src/components/Modal.ts&lt;/code&gt;. With no matching files, Lefthook prints &lt;code&gt;(skip) no files for inspection&lt;/code&gt; and moves on, and the commit succeeds. A guard you believe is watching is worse than no guard, because you stop checking by hand.&lt;/p&gt;

&lt;p&gt;There are two ways out. The recommended one is to switch to standard glob semantics for the whole config:&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;glob_matcher&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;doublestar&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;doublestar&lt;/code&gt;, &lt;code&gt;**/&lt;/code&gt; matches zero or more directories, so &lt;code&gt;src/components/**/*.{ts,tsx}&lt;/code&gt; matches &lt;code&gt;Modal.ts&lt;/code&gt; as expected - this is the reason the setting appears at the top of the config above. The catch is that the same setting also makes &lt;code&gt;*&lt;/code&gt; separator-aware: a bare &lt;code&gt;*.ts&lt;/code&gt; now matches only files in the repository root, where the default matcher had let &lt;code&gt;*&lt;/code&gt; cross &lt;code&gt;/&lt;/code&gt;. That is why every file glob in the config above uses &lt;code&gt;**/*.{...}&lt;/code&gt;: with &lt;code&gt;doublestar&lt;/code&gt;, that is how you say "at any depth".&lt;/p&gt;

&lt;p&gt;The second option is to keep the default matcher and write the pattern for its rules: because &lt;code&gt;*&lt;/code&gt; crosses &lt;code&gt;/&lt;/code&gt; there, &lt;code&gt;src/components/*.{ts,tsx}&lt;/code&gt; already matches at any depth. That works, but it depends on a matcher behavior most people do not expect, so I prefer the explicit switch. Either way, the lesson generalizes: after you add a file-filtering rule, verify it actually fires. Lefthook's &lt;code&gt;(skip) no files for inspection&lt;/code&gt; line in the output is the tell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the Agent Run the Checks Too
&lt;/h2&gt;

&lt;p&gt;The hooks above fire on &lt;code&gt;git commit&lt;/code&gt; and &lt;code&gt;git push&lt;/code&gt;. An agent that edits twenty files and stops - without committing - never triggers them. Lefthook has a beta feature for exactly this: an &lt;code&gt;ai:&lt;/code&gt; block that declares which of your hooks an agent should call on which of its own events. During &lt;code&gt;lefthook install&lt;/code&gt;, Lefthook generates the provider's settings file for you.&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;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;claude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Stop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;verify&lt;/span&gt;
  &lt;span class="na"&gt;cursor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;stop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;verify&lt;/span&gt;

&lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&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;typecheck&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;pnpm check:types&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;unit tests&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;pnpm test:unit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Stop&lt;/code&gt; is Claude Code's event for "the agent finished responding", and &lt;code&gt;stop&lt;/code&gt; is the Cursor equivalent; both map to the &lt;code&gt;verify&lt;/code&gt; hook that Lefthook then runs. Supported providers are &lt;code&gt;claude&lt;/code&gt;, &lt;code&gt;codex&lt;/code&gt;, &lt;code&gt;cursor&lt;/code&gt;, and &lt;code&gt;copilot&lt;/code&gt;. Running &lt;code&gt;lefthook install&lt;/code&gt; with the config above updates &lt;code&gt;.claude/settings.json&lt;/code&gt;:&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;"hooks"&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;"Stop"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm exec lefthook run verify"&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;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="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;Note the command. Generated hook commands use the top-level &lt;code&gt;lefthook:&lt;/code&gt; value when it is set - here &lt;code&gt;pnpm exec lefthook&lt;/code&gt; - and otherwise fall back to the absolute path of the binary that ran &lt;code&gt;install&lt;/code&gt;. Since that absolute path is machine-specific and would poison a committed settings file, setting &lt;code&gt;lefthook:&lt;/code&gt; to a portable command is the difference between a file your teammates can share and a file that only works on your laptop. This feature is marked beta, so treat the generated output as version-sensitive and keep an eye on it when you upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lefthook vs. pre-commit vs. Husky
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Lefthook&lt;/th&gt;
&lt;th&gt;pre-commit&lt;/th&gt;
&lt;th&gt;Husky&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Single Go binary, no runtime&lt;/td&gt;
&lt;td&gt;Python framework&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config&lt;/td&gt;
&lt;td&gt;One YAML/TOML/JSON file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.pre-commit-config.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shell scripts in &lt;code&gt;.husky/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs against&lt;/td&gt;
&lt;td&gt;Staged/pushed/all files via templates&lt;/td&gt;
&lt;td&gt;Staged files by default&lt;/td&gt;
&lt;td&gt;Whatever the script does&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallel jobs&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;No (manual)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolated tool envs&lt;/td&gt;
&lt;td&gt;No (uses your project's tools)&lt;/td&gt;
&lt;td&gt;Yes (per-hook language envs)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language ecosystems&lt;/td&gt;
&lt;td&gt;Any (it just runs commands)&lt;/td&gt;
&lt;td&gt;Many, via hook repos&lt;/td&gt;
&lt;td&gt;Any, in Node projects&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is no objective winner here; pick the one that fits how your repository already works. If you rely on pre-commit's managed, per-language hook environments, stay there - that isolation is real value. If your repo is Node-based, Husky is familiar and perfectly capable. I reach for Lefthook for three reasons: it is one binary so nothing new has to be installed at runtime, its jobs run in parallel so the hook stays fast, and &lt;code&gt;{staged_files}&lt;/code&gt;, &lt;code&gt;stage_fixed&lt;/code&gt;, and glob filtering are built into the config rather than pushed into scripts. As always with tools, the right answer is the one your team will actually keep running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting &amp;amp; Gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A glob that matches nothing silently skips the job.&lt;/strong&gt; This is the failure from the section above, and it is the most dangerous one because the hook reports success. &lt;code&gt;**&lt;/code&gt; in the default &lt;code&gt;gobwas&lt;/code&gt; matcher means one or more directories; &lt;code&gt;(skip) no files for inspection&lt;/code&gt; in the output means your filter is excluding everything. Set &lt;code&gt;glob_matcher: doublestar&lt;/code&gt; and use &lt;code&gt;**/&lt;/code&gt; where you mean any depth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;stage_fixed&lt;/code&gt; only works on &lt;code&gt;pre-commit&lt;/code&gt;, and it needs the index.&lt;/strong&gt; It calls &lt;code&gt;git add&lt;/code&gt; after the job rewrites files, so it cannot fix anything on &lt;code&gt;pre-push&lt;/code&gt; or a custom hook. Keep auto-fixing jobs on &lt;code&gt;pre-commit&lt;/code&gt; and sequential: if several jobs run in parallel and rewrite the index at the same time, keep them out of that batch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--no-verify&lt;/code&gt; and &lt;code&gt;LEFTHOOK=0&lt;/code&gt; bypass everything.&lt;/strong&gt; Hooks are a convenience gate, not a security boundary. For rules that must hold, run the same checks in CI (&lt;code&gt;lefthook run pre-commit --all-files&lt;/code&gt;) and make that job required. Do not rely on the client-side hook as your only enforcement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pnpm gates the postinstall script.&lt;/strong&gt; The &lt;code&gt;lefthook&lt;/code&gt; npm package installs hooks from a &lt;code&gt;postinstall&lt;/code&gt; script, but pnpm blocks dependency build scripts by default. In pnpm 11/12 the setting moved from &lt;code&gt;pnpm.onlyBuiltDependencies&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt; to an &lt;code&gt;allowBuilds&lt;/code&gt; map in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;; approve it and the postinstall runs. Failing that, &lt;code&gt;lefthook install&lt;/code&gt; explicitly does the same thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fresh releases can be held back by supply-chain policies.&lt;/strong&gt; pnpm refused to install the just-published &lt;code&gt;lefthook@2.1.17&lt;/code&gt; because it was inside the default &lt;code&gt;minimumReleaseAge&lt;/code&gt; window; &lt;code&gt;minimumReleaseAge: 0&lt;/code&gt; (or waiting out the window) is the adjustment. Useful to know when a version you just saw released will not install yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A long file list is split into several commands.&lt;/strong&gt; Command-line length is capped on every OS, so Lefthook runs your command in batches when &lt;code&gt;{staged_files}&lt;/code&gt; is large. A linter that prints per-run summaries will print one per batch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check that the hooks are actually installed.&lt;/strong&gt; A fresh clone has no hooks until &lt;code&gt;lefthook install&lt;/code&gt; runs - the npm postinstall usually handles it, but clones that skip install scripts will not, and &lt;code&gt;CI=true&lt;/code&gt; makes the postinstall skip on purpose. &lt;code&gt;lefthook check-install&lt;/code&gt; returns exit code 0 only when the hooks are present and in sync, which makes it a cheap guard in a setup script.&lt;/p&gt;

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

&lt;p&gt;Agentic coding has not changed the need for quality gates; it has made them more valuable, because the volume of change goes up while the attention per change goes down. Writing the rules in an instruction file is still worth doing, but it is not enforcement. A Lefthook config is a few dozen lines of YAML that makes the same checks run on every commit and push, whoever - or whatever - wrote the code, and the beta &lt;code&gt;ai:&lt;/code&gt; block extends the same jobs to the agent's own events. Start with one hook: format and lint the staged files, re-stage the fixes, and refuse the commit if the tests fail. In a large codebase, that one gate prevents more bad diffs than any prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://lefthook.dev/" rel="noopener noreferrer"&gt;Lefthook documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lefthook.dev/configuration/" rel="noopener noreferrer"&gt;Configuration reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lefthook.dev/install/" rel="noopener noreferrer"&gt;Installation guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lefthook.dev/configuration/glob_matcher" rel="noopener noreferrer"&gt;&lt;code&gt;glob_matcher&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lefthook.dev/configuration/stage_fixed" rel="noopener noreferrer"&gt;&lt;code&gt;stage_fixed&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lefthook.dev/configuration/ai" rel="noopener noreferrer"&gt;&lt;code&gt;ai&lt;/code&gt; - agent hooks (beta)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/evilmartians/lefthook" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pre-commit.com/" rel="noopener noreferrer"&gt;pre-commit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  My AI coding agent keeps committing code that fails lint and tests. How do I stop it?
&lt;/h3&gt;

&lt;p&gt;Install Git hooks with Lefthook so the checks run before the commit is created. Add a &lt;code&gt;lefthook.yml&lt;/code&gt; with a &lt;code&gt;pre-commit&lt;/code&gt; hook whose jobs run your formatter, linter, type checker, and tests, then run &lt;code&gt;lefthook install&lt;/code&gt; once per clone. If any Lefthook job exits non-zero, the commit is aborted, so code that fails your checks cannot be committed even if an agent ignored the same rules written in an instruction file.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I make Git hooks run automatically after every clone without asking each developer to install them?
&lt;/h3&gt;

&lt;p&gt;Add Lefthook to your project's dependencies - for example &lt;code&gt;pnpm add -D lefthook&lt;/code&gt; or &lt;code&gt;npm install --save-dev lefthook&lt;/code&gt;. The package's &lt;code&gt;postinstall&lt;/code&gt; script runs &lt;code&gt;lefthook install&lt;/code&gt;, which writes the hooks into &lt;code&gt;.git/hooks&lt;/code&gt; for each developer automatically. Note that pnpm blocks dependency build scripts by default, so you must approve Lefthook's build in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;, and a fresh clone can always run &lt;code&gt;lefthook install&lt;/code&gt; manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why did my Lefthook job not run and print "no files for inspection"?
&lt;/h3&gt;

&lt;p&gt;Lefthook's default glob matcher treats &lt;code&gt;**&lt;/code&gt; as one or more directories, so a pattern like &lt;code&gt;src/components/**/*.ts&lt;/code&gt; does not match &lt;code&gt;src/components/Modal.ts&lt;/code&gt; - it matches nothing, and the job is skipped instead of failing. Set &lt;code&gt;glob_matcher: doublestar&lt;/code&gt; in &lt;code&gt;lefthook.yml&lt;/code&gt; so &lt;code&gt;**/&lt;/code&gt; matches zero or more directories, and remember that with &lt;code&gt;doublestar&lt;/code&gt; a bare &lt;code&gt;*.ts&lt;/code&gt; only matches the repository root, so use &lt;code&gt;**/*.ts&lt;/code&gt; for files at any depth.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I make Lefthook commit the files that a formatter fixed?
&lt;/h3&gt;

&lt;p&gt;Set &lt;code&gt;stage_fixed: true&lt;/code&gt; on the Lefthook job. After the job's command rewrites the staged files, Lefthook runs &lt;code&gt;git add&lt;/code&gt; on them, so the corrected content is included in the same commit. &lt;code&gt;stage_fixed&lt;/code&gt; works only on the &lt;code&gt;pre-commit&lt;/code&gt; hook, and jobs that use it should run sequentially rather than in parallel because they modify the Git index.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I skip Git hooks for a single commit?
&lt;/h3&gt;

&lt;p&gt;Yes. Run &lt;code&gt;LEFTHOOK=0 git commit&lt;/code&gt; to disable Lefthook for that one command, or &lt;code&gt;git commit --no-verify&lt;/code&gt; to skip Git hooks entirely. Both are intentional escape hatches; because either can bypass a &lt;code&gt;pre-commit&lt;/code&gt; hook, rules that must always hold should also be enforced in CI by running &lt;code&gt;lefthook run pre-commit --all-files&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I run Lefthook hooks in CI when hooks are a local Git feature?
&lt;/h3&gt;

&lt;p&gt;Install the Lefthook binary in your CI job, then invoke the hook explicitly instead of relying on &lt;code&gt;.git/hooks&lt;/code&gt;: &lt;code&gt;pnpm exec lefthook run pre-commit --all-files&lt;/code&gt;. The &lt;code&gt;--all-files&lt;/code&gt; flag replaces &lt;code&gt;{staged_files}&lt;/code&gt; with every tracked file, since there is no staging area to speak of. Setting &lt;code&gt;CI=true&lt;/code&gt; makes the Lefthook npm package skip its &lt;code&gt;postinstall&lt;/code&gt; hook installation, which is the desired behavior in CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Lefthook work with Claude Code, Cursor, Codex, and GitHub Copilot?
&lt;/h3&gt;

&lt;p&gt;Yes, through the beta &lt;code&gt;ai:&lt;/code&gt; block in &lt;code&gt;lefthook.yml&lt;/code&gt;. You map a provider's event names - for example Claude Code's &lt;code&gt;Stop&lt;/code&gt; or Cursor's &lt;code&gt;stop&lt;/code&gt; - to a Lefthook hook, and &lt;code&gt;lefthook install&lt;/code&gt; generates the provider's settings file so the agent calls &lt;code&gt;lefthook run &amp;lt;hook&amp;gt;&lt;/code&gt; when that event fires. The feature supports &lt;code&gt;claude&lt;/code&gt;, &lt;code&gt;codex&lt;/code&gt;, &lt;code&gt;cursor&lt;/code&gt;, and &lt;code&gt;copilot&lt;/code&gt;, and because it is beta its generated files are worth re-checking after a Lefthook upgrade.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Lefthook faster than pre-commit or Husky?
&lt;/h3&gt;

&lt;p&gt;Lefthook is distributed as a single Go binary with no runtime to load, and it can run a hook's jobs in parallel, so it does not pay the startup cost of a Python framework or a Node process. pre-commit's advantage is its isolated per-language hook environments, and Husky's is familiarity in Node projects, so the choice usually comes down to which model fits your repository rather than raw speed.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>qa</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Achieving Instant Navigations in Next.js 16.3</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Wed, 07 Oct 2026 07:09:06 +0000</pubDate>
      <link>https://dev.to/u11d/achieving-instant-navigations-in-nextjs-163-47ok</link>
      <guid>https://dev.to/u11d/achieving-instant-navigations-in-nextjs-163-47ok</guid>
      <description>&lt;p&gt;Server Components can reduce client-side JavaScript, keep sensitive logic on the server, and prevent data-fetching waterfalls. But they also introduced a frustrating user experience in many dynamic applications: after clicking a link, the user sometimes had to wait for the server before seeing the next screen.&lt;/p&gt;

&lt;p&gt;Next.js 16.3 addresses this problem with Instant Navigations, an opt-in set of rendering, prefetching, development, and testing tools. The goal is not to make every data request finish immediately. It is to make sure the application always has meaningful UI ready when navigation begins.&lt;/p&gt;

&lt;p&gt;This is particularly useful for e-commerce platforms, dashboards, and account areas. These applications cannot prerender every personalized state at build time, but they should still respond to clicks without a visible pause.&lt;/p&gt;

&lt;p&gt;In this guide, I will explain how Instant Navigations work, how to choose between streaming and caching, and how to protect navigation performance with Playwright tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Dynamic Next.js Navigations Can Feel Slow
&lt;/h2&gt;

&lt;p&gt;In a traditional server-driven navigation, the sequence often looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user clicks a link.&lt;/li&gt;
&lt;li&gt;The browser requests the target route from the server.&lt;/li&gt;
&lt;li&gt;The server reads data and renders the route.&lt;/li&gt;
&lt;li&gt;The browser finally displays the next UI.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The architecture can be efficient, but the delay between the first and fourth steps is visible. A client-driven SPA usually feels more responsive because it already has enough code in the browser to display the next screen or its loading state immediately.&lt;/p&gt;

&lt;p&gt;Before Next.js 16.3, teams had two common ways to reduce this delay:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prerender the full page at build time, which does not work for every dynamic or personalized route&lt;/li&gt;
&lt;li&gt;fully prefetch each destination with &lt;code&gt;&amp;lt;Link prefetch={true}&amp;gt;&lt;/code&gt;, which can generate unnecessary requests and increase server load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;code&gt;loading.tsx&lt;/code&gt; file could provide an immediate loading state, but it was easy to forget and applied at the route-segment level rather than around the exact data dependency.&lt;/p&gt;

&lt;p&gt;Instant Navigations provide a more precise model. Instead of asking whether the whole route is static or dynamic, you decide which parts can be ready before the click and which parts should arrive later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling Instant Navigations in Next.js 16.3
&lt;/h2&gt;

&lt;p&gt;Instant Navigations are opt-in in Next.js 16.3. Enable Cache Components and Partial Prefetching in &lt;code&gt;next.config.ts&lt;/code&gt;:&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="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;NextConfig&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&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;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cacheComponents&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="na"&gt;partialPrefetching&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cacheComponents&lt;/code&gt; enables the explicit caching model built around &lt;code&gt;'use cache'&lt;/code&gt;, &lt;code&gt;cacheLife&lt;/code&gt;, and &lt;code&gt;cacheTag&lt;/code&gt;. It also allows routes to combine prerendered UI with dynamic content behind Suspense boundaries.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;partialPrefetching&lt;/code&gt; changes how Next.js prepares routes on the client. Instead of treating prefetching as an all-or-nothing decision, Next.js can prefetch only the reusable UI needed to start a navigation.&lt;/p&gt;

&lt;p&gt;Enabling&amp;nbsp;&lt;code&gt;cacheComponents&lt;/code&gt;&amp;nbsp;is a migration step, not just a performance flag. During production builds, Next.js attempts to prerender as much of a route as possible and can report an error when runtime or uncached data is accessed outside a Suspense boundary. Each case must be streamed behind Suspense, cached with&amp;nbsp;&lt;code&gt;'use cache'&lt;/code&gt;&amp;nbsp;when appropriate, or explicitly allowed to block with&amp;nbsp;&lt;code&gt;export const instant = false&lt;/code&gt;. Existing applications may also need to remove incompatible route segment configuration such as&amp;nbsp;&lt;code&gt;dynamic&lt;/code&gt;,&amp;nbsp;&lt;code&gt;revalidate&lt;/code&gt;, or&amp;nbsp;&lt;code&gt;fetchCache&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These behaviors are planned to become defaults in a future major version. In 16.3, enabling them explicitly gives teams time to review their asynchronous boundaries and migration requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Model: Stream, Cache, or Block
&lt;/h2&gt;

&lt;p&gt;When a route depends on asynchronous data, Next.js 16.3 asks you to make the intended behavior explicit. There are three useful choices.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Stream dynamic content with Suspense
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; when the data must stay fresh or depends on the active request. The fallback becomes part of the route shell, so it can appear immediately while the server renders the final content.&lt;/p&gt;

&lt;p&gt;This is a good fit for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inventory and delivery estimates&lt;/li&gt;
&lt;li&gt;cart state&lt;/li&gt;
&lt;li&gt;account-specific data&lt;/li&gt;
&lt;li&gt;live analytics&lt;/li&gt;
&lt;li&gt;personalized recommendations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The navigation is instant because the user sees the fallback without waiting for the dynamic operation. The final data still arrives from the server later.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Cache reusable content with &lt;code&gt;'use cache'&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;'use cache'&lt;/code&gt; when the result can be safely reused. Cached UI can be included in the prefetched response and displayed immediately during navigation.&lt;/p&gt;

&lt;p&gt;This works well for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;product descriptions&lt;/li&gt;
&lt;li&gt;category content&lt;/li&gt;
&lt;li&gt;CMS blocks&lt;/li&gt;
&lt;li&gt;public pricing shared by a region&lt;/li&gt;
&lt;li&gt;configuration that changes infrequently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cache boundary should follow the business meaning of the data. Content that is safe to share globally should not be mixed with customer-specific prices or session state.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Allow the route to block
&lt;/h3&gt;

&lt;p&gt;Not every route needs to navigate instantly. If displaying a loading state would be misleading and the route should wait for complete server output, mark that decision explicitly:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;instant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can be appropriate for a simple content page where the team prefers to show the complete article at once, or for a flow where a partial state would create confusion.&lt;/p&gt;

&lt;p&gt;The important change is that blocking becomes an intentional route decision rather than an accidental result of one unresolved data dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Partial Prefetching Changes the Network Cost
&lt;/h2&gt;

&lt;p&gt;The previous prefetching model could issue a request for every visible link. Consider an account sidebar with twenty links to &lt;code&gt;/orders/[id]&lt;/code&gt;. Even though all links share the same route structure, the browser could prefetch each destination separately.&lt;/p&gt;

&lt;p&gt;With Partial Prefetching, Next.js can generate one reusable shell for &lt;code&gt;/orders/[id]&lt;/code&gt;, store it in the browser, and use it for multiple links during the session. This is closer to route-level code splitting in a SPA: the browser receives enough UI to react immediately, but it does not need the complete data for every possible destination.&lt;/p&gt;

&lt;p&gt;If a particular link deserves a deeper prefetch, you can still opt in:&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`/products/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;prefetch&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="si"&gt;}&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;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&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;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next.js then prerenders as much of that destination as it safely can. Synchronous values from the URL and content marked with &lt;code&gt;'use cache'&lt;/code&gt; can be included, while request-bound work remains behind its Suspense boundary.&lt;/p&gt;

&lt;p&gt;This avoids a common tradeoff in dynamic applications: either prefetch nothing and make the user wait, or prefetch complete pages and make the server do work that may never be used.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical E-Commerce Example
&lt;/h2&gt;

&lt;p&gt;A product page usually combines data with very different freshness requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;product copy and media are broadly reusable&lt;/li&gt;
&lt;li&gt;inventory changes often&lt;/li&gt;
&lt;li&gt;customer pricing may depend on identity, region, or sales channel&lt;/li&gt;
&lt;li&gt;the cart belongs to the current session&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Making the whole page dynamic means one slow operation can block navigation. Caching the whole page is more dangerous because it can expose stale or private data.&lt;/p&gt;

&lt;p&gt;A better boundary looks like this:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&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;cacheLife&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cacheTag&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/cache&lt;/span&gt;&lt;span class="dl"&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;ProductSummary&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;slug&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

  &lt;span class="nf"&gt;cacheTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`product:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&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="nf"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hours&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;product&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;getProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&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;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;slug&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;const&lt;/span&gt; &lt;span class="nx"&gt;inventory&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;getLiveInventory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; items available&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;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;ProductPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;PageProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/products/[slug]&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&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;params&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProductSummary&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Checking availability...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Inventory&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The product summary can be cached and reused. Live inventory remains dynamic, but it no longer prevents the route from showing useful content. During navigation, the browser can display the product UI or its prepared shell first and fill in availability when the server responds.&lt;/p&gt;

&lt;p&gt;For a real storefront, customer-specific pricing should follow the same principle as inventory: isolate it in a private or request-time boundary instead of adding cookies or session access to the entire page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding Slow Routes with Instant Insights
&lt;/h2&gt;

&lt;p&gt;Performance problems are difficult to fix when they only appear as a vague feeling that the application is slow. Next.js 16.3 adds Instant Insights to the development tools to make the problem visible.&lt;/p&gt;

&lt;p&gt;When you encounter a non-instant navigation during development, Instant Insights identifies the route and points to the asynchronous work that blocks its shell. In most cases, the fix is one of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;move request-specific work below a Suspense boundary&lt;/li&gt;
&lt;li&gt;mark reusable work with &lt;code&gt;'use cache'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;move a blocking dependency out of a shared layout&lt;/li&gt;
&lt;li&gt;explicitly accept blocking with &lt;code&gt;export const instant = false&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Navigation Inspector complements this by pausing navigation at the prefetched shell. This lets you inspect exactly what the user can see before dynamic content arrives. It is useful because production prefetching is normally disabled in development, where loading behavior can otherwise be difficult to reproduce accurately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Instant Navigations with Playwright
&lt;/h2&gt;

&lt;p&gt;A route that feels instant today can become blocking after a small refactor. A new &lt;code&gt;cookies()&lt;/code&gt; call in a shared component or a moved Suspense boundary may be enough to change the result.&lt;/p&gt;

&lt;p&gt;Next.js 16.3 includes the &lt;code&gt;instant()&lt;/code&gt; helper in &lt;code&gt;@next/playwright&lt;/code&gt;. It temporarily holds back dynamic content, allowing the test to assert which UI is available without waiting for the network:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;test&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;@playwright/test&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;instant&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/playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;product navigation shows useful UI instantly&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&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;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/products/shoes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;instant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&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[href="/products/hats"]&lt;/span&gt;&lt;span class="dl"&gt;'&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForURL&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/products/hats&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;h1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toContainText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Baseball Cap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Checking availability...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/items available/&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&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;The test describes a user-facing contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the product title must be available immediately&lt;/li&gt;
&lt;li&gt;the inventory fallback must be visible while live data is pending&lt;/li&gt;
&lt;li&gt;the final inventory must appear after the dynamic work completes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is more useful than testing an arbitrary duration such as 100 milliseconds. Network and CI timing can vary, but the boundary between prefetched UI and network-dependent UI is deterministic.&lt;/p&gt;

&lt;p&gt;The v0 team used this property to improve real routes with a coding agent. The agent wrote a failing &lt;code&gt;instant()&lt;/code&gt; test, moved or cached the blocking work, rebuilt the application, and repeated the process until the test passed. The resulting 16 tests were kept in CI to prevent future regressions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Teams Usually Get It Wrong
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: Treating “instant” as “fully loaded”
&lt;/h3&gt;

&lt;p&gt;Instant Navigations do not make databases or APIs respond with zero latency. They guarantee that useful UI can respond to the click without waiting for those systems.&lt;/p&gt;

&lt;p&gt;The distinction matters. A complete page may still stream in over time, but the application should never look unresponsive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: Placing Suspense too high in the tree
&lt;/h3&gt;

&lt;p&gt;Wrapping an entire page in one Suspense boundary may technically make navigation instant, but the user will see a generic full-page skeleton. Place boundaries close to the data that actually needs to wait so stable navigation, headings, and controls remain visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: Reading dynamic data in a shared layout
&lt;/h3&gt;

&lt;p&gt;A session or request-dependent operation near the root can block many routes at once. Keep shared layouts focused on reusable UI and move dynamic reads into the components that need them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 4: Enabling full prefetching on every link
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;prefetch={true}&lt;/code&gt; is still useful, but it should represent a deliberate choice to prepare more than the route shell. Applying it everywhere can recreate the server and network cost that Partial Prefetching is designed to avoid.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 5: Using meaningless loading states
&lt;/h3&gt;

&lt;p&gt;An instant blank rectangle is not a good user experience. The shell should preserve the layout and provide enough context for the user to understand where the navigation is going.&lt;/p&gt;

&lt;p&gt;For a product page, that may include the title, gallery frame, price area, and an inventory placeholder. For an account page, it may include the navigation, page heading, and form structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Adoption Strategy
&lt;/h2&gt;

&lt;p&gt;For an existing Next.js application, avoid changing every route at once. Start with the journeys where a delay has the largest user or business impact.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable &lt;code&gt;cacheComponents&lt;/code&gt; and &lt;code&gt;partialPrefetching&lt;/code&gt; in a branch.&lt;/li&gt;
&lt;li&gt;Use Instant Insights to identify blocking navigations.&lt;/li&gt;
&lt;li&gt;Choose one important journey, such as category to product or cart to checkout.&lt;/li&gt;
&lt;li&gt;Write a failing test with &lt;code&gt;instant()&lt;/code&gt; that describes the minimum UI required after the click.&lt;/li&gt;
&lt;li&gt;Stream request-bound data and cache only content that is safe to reuse.&lt;/li&gt;
&lt;li&gt;Inspect the shell with Navigation Inspector.&lt;/li&gt;
&lt;li&gt;Keep the test in CI after the route passes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This process turns perceived performance into an architectural requirement that can be reviewed and tested. It also creates a clear stopping point: the work is complete when the intended shell is visible without network-dependent content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The most important part of Instant Navigations is not another prefetch option. It is the clearer boundary between UI that must be ready for a click and data that is allowed to arrive later.&lt;/p&gt;

&lt;p&gt;For dynamic applications, this removes the need to choose between a server-driven architecture and SPA-like responsiveness. Server Components can continue to handle rendering and sensitive data, while Suspense, Cache Components, and reusable route shells keep navigation responsive.&lt;/p&gt;

&lt;p&gt;The best result comes from combining three decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stream data that must remain dynamic&lt;/li&gt;
&lt;li&gt;cache content that is genuinely reusable&lt;/li&gt;
&lt;li&gt;block only when waiting is an intentional product choice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once those decisions are protected by &lt;code&gt;instant()&lt;/code&gt; tests, navigation performance stops being something that can disappear unnoticed during the next refactor.&lt;/p&gt;

</description>
      <category>nextjs</category>
    </item>
    <item>
      <title>WooCommerce, Shopify, or Medusa: Choosing an E-Commerce Platform for Bullion Dealers</title>
      <dc:creator>Michał Miler</dc:creator>
      <pubDate>Wed, 30 Sep 2026 09:01:15 +0000</pubDate>
      <link>https://dev.to/u11d/woocommerce-shopify-or-medusa-choosing-an-e-commerce-platform-for-bullion-dealers-4mi7</link>
      <guid>https://dev.to/u11d/woocommerce-shopify-or-medusa-choosing-an-e-commerce-platform-for-bullion-dealers-4mi7</guid>
      <description>&lt;h1&gt;
  
  
  WooCommerce vs. Shopify vs. Medusa: Choosing an E-Commerce Platform for Bullion Dealers
&lt;/h1&gt;

&lt;p&gt;A bullion dealer's product catalog doesn't have prices. It has formulas. A 1 oz gold coin isn't "$2,100" the way a t-shirt is "$25" — it's &lt;code&gt;weight × today's spot price × your spread&lt;/code&gt;, plus whatever fixed minting or handling premium you charge on top. Spot prices for gold and silver move throughout the trading day, sometimes several times a minute, which means the "correct" price for every SKU in the catalog is technically wrong again a few seconds after you last calculated it.&lt;/p&gt;

&lt;p&gt;Most e-commerce platforms were not built around that assumption. They were built around the idea that a product has a price, someone sets it, and it stays put until someone changes it again. That's a completely reasonable default for almost every category of goods sold online — until the goods themselves are priced off a market that never stops moving.&lt;/p&gt;

&lt;p&gt;This creates a specific set of requirements that a general-purpose store doesn't have to think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live pricing tied to a market feed&lt;/strong&gt;, not a value someone typed into an admin panel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A pricing formula per product or product line&lt;/strong&gt; — spread, fixed premiums, percentage premiums — layered on top of that feed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frequent updates&lt;/strong&gt;, ideally continuous during market hours, not a batch job that runs once an hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkout price locking&lt;/strong&gt; — the price a customer sees when they start checkout has to hold for a defined window, or the order has to be rejected, otherwise either the dealer or the customer eats the difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability when the feed itself fails&lt;/strong&gt; — a stale or missing spot price can't silently turn into a wrong price or a broken checkout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrations&lt;/strong&gt; with ERP systems, inventory/vaulting software, and payment processors that are comfortable underwriting a high-value, high-risk category.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Room to customize&lt;/strong&gt; the pricing logic as the business's spreads, premium structure, or product mix changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WooCommerce, Shopify, and Medusa can all run a bullion store. The difference is how much of the above you get by default, how much you build yourself, and how much friction you hit doing it. This article compares all three specifically against that list — not as a generic "which platform is better" question, but as the specific decision a bullion or precious-metals dealer is actually facing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;WooCommerce&lt;/th&gt;
&lt;th&gt;Shopify&lt;/th&gt;
&lt;th&gt;Medusa&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Live spot price → product price&lt;/td&gt;
&lt;td&gt;Via third-party plugins (e.g. real-time precious-metals pricing plugins); price is written to the product record&lt;/td&gt;
&lt;td&gt;Not native — price is a stored variant field; apps push updates via the Admin API on a schedule, not per-request&lt;/td&gt;
&lt;td&gt;Not native either, but the modular architecture is built for computing a price at read time instead of storing one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Formula-based premiums/spreads&lt;/td&gt;
&lt;td&gt;Supported by dedicated bullion pricing plugins&lt;/td&gt;
&lt;td&gt;Requires a custom app or third-party pricing app; no first-class concept of spot + spread + premium&lt;/td&gt;
&lt;td&gt;Add a custom pricing module (or use an existing one, like Fluctum) that applies the formula&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time storefront price updates (no reload)&lt;/td&gt;
&lt;td&gt;Not native; needs custom JS polling on top of a pricing plugin&lt;/td&gt;
&lt;td&gt;Not native; storefront reflects whatever price was last written by the app&lt;/td&gt;
&lt;td&gt;Not native to Medusa core; a pricing module can add it (Fluctum uses Server-Sent Events)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout price locking / quote expiry&lt;/td&gt;
&lt;td&gt;No built-in concept — cart totals recalculate from the current stored price on every load; locking is custom dev work&lt;/td&gt;
&lt;td&gt;No built-in concept; Shopify Functions customize discounts, delivery, and validation, not a live commodity price&lt;/td&gt;
&lt;td&gt;No built-in concept in core Medusa; a module can add a lock-and-validate step (Fluctum does this)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behavior on feed outage&lt;/td&gt;
&lt;td&gt;Depends entirely on the plugin&lt;/td&gt;
&lt;td&gt;Depends entirely on the app&lt;/td&gt;
&lt;td&gt;Depends entirely on the module — a well-built one falls back to the last known price&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiered / wholesale pricing&lt;/td&gt;
&lt;td&gt;Needs a wholesale/dynamic-pricing plugin (e.g. Wholesale Customers For WooCommerce)&lt;/td&gt;
&lt;td&gt;Native volume pricing and quantity rules exist, but the more advanced B2B catalog features are Shopify Plus-only&lt;/td&gt;
&lt;td&gt;Native in core — quantity tiers and customer-group price rules, no plugin or plan gate required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extending with new integrations (tax, ERP, payments)&lt;/td&gt;
&lt;td&gt;Full PHP access, thousands of hooks/filters, huge plugin ecosystem&lt;/td&gt;
&lt;td&gt;Public apps run on any plan; custom apps with Shopify Functions require Shopify Plus&lt;/td&gt;
&lt;td&gt;Modules → workflows → provider interfaces plugin; extend without forking, no plan gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting model&lt;/td&gt;
&lt;td&gt;Self-hosted (or managed WordPress hosting); you own ops&lt;/td&gt;
&lt;td&gt;Fully managed by Shopify&lt;/td&gt;
&lt;td&gt;Self-hosted or Medusa Cloud (managed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction fees &amp;amp; monthly cost&lt;/td&gt;
&lt;td&gt;Gateway fees (~2.9% + 30¢) + hosting &amp;amp; plugin costs. &lt;strong&gt;No platform fees on GMV.&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Shopify Plus plan (&lt;strong&gt;$2,000+/mo&lt;/strong&gt;) required for custom checkout logic, plus a &lt;strong&gt;0.15% platform fee&lt;/strong&gt; on third-party gateways.&lt;/td&gt;
&lt;td&gt;Gateway fees only. &lt;strong&gt;No platform fees on GMV.&lt;/strong&gt; Costs are hosting (self-hosted or Medusa Cloud) and development.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Smaller catalog, WordPress-based stack already in place, moderate technical budget&lt;/td&gt;
&lt;td&gt;Fast launch, managed infrastructure, simpler or less frequent pricing updates&lt;/td&gt;
&lt;td&gt;Dealers who need real-time pricing plus specific ERP, payment, or wholesale requirements built once and owned outright&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why Bullion E-Commerce Doesn't Behave Like a Normal Store
&lt;/h2&gt;

&lt;p&gt;Before comparing platforms, it's worth being precise about what's actually different, because it's easy to under-scope this as "just update the price more often."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The price is derived, not entered.&lt;/strong&gt; A normal product's price is a fact you record. A bullion product's price is the output of a calculation: current spot price for that metal, times the product's weight, times a spread factor, plus any fixed or percentage premium (minting costs, rarity, packaging). Change the spot price and every affected SKU's correct price changes with it — instantly, in principle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update frequency is high and continuous.&lt;/strong&gt; Gold and silver spot prices move throughout market hours. A store that only updates prices a few times a day is either giving away margin between updates (selling below the current market) or overcharging (selling above it) — and both outcomes cost the dealer customers over time, just in different ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkout needs a quote, not a live price.&lt;/strong&gt; A customer needs the price to hold still for the few minutes it takes to fill in an address and pay. If the price is still tied to a live feed while payment is being processed, the amount charged can differ from the amount the customer agreed to. The standard fix is a "lock": freeze the computed price the moment checkout starts, attach an expiry to it, and refuse to complete the order if that lock has expired by the time payment is submitted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A feed outage can't become a wrong price.&lt;/strong&gt; If the spot-price provider goes down for ten minutes, the store has to keep functioning — showing the last known good price and continuing to accept orders — rather than either breaking checkout or, worse, silently pricing products at $0 or a stale multi-day-old value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The integration surface is wider than checkout.&lt;/strong&gt; Bullion dealers commonly need to sync with an ERP or inventory/vaulting system, and payment processing for precious metals is frequently treated as a reviewed or higher-risk category by processors — worth confirming with your payment provider early, regardless of which platform you choose.&lt;/p&gt;

&lt;p&gt;None of this is exotic engineering. But it's also not something any of the three platforms below solves out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  WooCommerce for Bullion Dealers
&lt;/h2&gt;

&lt;p&gt;WooCommerce's strength here is exactly its strength everywhere else: it's open PHP, self-hosted, and has an enormous plugin ecosystem, so there's a real, mature market of niche plugins for this exact use case. Plugins like &lt;a href="https://wordpress.org/plugins/precious-metals-automated-product-pricing-pro/" rel="noopener noreferrer"&gt;Precious Metals Product Prices for WooCommerce&lt;/a&gt; advertise real-time precious-metals pricing, product sync, and wholesale drop-ship order automation specifically for bullion and coin stores, and simpler options like &lt;a href="https://wordpress.org/plugins/gold-price-based-on-weight/" rel="noopener noreferrer"&gt;Gold Price Based on Weight&lt;/a&gt; calculate a product's price from a global price-per-gram figure and the product's weight. These plugins genuinely solve the "pull a live price and multiply it out" problem.&lt;/p&gt;

&lt;p&gt;Where WooCommerce runs into friction is checkout behavior. WooCommerce recalculates the cart and checkout totals from each product's &lt;em&gt;currently stored&lt;/em&gt; price every time the cart is loaded — there's no built-in concept of "hold this exact quoted price for the next 10 minutes, then reject the order if it expires." A dealer who wants that has to build it as custom logic on top of one of the pricing plugins: snapshotting the computed price into cart or session data, and validating it again before the order is allowed to complete.&lt;/p&gt;

&lt;p&gt;Real-time storefront updates (a price that visibly ticks upward or downward while a customer is looking at the cart, without a page reload) also aren't native. Even with WooCommerce's newer Cart and Checkout blocks, the built-in reactivity is around cart operations like changing a quantity — it won't push an externally changing spot price into an already-open page without custom JavaScript polling a price endpoint yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WooCommerce is a good fit&lt;/strong&gt; for a smaller dealer already running WordPress, with a catalog that doesn't need sub-minute pricing precision, and a technical budget that can stretch to some custom development for checkout locking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shopify for Bullion Dealers
&lt;/h2&gt;

&lt;p&gt;Shopify's case is speed and operational simplicity: managed hosting, PCI compliance handled for you, a fast path to launch, and by far the largest app ecosystem of the three. For a dealer who wants to be selling quickly and isn't chasing sub-minute pricing precision, that's a legitimate, low-friction starting point.&lt;/p&gt;

&lt;p&gt;The friction shows up in how Shopify models price and checkout. A product variant's price is a stored field, not a computed value — a pricing app updates it by calling the Admin API on a schedule or in response to a webhook, which is fundamentally a batch update loop, not a live per-request formula tied to a streaming feed. Checkout customization today runs through &lt;a href="https://shopify.dev/docs/apps/build/functions" rel="noopener noreferrer"&gt;Shopify Functions&lt;/a&gt;: discount functions, cart/checkout validation functions, delivery and payment-method customization functions, and similar. Every documented Function category is scoped to adjusting discounts, validation, or checkout behavior &lt;em&gt;around&lt;/em&gt; an existing price — none of them is designed to recompute a commodity-linked unit price from a live formula on the fly.&lt;/p&gt;

&lt;p&gt;There's also a plan gate to be aware of: per Shopify's own documentation, stores on any plan can install a public app from the Shopify App Store that happens to contain Functions, but building and running a &lt;em&gt;custom&lt;/em&gt; app with Shopify Function APIs requires a Shopify Plus plan. That matters for a dealer planning to build bespoke pricing logic in-house rather than relying entirely on a third-party app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shopify is a good fit&lt;/strong&gt; for a dealer prioritizing managed infrastructure and fast launch, where pricing updates on the order of minutes-via-API (rather than a continuous live feed) and standard discount/checkout customization are acceptable trade-offs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Medusa's Extensibility: Building Only the Part That's Actually Custom
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://medusajs.com" rel="noopener noreferrer"&gt;Medusa&lt;/a&gt; doesn't ship dynamic commodity pricing, ERP connectors, or a high-risk-friendly payment processor out of the box — no platform in this comparison does. The difference is what it costs to add them. Medusa's core is organized as modules (self-contained data and logic for one domain — pricing, tax, payments, inventory), workflows (business logic composed of retryable, rollback-safe steps), and module links (which connect data across modules without collapsing everything into one table). That structure means a dealer-specific requirement gets built as an addition that plugs into Medusa's existing provider interfaces and event system, not as a fork of the platform or a rebuild of checkout, promotions, multi-region support, or order management.&lt;/p&gt;

&lt;p&gt;This isn't a theoretical benefit. u11d's own &lt;a href="https://www.npmjs.com/package/@u11d/medusa-avalara" rel="noopener noreferrer"&gt;@u11d/medusa-avalara&lt;/a&gt; plugin is a working example of the same pattern applied to tax instead of pricing. It installs into an existing Medusa store, registers itself through Medusa's tax-provider interface, and takes over the full transaction lifecycle: it creates a sales invoice in Avalara when an order is placed, commits it when the order completes, and voids it if the order is canceled — all without any change to Medusa's own cart, checkout, or order code. A dealer picks Avalara as their tax provider in the Medusa admin the same way they'd pick any other provider, and everything else in the store keeps working exactly as it did before.&lt;/p&gt;

&lt;p&gt;The same pattern generalizes to the other systems a bullion dealer actually needs to connect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ERP and vaulting/inventory sync.&lt;/strong&gt; Medusa emits events for order placement, fulfillment, and inventory changes. A module can subscribe to those events, and a workflow can push the relevant data out to an ERP or a vaulting/inventory system — the integration lives in its own module, not scattered across checkout code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment processors.&lt;/strong&gt; Medusa's payment system is provider-based, with Stripe, PayPal, Klarna, and Adyen supported out of the box. Bullion dealers often need a processor comfortable underwriting a high-value, high-risk category rather than a default consumer gateway — adding one means implementing Medusa's payment provider interface, not replacing the checkout flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiered and wholesale pricing.&lt;/strong&gt; This one doesn't even require an add-on: Medusa's core Pricing Module natively supports quantity-based tiers (a lower unit price once a customer buys 10+ or 50+ ounces) and rule-based pricing by customer group, region, or cart total — genuinely useful for a dealer serving both retail customers and wholesale/dealer accounts from the same catalog.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Market-driven pricing.&lt;/strong&gt; The same module pattern is what lets a plugin compute a product's final price at read time from a live spot price plus a merchant-configured formula, without ever writing that value into Medusa's own price records — covered in detail below with Fluctum.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this requires forking Medusa or building a bullion-specific commerce platform from zero. It's the difference between building the tax logic, the ERP sync, the payment integration, and the pricing engine all from scratch, and building only the one piece that's actually specific to the business on top of a platform that already handles everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Checkout Price-Locking Problem
&lt;/h2&gt;

&lt;p&gt;Across all three platforms, this is the piece that consistently doesn't come for free: freezing a computed price at the moment a customer starts checkout, and refusing to let the order complete if that lock has expired. It's not a hard problem conceptually, but getting it right requires the platform to support:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Recording the exact price, and the inputs that produced it (spot price, spread, premium), at the moment checkout begins.&lt;/li&gt;
&lt;li&gt;An expiry window on that record.&lt;/li&gt;
&lt;li&gt;A hard check, at order completion, that rejects the order if the lock is missing or expired — not just a soft warning.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;WooCommerce and Shopify both leave this entirely to custom development or a third-party app's own implementation of it. Neither has a first-class "price lock" primitive in its core commerce model. This is precisely the gap Fluctum was built to close on Medusa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fluctum: Market-Driven Dynamic Pricing for Medusa
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.fluctum.io/" rel="noopener noreferrer"&gt;Fluctum&lt;/a&gt; is an open-source, MIT-licensed Medusa plugin built by u11d that implements the full pricing pipeline described above, so a Medusa store doesn't need a custom-built pricing engine to sell spot-priced goods.&lt;/p&gt;

&lt;p&gt;It fetches live spot prices on a schedule, and computes each product's final price from the current spot price, the product's weight, and a merchant-configured formula — a spread factor, a fixed spread fee, a percentage premium, and a fixed premium (for minting or handling costs, for example) — without ever writing that computed value back into a Medusa product's own price fields. The storefront reflects price changes in real time via Server-Sent Events, so a customer watching the cart sees the price move without a page reload.&lt;/p&gt;

&lt;p&gt;At checkout, Fluctum locks the computed price into an auditable record and gives it an expiry. If a customer completes payment before that lock expires, the order proceeds at the locked price. If the lock has expired — because checkout took too long, or the price moved and wasn't refreshed — order completion is rejected rather than silently charging a stale amount. If the spot-price feed itself goes down, the storefront and checkout keep working off the last known good price instead of breaking.&lt;/p&gt;

&lt;p&gt;Because it's a Medusa plugin rather than a separate system, installing Fluctum on an existing Medusa store keeps everything else Medusa already provides — multi-region and multi-currency support, promotions, customer accounts, taxes, shipping, and payments — and adds market-driven pricing on top of it. It's free to use and, being open-source, can be extended by any developer to sync with an ERP, add payment providers, or add business logic specific to a given dealer.&lt;/p&gt;

&lt;p&gt;We've written about Fluctum from two angles: a &lt;a href="https://u11d.com/blog/fluctum-open-source-dynamic-pricing/" rel="noopener noreferrer"&gt;business-facing overview of the build-vs-buy decision&lt;/a&gt; for store owners deciding whether to build this themselves, and a &lt;a href="https://u11d.com/blog/medusa-live-pricing-fluctum-developer-guide/" rel="noopener noreferrer"&gt;developer guide&lt;/a&gt; covering the pricing formula, the price-lock lifecycle, and a full local setup walkthrough. A live demo is available at &lt;a href="https://fluctum.medusajs.site" rel="noopener noreferrer"&gt;fluctum.medusajs.site&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Comparison
&lt;/h2&gt;

&lt;p&gt;A full cost-of-ownership model for a bullion dealer is more than just the monthly subscription fee. It's a mix of platform fees, transaction fees as a percentage of gross merchandise value (GMV), app/plugin costs, and hosting or infrastructure overhead.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;WooCommerce:&lt;/strong&gt; The core software is free, so there's no monthly platform fee. Costs are a combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hosting:&lt;/strong&gt; A reliable WordPress host capable of handling e-commerce can range from &lt;strong&gt;$25 to $100+ per month&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Fees:&lt;/strong&gt; These are charged by your payment gateway (e.g., WooCommerce Payments, Stripe, PayPal) and are typically around &lt;strong&gt;2.9% + $0.30 per transaction&lt;/strong&gt; for US cards. These are often negotiable for high-volume merchants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin Costs:&lt;/strong&gt; A robust "Pro" version of a precious metals pricing plugin is essential and can cost &lt;strong&gt;$100 - $300 per year&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational Overhead:&lt;/strong&gt; You are responsible for all maintenance, security, and updates.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Shopify:&lt;/strong&gt; Pricing is based on a monthly subscription, but the need for custom checkout logic creates a significant cost hurdle.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monthly Subscription:&lt;/strong&gt; To implement the required checkout customizations for live pricing, a dealer must use the &lt;strong&gt;Shopify Plus plan, which starts at $2,000/month&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Fees:&lt;/strong&gt; If you don't use Shopify Payments, Shopify adds its own fee on top of your payment gateway's charge. This fee is &lt;strong&gt;0.15% on Shopify Plus&lt;/strong&gt;—a direct tax on your GMV.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App Costs:&lt;/strong&gt; You'll need a third-party app for the pricing logic itself, which is an additional monthly cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Development Costs:&lt;/strong&gt; You'll need to hire developers to build a custom app using Shopify Functions to handle the live pricing at checkout.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Medusa:&lt;/strong&gt; As an open-source framework, Medusa has no platform or transaction fees, offering the most control over your cost structure.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hosting:&lt;/strong&gt; You can self-host on services like AWS or Vercel (typically &lt;strong&gt;$50 - $200+ per month&lt;/strong&gt; depending on scale) or use &lt;strong&gt;Medusa Cloud&lt;/strong&gt; (plans start from $29/month and scale up).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Fees:&lt;/strong&gt; You only pay the fees from your chosen payment processor (e.g., Stripe), with &lt;strong&gt;no additional platform tax on GMV&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Development Costs:&lt;/strong&gt; This is the primary investment. You need development resources to build and maintain the live pricing logic, integrations, and storefront. However, you only build the parts that are unique to your business, leveraging Medusa's core commerce features for the rest. Fluctum can significantly reduce this initial development cost.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which Platform Should You Choose?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If your catalog is small and your pricing updates a handful of times a day is acceptable&lt;/strong&gt;, WooCommerce or Shopify are both legitimate, lower-friction choices — pick based on whether you'd rather own a WordPress-based stack (WooCommerce) or hand hosting and PCI compliance to a managed platform (Shopify).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you need pricing that genuinely tracks the live market, or you know you'll need to connect a specific ERP, tax provider, payment processor, or wholesale pricing model beyond what a plugin already covers&lt;/strong&gt;, that combination pushes hard against how WooCommerce and Shopify are built, and it's the situation Medusa's modular architecture was built to accommodate. The same module/workflow/provider pattern that lets Fluctum add live pricing and u11d's own Avalara plugin handle tax automation is available for whatever a dealer needs to connect next — a dealer in that situation doesn't have to build each of those pieces from scratch.&lt;/p&gt;

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

&lt;p&gt;For a bullion dealer, the choice between WooCommerce, Shopify, and Medusa is a decision about where you want to spend your money and how much control you need.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shopify&lt;/strong&gt; is the highest-cost option due to the non-negotiable requirement of the &lt;strong&gt;Shopify Plus plan ($2,000+/month)&lt;/strong&gt; to achieve true dynamic pricing at checkout. You're paying a premium for managed infrastructure, but also a platform tax on every transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WooCommerce&lt;/strong&gt; offers a much lower entry cost, with no monthly platform fees. It's a viable option if your pricing logic is relatively simple and you have the technical resources to manage a WordPress/WooCommerce stack securely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medusa&lt;/strong&gt; presents the lowest long-term platform cost, with &lt;strong&gt;zero platform fees on your GMV&lt;/strong&gt;. The investment is in development, giving you complete ownership and control over the critical pricing and checkout logic that defines your business. For a serious dealer planning for scale, avoiding a platform tax on a high-GMV business is a significant financial advantage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Medusa's modular architecture is what makes this control possible and cost-effective. The same pattern that lets Fluctum add live spot pricing is what lets u11d's own Avalara plugin automate tax, and it's the same pattern available for any other integration a dealer needs. u11d builds and maintains these open-source tools and works with bullion dealers on their Medusa storefronts precisely because of this flexibility. If live market pricing and long-term cost of ownership are your primary concerns, that's the problem Medusa, and by extension Fluctum, is built to solve.&lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>woocommerce</category>
      <category>shopify</category>
      <category>medusa</category>
    </item>
    <item>
      <title>MinIO Is Deprecated: Best Alternatives for S3-Compatible Storage</title>
      <dc:creator>Bartek Gałęzowski</dc:creator>
      <pubDate>Mon, 28 Sep 2026 10:16:42 +0000</pubDate>
      <link>https://dev.to/u11d/minio-is-deprecated-best-alternatives-for-s3-compatible-storage-2g4j</link>
      <guid>https://dev.to/u11d/minio-is-deprecated-best-alternatives-for-s3-compatible-storage-2g4j</guid>
      <description>&lt;p&gt;For years, MinIO was the obvious answer whenever an engineering team needed S3-compatible object storage outside AWS. You wanted an S3-compatible API on Kubernetes? You needed object storage for backups? You wanted something you could run locally with Docker Compose? The answer was the same - MinIO.&lt;/p&gt;

&lt;p&gt;That recommendation has changed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In 2026, the open-source MinIO project is no longer a maintained project in the way engineers historically expected it to be. The upstream GitHub repository was archived on April 25, 2026, and its README explicitly states that the repository is no longer maintained.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That raises an obvious question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What should replace MinIO?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There isn't a single answer. Three projects are particularly worth looking at: &lt;strong&gt;Garage, RustFS, and SeaweedFS&lt;/strong&gt;. All three provide S3-compatible storage, but they are built around quite different ideas. Garage focuses heavily on decentralized and geographically distributed object storage, RustFS is particularly interesting for engineers looking for something close to the MinIO model, while SeaweedFS takes a much broader approach and combines object storage with distributed file storage and other storage interfaces.&lt;/p&gt;

&lt;p&gt;The important thing is therefore not simply to find "the next MinIO", but to understand what each project is actually designed to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened to MinIO?
&lt;/h2&gt;

&lt;p&gt;The MinIO repository on GitHub is now archived and read-only. Its current README states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"THIS REPOSITORY IS NO LONGER MAINTAINED."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For any production engineer, this should immediately raise a question: what are the alternatives? But let's be realistic. Thousands of projects are still running MinIO, and many of them are running perfectly fine on the latest version they deployed. A running MinIO cluster does not magically stop serving objects because the GitHub repository was archived. If your cluster is healthy and you already have the required binaries or container images, it can continue operating. The bigger problem is the &lt;strong&gt;future lifecycle&lt;/strong&gt; of the installation.&lt;/p&gt;

&lt;p&gt;In September 2026, the &lt;code&gt;minio/minio&lt;/code&gt; repository on Docker Hub was archived, and users started reporting that both &lt;code&gt;minio/minio&lt;/code&gt; and &lt;code&gt;minio/mc&lt;/code&gt; could no longer be pulled from Docker Hub. This is exactly the kind of failure that makes infrastructure engineers nervous. Imagine that your application hasn't changed at all. Your Kubernetes manifests are still valid, your infrastructure code is still valid, and your production cluster is still running. Then a node fails. You need to recreate the workload, and Kubernetes tries to pull:&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minio/minio:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the image is no longer available.&lt;/p&gt;

&lt;p&gt;If you haven't mirrored the image to your own registry, your deployment is blocked. You may end up relying on a colleague who still has the image locally, or trying to recover an old image from somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternative #1: Garage
&lt;/h2&gt;

&lt;p&gt;Garage is a distributed object storage system written in Rust that implements the S3 API.&lt;/p&gt;

&lt;p&gt;Its philosophy is somewhat different from MinIO. Garage is particularly interesting if your goal isn't simply "I want a server that looks like MinIO", but rather "I want distributed object storage that can operate across multiple locations."&lt;/p&gt;

&lt;p&gt;Garage uses a decentralized architecture where nodes participate in the distributed storage system. This makes it particularly interesting for deployments where you might have several relatively small nodes spread across different locations instead of one large storage cluster.&lt;/p&gt;

&lt;p&gt;For example, you could have infrastructure distributed across several sites:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph SiteA["Site A (Primary DC)"]
        A1["Node 1"] --- A2["Node 2"] --- A3["Node 3"]
    end

    subgraph SiteB["Site B (Secondary / Edge)"]
        B1["Node 4"] --- B2["Node 5"] --- B3["Node 6"]
    end

    SiteA &amp;lt;--&amp;gt;|Geo-distributed sync / WAN| SiteB&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;This is one of the areas where Garage differs significantly from the traditional MinIO mental model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternative #2: RustFS
&lt;/h2&gt;

&lt;p&gt;RustFS takes a different approach from Garage, although it shares some important characteristics. It is also written in Rust and provides an AWS S3-compatible API.&lt;/p&gt;

&lt;p&gt;For MinIO users, the interesting part is its design philosophy. RustFS explicitly describes its architecture as being inspired by MinIO's design philosophy, which makes it particularly interesting when your starting point is: "I already know MinIO, and I want something that feels familiar."&lt;/p&gt;

&lt;p&gt;RustFS uses a distributed model consisting of nodes and drives. Conceptually, it looks like:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    Client["S3 Client Traffic"] --&amp;gt; N3 &amp;amp; N2 &amp;amp; N1

    subgraph Cluster["RustFS Cluster (Erasure Set)"]
        subgraph N3["Node 3"]
            D5["Drive 5"]
            D6["Drive 6"]
        end
        subgraph N2["Node 2"]
            D3["Drive 3"]
            D4["Drive 4"]
        end
        subgraph N1["Node 1"]
            D1["Drive 1"]
            D2["Drive 2"]
        end
    end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The cluster is divided into sets, with drives distributed across nodes. For engineers coming from MinIO, the mental model is relatively familiar. This is what makes RustFS one of the most obvious projects to investigate if the main objective is replacing an existing MinIO installation while keeping the application layer largely unchanged.&lt;/p&gt;

&lt;p&gt;That doesn't mean that migration is completely transparent. S3 compatibility needs to be tested against the actual application workload, and the operational model of the new cluster still needs to be understood.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternative #3: SeaweedFS
&lt;/h2&gt;

&lt;p&gt;SeaweedFS takes a broader approach than Garage and RustFS. While both are primarily distributed object-storage systems, SeaweedFS is designed as a distributed storage platform supporting S3, FUSE, HDFS, WebDAV, SFTP, and other interfaces.&lt;/p&gt;

&lt;p&gt;For MinIO users, this makes SeaweedFS interesting when S3 is only part of the requirement. If you simply need an S3-compatible API, SeaweedFS may provide more infrastructure than necessary. But if you need multiple storage interfaces or have workloads involving very large numbers of small objects, its architecture becomes more compelling.&lt;/p&gt;

&lt;p&gt;Conceptually, it looks like:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph Interfaces["Storage Interfaces"]
        S3["S3 API"]
        FUSE["FUSE"]
        HDFS["HDFS"]
    end

    subgraph Cluster["SeaweedFS Cluster"]
        Storage["Storage Layer"]
    end

    Interfaces --&amp;gt; Cluster&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;SeaweedFS separates storage into master servers, volume servers, and the Filer. The master manages cluster metadata, volume servers store the data, and the Filer provides a filesystem-like namespace. The S3 gateway sits on top of this architecture and can be scaled independently.&lt;/p&gt;

&lt;p&gt;Another notable feature is its support for both replication and erasure coding, together with a volume-based design that works well with large numbers of small objects. This makes SeaweedFS worth considering for datasets, thumbnails, generated files, and other high-object-count workloads.&lt;/p&gt;

&lt;p&gt;The trade-off is complexity. SeaweedFS offers considerably more functionality than a simple S3-compatible object store, so the additional flexibility needs to be weighed against the operational overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Garage vs RustFS vs SeaweedFS
&lt;/h2&gt;

&lt;p&gt;At this point, it should be clear that these three projects aren't simply three interchangeable MinIO clones.&lt;/p&gt;

&lt;p&gt;They all provide S3-compatible storage, but they target somewhat different requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Garage&lt;/strong&gt; is focused on decentralized object storage and is particularly interesting for geographically distributed or multi-site deployments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RustFS&lt;/strong&gt; is the most natural project to investigate when your primary requirement is replacing MinIO with another distributed S3-compatible object store while retaining a relatively familiar architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SeaweedFS&lt;/strong&gt; takes a broader approach and makes sense when object storage is only one part of the problem. Its ability to expose the same underlying storage through S3, filesystem, HDFS, and other interfaces can be valuable for larger or more diverse storage environments.&lt;/p&gt;

&lt;p&gt;The important distinction is therefore not simply performance or feature count. It is the architecture you want to operate.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Garage&lt;/th&gt;
&lt;th&gt;RustFS&lt;/th&gt;
&lt;th&gt;SeaweedFS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary language&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 compatibility&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;Distributed, decentralized&lt;/td&gt;
&lt;td&gt;Distributed, decentralized&lt;/td&gt;
&lt;td&gt;Layered distributed storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary focus&lt;/td&gt;
&lt;td&gt;Distributed object storage&lt;/td&gt;
&lt;td&gt;S3 object storage&lt;/td&gt;
&lt;td&gt;Object + file storage platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MinIO migration&lt;/td&gt;
&lt;td&gt;Requires architectural changes&lt;/td&gt;
&lt;td&gt;Conceptually closer&lt;/td&gt;
&lt;td&gt;Requires architectural changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Geo-distribution&lt;/td&gt;
&lt;td&gt;Strong focus&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filesystem access&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FUSE&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDFS&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small-object workloads&lt;/td&gt;
&lt;td&gt;Good fit&lt;/td&gt;
&lt;td&gt;Good fit&lt;/td&gt;
&lt;td&gt;Strong focus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Erasure coding&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operational complexity&lt;/td&gt;
&lt;td&gt;Relatively focused&lt;/td&gt;
&lt;td&gt;Relatively focused&lt;/td&gt;
&lt;td&gt;Higher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best suited for&lt;/td&gt;
&lt;td&gt;Multi-site / decentralized storage&lt;/td&gt;
&lt;td&gt;MinIO replacement&lt;/td&gt;
&lt;td&gt;Multi-purpose distributed storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration complexity&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low–medium&lt;/td&gt;
&lt;td&gt;Medium–high&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table is not intended to declare a winner. The projects solve different problems, and the right choice depends on the workload and architecture you need to support.&lt;/p&gt;

&lt;p&gt;If you have multiple locations and want decentralized storage that treats geographic distribution as a fundamental part of the architecture, Garage is worth investigating.&lt;/p&gt;

&lt;p&gt;If your main requirement is replacing MinIO with a modern, Rust-based, distributed S3-compatible object store that follows a relatively familiar model, RustFS is an obvious candidate.&lt;/p&gt;

&lt;p&gt;If you need more than S3 — for example, filesystem access, HDFS, huge numbers of small objects, or multiple storage interfaces — SeaweedFS deserves serious consideration.&lt;/p&gt;

&lt;p&gt;The important thing is that you don't have to choose the project with the longest feature list. You have to choose the architecture that matches your workload.&lt;/p&gt;

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

&lt;p&gt;MinIO may no longer be the default choice it once was, but that doesn't mean there is a single project waiting to take its place. Garage, RustFS, and SeaweedFS represent three different directions for self-hosted object storage. Garage focuses on decentralized, multi-site storage. RustFS is the closest fit for teams looking for a familiar distributed S3 model. SeaweedFS goes beyond object storage and provides a broader storage platform with multiple interfaces and a design particularly suited to large numbers of objects. The era of treating MinIO as the automatic default for self-hosted S3 is over. The right replacement depends on the operational model, workload, and architecture you want to maintain long after the migration is complete.&lt;/p&gt;

&lt;p&gt;For an existing MinIO deployment, the decision should not start with &lt;strong&gt;"Which project is the best?"&lt;/strong&gt;. It should start with &lt;strong&gt;"What do we actually need from our storage layer?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you answer that question, choosing between Garage, RustFS, and SeaweedFS becomes much less about finding the next MinIO and much more about choosing the storage architecture that actually fits your infrastructure.&lt;/p&gt;

</description>
      <category>minio</category>
      <category>s3</category>
      <category>rustfs</category>
      <category>garage</category>
    </item>
    <item>
      <title>TypeSafe AI Jev explained: examples, use cases, and LLM comparisons</title>
      <dc:creator>Paweł Swiridow</dc:creator>
      <pubDate>Wed, 23 Sep 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/u11d/typesafe-ai-jev-explained-examples-use-cases-and-llm-comparisons-2k4o</link>
      <guid>https://dev.to/u11d/typesafe-ai-jev-explained-examples-use-cases-and-llm-comparisons-2k4o</guid>
      <description>&lt;p&gt;TypeSafe AI Jev is a model for making bounded decisions inside software. You give it text or structured state plus questions whose valid answers you define. It returns typed choices, scores, probabilities, and confidence values instead of generating prose.&lt;/p&gt;

&lt;p&gt;That makes Jev useful for classification, routing, scoring, and review gates. It does not replace GPT, Claude, or another generative model when you need writing, code, explanations, or open-ended reasoning. Think of it as a fuzzy decision function, not a chatbot wearing a smaller hat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answer
&lt;/h2&gt;

&lt;p&gt;Use Jev when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every valid answer can be defined before the call.&lt;/li&gt;
&lt;li&gt;The hard part is semantic judgment, not arithmetic or deterministic policy.&lt;/li&gt;
&lt;li&gt;Software needs probabilities or confidence to decide whether to act, retry, or escalate.&lt;/li&gt;
&lt;li&gt;The workload repeats often enough for latency and per-call cost to matter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use something else when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The output must contain newly written text, code, or an explanation.&lt;/li&gt;
&lt;li&gt;Correctness depends on exact math, dates, permissions, or database state.&lt;/li&gt;
&lt;li&gt;The task requires long, multi-step reasoning.&lt;/li&gt;
&lt;li&gt;The model would be the only security or authorization boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is TypeSafe AI Jev?
&lt;/h2&gt;

&lt;p&gt;TypeSafe calls Jev its first System One model. That is the company's product category for models built to make fast, structured decisions that software can consume directly. It is not a new chat interface and it is not an industry-standard model category.&lt;/p&gt;

&lt;p&gt;The contract is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;state + typed questions -&amp;gt; Jev -&amp;gt; typed answers + probabilities -&amp;gt; application code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The state can be a string, JSON object, or array containing text. Each question defines its answer space. Jev evaluates independent questions against the same state in parallel, then your code decides what happens next.&lt;/p&gt;

&lt;p&gt;At the time of writing, &lt;code&gt;jev-latest&lt;/code&gt; resolves to &lt;code&gt;jev-1.13.0&lt;/code&gt;. TypeSafe's &lt;a href="https://docs.typesafe.ai/models" rel="noopener noreferrer"&gt;model documentation&lt;/a&gt; lists it at $0.042 per million input tokens with free output tokens. The documented context limit is 64,000 tokens per request, with a separate 32,000-token limit for the state plus the longest question. Input is text only, so images, audio, and video must be converted before they reach Jev.&lt;/p&gt;

&lt;p&gt;Pin a version when tuning thresholds and rerun evaluation before upgrading. A moving alias is convenient until it changes a production route at 02:00.&lt;/p&gt;

&lt;p&gt;TypeSafe's &lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;launch material&lt;/a&gt; describes Jev as unable to hallucinate because it cannot emit a value outside the schema. That claim needs a narrower reading. Jev cannot invent a fourth result when your Choice defines three, but it can choose the wrong one. Type safety protects the shape of an answer. It does not make the judgment correct. &lt;a href="https://arize.com/blog/typesafe-jev-llm-judge/" rel="noopener noreferrer"&gt;Independent analysis from Arize&lt;/a&gt; makes the same distinction.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Choice, Score, and Noul work
&lt;/h2&gt;

&lt;p&gt;Jev exposes three question types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Primitive&lt;/th&gt;
&lt;th&gt;Question it answers&lt;/th&gt;
&lt;th&gt;Returned data&lt;/th&gt;
&lt;th&gt;Good fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Choice&lt;/td&gt;
&lt;td&gt;Which predefined option fits best?&lt;/td&gt;
&lt;td&gt;selected option, option probabilities, confidence&lt;/td&gt;
&lt;td&gt;intent routing, taxonomy classification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Score&lt;/td&gt;
&lt;td&gt;Where does this fall on an ordered rubric?&lt;/td&gt;
&lt;td&gt;weighted score, level probabilities, legend, confidence&lt;/td&gt;
&lt;td&gt;severity, quality, urgency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noul&lt;/td&gt;
&lt;td&gt;How likely is this yes/no proposition?&lt;/td&gt;
&lt;td&gt;probability from 0 to 1&lt;/td&gt;
&lt;td&gt;detection, verification, binary review signals&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A Choice can contain up to 255 options. A Score accepts between two and ten ordered levels. Choice and Score return a &lt;code&gt;confidence&lt;/code&gt; value derived from their probability distributions; Noul returns the probability of yes without a separate confidence field.&lt;/p&gt;

&lt;p&gt;Questions in one request are independent. If question B depends on question A, use another call or express the dependency in code. Do not assume that two differently phrased questions will obey arithmetic identities either. TypeSafe documents cases where a Noul and an equivalent-looking Choice produce different numbers because they ask different statistical questions.&lt;/p&gt;

&lt;p&gt;Keep questions atomic. Ask separately whether a refund was requested, score impact, classify intent, then let code apply policy. Models judge fuzzy meaning; code does not reinterpret accounting rules overnight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run a Jev decision in Python
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install the SDK
&lt;/h3&gt;

&lt;p&gt;The Python SDK requires an API key in &lt;code&gt;TYPESAFE_API_KEY&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;typesafe-sdk
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TYPESAFE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'replace-with-a-key-from-console.typesafe.ai'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use your normal secret manager in CI and production. Committing the key beside the example would turn a quick start into an incident report.&lt;/p&gt;

&lt;h3&gt;
  
  
  Classify and route a support ticket
&lt;/h3&gt;

&lt;p&gt;This example asks one Choice, one Score, and one Noul in a single call. It then applies routing thresholds in ordinary Python.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typesafe_sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Noul&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TypeSafeClient&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TYPESAFE_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Set TYPESAFE_API_KEY before running this example&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;INPUT_USD_PER_MTOK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.042&lt;/span&gt;  &lt;span class="c1"&gt;# TypeSafe list price at the time of writing
&lt;/span&gt;
&lt;span class="n"&gt;ticket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Checkout fails after payment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Our card was charged twice and checkout still shows an error. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;We need this fixed today because customers cannot place orders.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;business&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;TypeSafeClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jev-1.13.0&lt;/span&gt;&lt;span class="sh"&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;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;system_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;questions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Which team should own this ticket first?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Charges, invoices, refunds, or subscriptions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;technical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bugs, outages, failed integrations, or broken product behavior&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Login, identity, permissions, or account access&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The ticket does not fit the other categories&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How severely does this issue block the customer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s work?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Minor inconvenience with a clear workaround&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Important feature is degraded but work can continue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Critical workflow is blocked or customers are losing money&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requests_refund&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Noul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Does the customer explicitly ask for a refund?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The customer directly requests money back&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The customer reports a charge but does not request a refund&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;

&lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;severity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;refund_probability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nouls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requests_refund&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;noul&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.65&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human-review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;incident-response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;

&lt;span class="n"&gt;input_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;estimated_cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;INPUT_USD_PER_MTOK&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;destination=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;department_confidence=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;department&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refund_probability=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;refund_probability&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_tokens=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_tokens=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;elapsed_seconds=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;estimated_cost_usd=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;estimated_cost&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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 SDK exposes separate collections for Choice, Score, and Noul answers. The thresholds are examples, not TypeSafe defaults. Set them from errors and review costs measured on your own data.&lt;/p&gt;

&lt;p&gt;Pinning &lt;code&gt;jev-1.13.0&lt;/code&gt; prevents alias drift. In production, log the criteria version, probability distributions, chosen route, and human outcome.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sample output and cost
&lt;/h3&gt;

&lt;p&gt;A real run of the example above against &lt;code&gt;jev-1.13.0&lt;/code&gt; printed this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model=jev-1.13.0
destination=incident-response
department_confidence=0.95
severity=2.00
refund_probability=0.06
input_tokens=528
output_tokens=77
elapsed_seconds=0.64
estimated_cost_usd=0.00002218
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;usage&lt;/code&gt; block in the response is what makes the cost measurable without opening a billing dashboard. That call moved 528 input tokens. TypeSafe bills $0.042 per million input tokens and does not bill output tokens, so the run cost 528 / 1,000,000 * 0.042, or roughly $0.000022 at current list prices. A thousand calls of this size cost about two cents, and one dollar buys roughly 45,000 of them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;elapsed_seconds&lt;/code&gt; covers client setup plus the round trip, not just server time. Values move with the input, criteria, and model version, so treat the transcript as a real example rather than a fixed contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Jev use cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Route support requests
&lt;/h3&gt;

&lt;p&gt;Support routing is the natural first pilot. Put the ticket text and relevant account context in state. Use Choice for the owning queue, Score for impact, and Noul for independent conditions such as an explicit refund request:&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;"state"&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;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wrong item delivered, replacement needed before Friday"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order 48213 arrived with two left boots. I need the replacement before the conference on Friday or I want to cancel."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"account_tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"enterprise"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"earlier_tickets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jev-1.13.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"questions"&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;"queue"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Which queue should own this ticket first?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&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;"returns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wrong, damaged, or missing items, replacements, and shipping problems"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"billing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Charges, invoices, refunds, and subscriptions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"technical"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Product failures, errors, and defects"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Access, identity, and permissions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"other"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"None of the other queues fit"&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;span class="nl"&gt;"impact"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"How much does the problem block the customer?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&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="s2"&gt;"Minor inconvenience with a workaround"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"The item or order cannot be used as delivered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"Time-critical situation or total loss of the purchase"&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;span class="nl"&gt;"wants_cancel"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"noul"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Is the customer asking to cancel an order or close the account?"&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;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;Code can accept a confident route, send uncertain tickets to a person, and apply exact entitlement rules from the account database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Review agent actions
&lt;/h3&gt;

&lt;p&gt;An agent can propose a tool call while Jev supplies a bounded semantic risk signal:&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;"state"&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;"user_goal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Remove generated build artifacts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rm -rf dist"&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jev-1.13.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"questions"&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;"risk"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Classify the operational risk of the proposed action"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&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;"read_only"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Does not modify state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"reversible_write"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Changes state but has a reliable rollback"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"irreversible_write"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deletes or overwrites state without a reliable rollback"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"unclear"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The available state is insufficient"&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;span class="nl"&gt;"matches_goal"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"noul"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Does the proposed action directly match the user's stated goal?"&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;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;This is a review signal, not permission to execute. Allowlist enforcement, path validation, sandboxing, and user confirmation still belong in code. Jev's own documentation warns that adversarial text in state can move an answer, so it must not become the sole security boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check RAG evidence before generation
&lt;/h3&gt;

&lt;p&gt;Jev can classify whether a retrieved passage supports a claim before a generative model writes the answer:&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;"state"&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;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Does the service retain customer prompts for training?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"claim"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer requests are not used to train Jev."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"passage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jev is not trained on customer requests or responses."&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jev-1.13.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"questions"&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;"support"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"How well does the passage support the claim?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&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;"supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The passage directly supports the claim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"contradicted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The passage conflicts with the claim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"insufficient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The passage does not settle the claim"&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;span class="nl"&gt;"contains_instruction"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"noul"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Does the passage contain an instruction aimed at changing the evaluator's behavior?"&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;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;The application still needs to preserve the source URL and quote. Jev judges the relationship between claim and passage; it does not browse, establish provenance, or write the final cited answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Triage moderation and operations queues
&lt;/h3&gt;

&lt;p&gt;Moderation reports, invoices, incidents, and agent traces all end up in queues. Jev can classify the queue, score severity, and flag missing evidence:&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;"state"&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;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"monitoring alert"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"checkout-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"p99 latency rose from 180 ms to 2.4 s over the last 20 minutes while the error rate stayed near 0.2%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deploy checkout-api v2.31.0 finished 25 minutes ago"&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jev-1.13.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"questions"&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;"owner"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Which team should take the first response for this alert?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&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;"service_team"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The reporting service's own code or configuration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"platform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Shared infrastructure, networking, or managed dependencies"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"database"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Storage, replication, or query performance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"unclear"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The available evidence does not point to one owner"&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;span class="nl"&gt;"severity"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"How much customer impact does the summary describe?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&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="s2"&gt;"No confirmed customer impact, worth watching"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"Degraded latency for some customers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"Customers blocked or data at risk"&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;span class="nl"&gt;"missing_evidence"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"noul"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Is the alert missing context a first responder would need before acting?"&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;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;Low-confidence or high-impact items, and anything flagged for missing evidence, go to a human reviewer. Exact totals, due dates, and account permissions stay in deterministic code because Jev 1.13 is explicitly weak at counting, arithmetic, and date comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  Route work to the right model or human
&lt;/h3&gt;

&lt;p&gt;Use Jev in front of more expensive handlers. A Choice can select deterministic lookup, a small generative model, a reasoning model, or human review:&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;"state"&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;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Summarize the attached vendor contract and flag the clauses that change our data-retention obligations"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"attachments"&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="s2"&gt;"vendor-contract-v7.pdf"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"requester"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"procurement"&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jev-1.13.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"questions"&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;"route"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Which handler should receive this request?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&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;"deterministic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A lookup, calculation, or template can answer it exactly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"small_model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Short generation or extraction a cheap model handles well"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"reasoning_model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Long-document analysis or multi-step reasoning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"human"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Judgment, negotiation, or accountability beyond the model stack"&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;span class="nl"&gt;"in_scope"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"noul"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Does the request fall inside the documented scope of the available handlers?"&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;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;Confidence determines whether the route is safe to accept. Below the threshold, send the request to a human instead of accepting a cheap guess that the matching handler will only have to redo later. This architecture spends generation tokens only when the task needs generation, rather than asking a novelist to sort every envelope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jev vs LLMs, classifiers, and rules
&lt;/h2&gt;

&lt;p&gt;Jev is not automatically better than the alternatives. It occupies a specific slot:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Rules and code&lt;/th&gt;
&lt;th&gt;Traditional classifier&lt;/th&gt;
&lt;th&gt;LLM structured output&lt;/th&gt;
&lt;th&gt;TypeSafe Jev&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best job&lt;/td&gt;
&lt;td&gt;exact deterministic checks&lt;/td&gt;
&lt;td&gt;stable prediction task with labeled data&lt;/td&gt;
&lt;td&gt;generation, extraction, explanation, broad reasoning&lt;/td&gt;
&lt;td&gt;bounded semantic decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;exact programmed value&lt;/td&gt;
&lt;td&gt;fixed trained labels and scores&lt;/td&gt;
&lt;td&gt;generated text constrained to a schema&lt;/td&gt;
&lt;td&gt;predefined Choice, Score, or Noul answers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New taxonomy&lt;/td&gt;
&lt;td&gt;edit code&lt;/td&gt;
&lt;td&gt;usually retrain&lt;/td&gt;
&lt;td&gt;change prompt or schema&lt;/td&gt;
&lt;td&gt;change request criteria&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Probability signal&lt;/td&gt;
&lt;td&gt;not applicable&lt;/td&gt;
&lt;td&gt;often available, calibration varies&lt;/td&gt;
&lt;td&gt;usually not a reliable application probability&lt;/td&gt;
&lt;td&gt;first-class probabilities; local validation still required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explanation&lt;/td&gt;
&lt;td&gt;code is inspectable&lt;/td&gt;
&lt;td&gt;usually limited&lt;/td&gt;
&lt;td&gt;can generate one&lt;/td&gt;
&lt;td&gt;no generated explanation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact math and policy&lt;/td&gt;
&lt;td&gt;best option&lt;/td&gt;
&lt;td&gt;depends on trained features&lt;/td&gt;
&lt;td&gt;possible but unreliable&lt;/td&gt;
&lt;td&gt;documented poor fit; keep it in code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment tradeoff&lt;/td&gt;
&lt;td&gt;engineering maintenance&lt;/td&gt;
&lt;td&gt;labels, training, serving&lt;/td&gt;
&lt;td&gt;latency, cost, parsing, model variance&lt;/td&gt;
&lt;td&gt;hosted early access, bounded interface, no generation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plain JSON mode guarantees valid JSON, not adherence to a supplied schema. Strict structured-output APIs can enforce supported JSON schemas, including required keys and enum values.&lt;/p&gt;

&lt;p&gt;Schema enforcement is therefore not unique to Jev. Its distinction is a decision-specific interface built around Choice, Score, and Noul, with probability distributions and confidence for bounded judgments.&lt;/p&gt;

&lt;p&gt;A conventional classifier may be the better choice for a stable, high-volume task when you already have good labels, training infrastructure, and predictable categories. In one early spam experiment, Jev reached 98.6% accuracy on a 5,733-email ham, spam, and phishing test without task-specific fine-tuning or labeled examples in its requests, while a TF-IDF logistic-regression baseline trained on labeled messages reached 98.9% (&lt;a href="https://github.com/bitnovus/jev-spam-eval" rel="noopener noreferrer"&gt;full writeup&lt;/a&gt;). The criteria had been refined after reviewing labeled examples, so this was useful early evidence, not magic zero-data victory.&lt;/p&gt;

&lt;p&gt;Rules win whenever the answer can be computed exactly. Do not ask any model whether an invoice is overdue if two parsed dates and a comparison operator can settle it.&lt;/p&gt;

&lt;p&gt;The cost and latency story is still notable.&lt;/p&gt;

&lt;p&gt;TypeSafe reports 70 to 500 ms end-to-end latency and $0.042 per million input tokens for suitable requests.&lt;/p&gt;

&lt;p&gt;Its launch workflow evaluation produced headline claims of 193.6 times faster and 444.6 times cheaper than its selected LLM comparison, but TypeSafe says those figures are likely near the high end of real-world gains and notes possible benchmark bias.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to use TypeSafe AI Jev
&lt;/h2&gt;

&lt;p&gt;Jev 1.13 is not trained to write text. It will not draft a customer reply, produce code, summarize a report, or explain why it chose an answer. Use a generative model when the output itself must be created. The missing explanation also matters for evaluation work: an LLM judge can describe a failure, while Jev returns probabilities that tell you where to inspect, not how to fix the problem.&lt;/p&gt;

&lt;p&gt;Keep arithmetic, counting, numeric interpolation, and date comparisons in code. TypeSafe's &lt;a href="https://docs.typesafe.ai/model-jaggedness/jev-1.13" rel="noopener noreferrer"&gt;limitations page&lt;/a&gt; says Jev can read dates as text but does not reliably treat them as ordered values. It also warns about literal phrasing, multi-hop indirection, irrelevant context, contradictory criteria, and adversarial content.&lt;/p&gt;

&lt;p&gt;More context is not automatically better. Retrieve and filter first, then send the fields needed for the decision. A 64,000-token limit is capacity, not a challenge.&lt;/p&gt;

&lt;p&gt;Test non-English workloads separately. TypeSafe says English is Jev's strongest training language and other languages are not handled equally well.&lt;/p&gt;

&lt;p&gt;Most importantly, separate a valid answer from a correct answer. A typed output prevents parser failures and out-of-schema labels. It does not prevent a confident, valid, expensive mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to evaluate Jev on your workload
&lt;/h2&gt;

&lt;p&gt;Start in shadow mode:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick one reversible decision with a closed answer set.&lt;/li&gt;
&lt;li&gt;Freeze a representative labeled dataset and the exact criteria text.&lt;/li&gt;
&lt;li&gt;Run Jev and the current baseline on identical records.&lt;/li&gt;
&lt;li&gt;Measure per-class errors, false automatic actions, human-review rate, calibration, end-to-end latency, and cost per completed decision.&lt;/li&gt;
&lt;li&gt;Tune thresholds by consequence. Sending a ticket to the wrong queue and approving a destructive action do not deserve the same confidence floor.&lt;/li&gt;
&lt;li&gt;Pin the model version and rerun the suite before changing either model or criteria.&lt;/li&gt;
&lt;li&gt;Keep permissions, arithmetic, and state changes in deterministic code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not optimize only for overall accuracy. A model can look good while failing badly on a rare queue that matters. Track confusion by class and inspect probability bands: cases scored near 0.9 should be correct more often than cases scored near 0.6. If that relationship does not hold on your data, the threshold is decoration.&lt;/p&gt;

&lt;p&gt;Include review capacity in the evaluation. A cautious threshold that sends 70 percent of cases to a human may be safe, but it has not automated much. The useful operating point balances wrong automatic actions against the cost of review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you use TypeSafe AI Jev?
&lt;/h2&gt;

&lt;p&gt;Pilot TypeSafe AI Jev when the answer space is bounded, the judgment is semantic, and software consumes the result often enough for decision latency and cost to matter. Support routing, relevance checks, moderation labels, model routing, and broad evaluation coverage are credible starting points.&lt;/p&gt;

&lt;p&gt;Keep an LLM when the output must be written, explained, or reasoned through. Keep code when the answer can be computed exactly. Keep a human in the path when a wrong but valid decision has serious consequences and your own calibration data is thin.&lt;/p&gt;

&lt;p&gt;Jev's useful idea is not that every LLM should be replaced. It is that generation and judgment do not have to be the same model call. Split them, measure them independently, and make code responsible for the final action.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>jev</category>
      <category>agents</category>
    </item>
    <item>
      <title>Implementing Postgres Row-Level Security in Next.js: The Drizzle Multi-Tenant Pattern</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Wed, 16 Sep 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/u11d/implementing-postgres-row-level-security-in-nextjs-the-drizzle-multi-tenant-pattern-428l</link>
      <guid>https://dev.to/u11d/implementing-postgres-row-level-security-in-nextjs-the-drizzle-multi-tenant-pattern-428l</guid>
      <description>&lt;p&gt;The first time we turned on Row-Level Security, nothing happened. Policies were in place, &lt;code&gt;ENABLE ROW LEVEL SECURITY&lt;/code&gt; had run, and every query still returned every tenant's rows. Took a minute to remember: we were connected as &lt;code&gt;postgres&lt;/code&gt;, the superuser, and superusers ignore RLS entirely. That one gotcha basically shaped the rest of this setup.&lt;/p&gt;

&lt;p&gt;The problem we were actually trying to solve is boring and everyone's had it: a route handler forgets &lt;code&gt;WHERE organization_id = ?&lt;/code&gt;, code review doesn't catch it because the query still "works," and Tenant A quietly gets to see Tenant B's data. Nobody notices until someone does.&lt;/p&gt;

&lt;p&gt;Our answer was to stop trusting application code to remember the filter and push isolation into Postgres itself with &lt;a href="https://www.postgresql.org/docs/current/ddl-rowsecurity.html" rel="noopener noreferrer"&gt;Row-Level Security&lt;/a&gt;. The pitch is simple: if a query forgets tenant scoping, it should get zero rows back, not someone else's data. Here's the setup we landed on — two DB roles, a &lt;code&gt;SET LOCAL&lt;/code&gt; session variable, a Next.js wrapper so route handlers don't have to think about any of this — plus the four things that actually bit us building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two roles, two connections
&lt;/h2&gt;

&lt;p&gt;Back to that first surprise: &lt;strong&gt;superusers and table owners bypass RLS by default.&lt;/strong&gt; The &lt;a href="https://www.postgresql.org/docs/current/ddl-rowsecurity.html" rel="noopener noreferrer"&gt;docs&lt;/a&gt; say it plainly — superuser or &lt;code&gt;BYPASSRLS&lt;/code&gt;, policies don't apply, full stop. Table owners get the same pass unless you explicitly add &lt;code&gt;FORCE ROW LEVEL SECURITY&lt;/code&gt;. If your app connects as &lt;code&gt;postgres&lt;/code&gt;, which is what basically every local Postgres setup defaults to, your carefully written policies are just decoration.&lt;/p&gt;

&lt;p&gt;So we run two roles now:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;RLS&lt;/th&gt;
&lt;th&gt;Used by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;postgres&lt;/code&gt; (superuser)&lt;/td&gt;
&lt;td&gt;Bypassed&lt;/td&gt;
&lt;td&gt;Auth library, admin routes, migrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;app_user&lt;/code&gt; (non-superuser)&lt;/td&gt;
&lt;td&gt;Enforced&lt;/td&gt;
&lt;td&gt;All tenant-scoped queries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each gets its own pool, wrapped as two Drizzle instances:&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;// db.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDbAdmin&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;PostgresJsDatabase&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Schema&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;_dbAdmin&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;adminConnectionConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// DATABASE_URL, superuser&lt;/span&gt;
      &lt;span class="na"&gt;max&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="na"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;application_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app-admin&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;_dbAdmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;drizzle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&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;_dbAdmin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDb&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;PostgresJsDatabase&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Schema&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;_db&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;appConnectionConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// APP_DATABASE_URL, app_user&lt;/span&gt;
      &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;application_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app-tenant&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;_db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;drizzle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&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;_db&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;Worth flagging two things here. First, the lazy init isn't stylistic — Next.js runs module scope at build time, and if you open a connection pool at import time, &lt;code&gt;next build&lt;/code&gt; needs a live database to succeed. That bit us once in CI. Wrapping construction in a getter defers the actual connection until something queries it.&lt;/p&gt;

&lt;p&gt;We also wrap both in a &lt;code&gt;Proxy&lt;/code&gt; so call sites can still just import &lt;code&gt;db&lt;/code&gt; and use it like a normal object:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&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;Proxy&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;PostgresJsDatabase&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Schema&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prop&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="nb"&gt;Reflect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getDb&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;prop&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;I won't pretend this is elegant. It works, and it means nobody importing &lt;code&gt;db&lt;/code&gt; has to know it's lazy underneath. Fine trade.&lt;/p&gt;

&lt;p&gt;Second thing: we set a distinct &lt;code&gt;application_name&lt;/code&gt; per pool. Costs nothing, and the first time something's connection-starved you'll be very glad &lt;code&gt;pg_stat_activity&lt;/code&gt; tells you exactly which pool is choking instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The policy itself
&lt;/h2&gt;

&lt;p&gt;Every tenant table gets the same shape:&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="n"&gt;projects&lt;/span&gt; &lt;span class="n"&gt;ENABLE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;tenant_isolation&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;projects&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;organization_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.organization_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;uuid&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;organization_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.organization_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter more than they look like they should.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;true&lt;/code&gt; as the second argument to &lt;a href="https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SET" rel="noopener noreferrer"&gt;&lt;code&gt;current_setting&lt;/code&gt;&lt;/a&gt; means "don't error if the setting's unset, just give me NULL." NULL compared to anything is NULL, and Postgres treats a NULL policy result as "exclude this row" — same as false. So a query that runs without tenant context isn't a crash and isn't a leak, it's just an empty table. Someone forgets to set context, they get a confusing empty result, they go debug it. Nobody's data went anywhere in the meantime. That's the property I actually care about here.&lt;/p&gt;

&lt;p&gt;And don't skip &lt;code&gt;WITH CHECK&lt;/code&gt;. It's easy to write the &lt;code&gt;USING&lt;/code&gt; clause, ship it, and feel done — but &lt;code&gt;USING&lt;/code&gt; only governs reads. Without &lt;code&gt;WITH CHECK&lt;/code&gt;, nothing stops a tenant from &lt;code&gt;INSERT&lt;/code&gt;ing or &lt;code&gt;UPDATE&lt;/code&gt;ing a row with someone else's &lt;code&gt;organization_id&lt;/code&gt; in it. We missed this on the first pass of one migration and caught it in review, which is exactly the kind of thing you don't want to catch in review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting context into the transaction: SET LOCAL
&lt;/h2&gt;

&lt;p&gt;Policies check &lt;code&gt;current_setting('app.organization_id')&lt;/code&gt;, so something has to set it per request. The obvious first attempt — plain &lt;code&gt;SET app.organization_id = '...'&lt;/code&gt; — is wrong, and it's wrong in a way that won't show up until you're under load. &lt;code&gt;SET&lt;/code&gt; is session-scoped, connections are pooled, and a session-scoped setting sticks around for whoever grabs that connection next. That's tenant bleed waiting to happen the moment your pool gets reused across requests, which it will.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-set.html" rel="noopener noreferrer"&gt;&lt;code&gt;SET LOCAL&lt;/code&gt;&lt;/a&gt; is the fix — scoped to the current transaction, reverts automatically on commit or rollback. Open transaction, &lt;code&gt;SET LOCAL&lt;/code&gt;, run your queries, done, connection goes back to the pool clean.&lt;/p&gt;

&lt;p&gt;In Next.js we wrapped this into one function every tenant route goes through:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AuthenticatedHandler&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;async &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;NextRequest&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;RouteContext&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;result&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;authenticate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// session lookup&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&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="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;user&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&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;await&lt;/span&gt; &lt;span class="nf"&gt;setOrgContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;handler&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="nx"&gt;user&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;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;castTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&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="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;Route handlers never see any of this. They just get a &lt;code&gt;tx&lt;/code&gt; that's already scoped:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tx&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;rows&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;tx&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="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// only this org's rows, guaranteed&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rows&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;No &lt;code&gt;WHERE organizationId = ?&lt;/code&gt; anywhere in that query, and it's still safe. We keep the explicit filters in most places anyway — belt and suspenders, and it documents intent for the next person reading the code — but the point stands: if you forgot it, RLS still has you.&lt;/p&gt;

&lt;p&gt;Server components got a matching helper since they're not going through route handlers at all:&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="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;withOrgDb&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;organizationId&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="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DbInstance&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;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;return&lt;/span&gt; &lt;span class="nf"&gt;getDb&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&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;await&lt;/span&gt; &lt;span class="nf"&gt;setOrgContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;castTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&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;h2&gt;
  
  
  Gotcha #1: you can't parameterize SET
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;setOrgContext&lt;/code&gt; looks like this:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UUID_RE&lt;/span&gt; &lt;span class="o"&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;0-9a-f&lt;/span&gt;&lt;span class="se"&gt;]{8}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9a-f&lt;/span&gt;&lt;span class="se"&gt;]{4}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9a-f&lt;/span&gt;&lt;span class="se"&gt;]{4}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9a-f&lt;/span&gt;&lt;span class="se"&gt;]{4}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9a-f&lt;/span&gt;&lt;span class="se"&gt;]{12}&lt;/span&gt;&lt;span class="sr"&gt;$/i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setOrgContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DrizzleTx&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="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="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;UUID_RE&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;organizationId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid organization ID format&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;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`SET LOCAL app.organization_id = '&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="nf"&gt;raw&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;'`&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;That &lt;code&gt;sql.raw()&lt;/code&gt; should bother you a little — it bothered me too, first time I wrote it. Here's the thing though: &lt;code&gt;SET&lt;/code&gt; is a utility command, not a regular statement, and utility commands in Postgres just don't take &lt;code&gt;$1&lt;/code&gt;-style parameters. You cannot write &lt;code&gt;SET LOCAL app.organization_id = $1&lt;/code&gt;. It'll error. You've got two real options — &lt;code&gt;set_config('app.organization_id', $1, true)&lt;/code&gt;, which is a normal function call and does accept a parameter, or raw string interpolation gated behind strict validation. We went with the second, gated on a UUID regex: the value either looks exactly like a UUID or the function throws before it ever touches SQL. I still don't love it stylistically, but there's no injection surface left once that regex has to match.&lt;/p&gt;

&lt;p&gt;Point either way: you're going to hit this wall the first time you try to parameterize a &lt;code&gt;SET&lt;/code&gt;, and it's not a bug in your code, it's just how the command works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #2: Drizzle's transaction type doesn't match its client type
&lt;/h2&gt;

&lt;p&gt;Drizzle's &lt;a href="https://orm.drizzle.team/docs/transactions" rel="noopener noreferrer"&gt;transaction callback&lt;/a&gt; hands you a &lt;code&gt;PostgresJsTransaction&lt;/code&gt;, and that type is not structurally the same as &lt;code&gt;PostgresJsDatabase&lt;/code&gt; — even though every method you'd actually call (&lt;code&gt;select&lt;/code&gt;, &lt;code&gt;insert&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;) is identical between them. If &lt;code&gt;AuthContext.tx&lt;/code&gt; gets typed as the transaction class, every shared data function downstream now needs that same internal type, which means importing a class Drizzle never really meant for you to reach into.&lt;/p&gt;

&lt;p&gt;We just centralized the cast once and moved on:&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;// Extract the tx type without importing internal Drizzle classes&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DrizzleTx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Parameters&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Parameters&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DbInstance&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transaction&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;gt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;castTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DrizzleTx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;DbInstance&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;tx&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;DbInstance&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;Now shared data-layer code just takes a &lt;code&gt;DbInstance&lt;/code&gt; and doesn't care if it's the root client, the admin client, or a live transaction. One cast, one comment explaining why, easy to delete later if Drizzle ever unifies the types properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #3: don't let a stream hold your transaction
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;withAuth&lt;/code&gt; holds a transaction — and the pooled connection under it — for as long as the request runs. For a JSON endpoint that returns in 40ms, fine. For a Server-Sent Events endpoint that stays open for twenty minutes, that's a problem: with a pool of 4, three people leaving a dashboard tab open exhausts it and everyone else starts timing out.&lt;/p&gt;

&lt;p&gt;We learned this the hard way and split off a separate wrapper for streaming routes — it authenticates, but deliberately never opens a transaction:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withAuthStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Short transaction for the initial state read&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;job&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;withOrgDb&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&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;tx&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="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;where&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="nx"&gt;jobs&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="nx"&gt;jobId&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;stream&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;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="cm"&gt;/* ...on each event, use withOrgDb() again for any query... */&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="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&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="s2"&gt;text/event-stream&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule we now follow: RLS context only lives as long as a transaction, so keep the transaction as short as the query needs — never as long as the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #4: knowing which connection you're on
&lt;/h2&gt;

&lt;p&gt;Two Drizzle instances means the failure mode shifted from "forgot a WHERE clause" to "grabbed the wrong client." We ended up needing a cheat sheet:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Connection&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tenant API route&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tx&lt;/code&gt; from &lt;code&gt;withAuth&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSE route&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;withOrgDb()&lt;/code&gt; per query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server component, tenant data&lt;/td&gt;
&lt;td&gt;&lt;code&gt;withOrgDb()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server component, auth tables only&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dbAdmin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin route&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dbAdmin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth library internals&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dbAdmin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migrations / background workers&lt;/td&gt;
&lt;td&gt;superuser&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The reassuring part: the mistake in the safe direction fails loudly. Query tenant tables through &lt;code&gt;db&lt;/code&gt; with no context set, and you get zero rows — a visibly broken feature in dev, not a security incident. Using &lt;code&gt;dbAdmin&lt;/code&gt; where you meant tenant scoping is the dangerous direction, and that's the one worth actually watching for in review. At least it's greppable.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>drizzle</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Fluctum: Open-Source Dynamic Pricing Solution for Stores Selling Spot-Priced Goods</title>
      <dc:creator>Michał Miler</dc:creator>
      <pubDate>Mon, 14 Sep 2026 06:35:04 +0000</pubDate>
      <link>https://dev.to/u11d/fluctum-open-source-dynamic-pricing-solution-for-stores-selling-spot-priced-goods-ani</link>
      <guid>https://dev.to/u11d/fluctum-open-source-dynamic-pricing-solution-for-stores-selling-spot-priced-goods-ani</guid>
      <description>&lt;p&gt;If you sell gold, silver, or anything else priced off a live market, you've probably already run into this problem: the price you show a customer and the actual market price start drifting apart the moment you set it. Update prices too rarely and you're either giving away margin or overcharging people - and both cost you customers eventually, just in different ways.&lt;/p&gt;

&lt;p&gt;Most store owners assume there are only two options: pay developers to build a custom pricing system, or keep manually updating prices and accept the risk. There's a third option that's easy to miss - using an existing, already-built open-source solution instead of starting from scratch.&lt;/p&gt;

&lt;p&gt;This post lays out what each path actually costs, in plain terms, so you can decide which one makes sense for your store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Problem Shows Up
&lt;/h2&gt;

&lt;p&gt;Precious metals dealers feel this first, because gold and silver prices move constantly and the margins are thin enough that a stale price matters. But the same problem shows up anywhere a product's fair price is tied to a live index rather than something you set once and leave alone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Precious and industrial metals&lt;/strong&gt; - bullion, plus copper, platinum, and palladium for wholesale operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B2B catalogs synced to an ERP&lt;/strong&gt; - pricing that has to reflect what your internal systems say right now, not what a database field said last week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Currency-sensitive goods&lt;/strong&gt; - high-value items where the price also has to move with exchange rates, not just the underlying market.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same logic extends further, too - anything priced off a live sale index rather than a fixed catalog value (think collectible trading cards or limited-run goods pegged to recent resale prices) runs into the same drift problem, even if the tooling for those categories is less mature today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Cost of Building This Yourself
&lt;/h2&gt;

&lt;p&gt;"Just show a live price" sounds simple, but a live-pricing checkout has to get several things right at once, and getting any one of them wrong costs real money:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pulling a live price feed reliably&lt;/strong&gt;, including handling the moments the feed is slow or unavailable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updating the price a customer sees in real time&lt;/strong&gt;, without expensive full-page reloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freezing the price at the right moment during checkout&lt;/strong&gt; - if the price can still move after the customer clicks "buy," you're back to the original problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refusing to complete an order if the frozen price has gone stale&lt;/strong&gt; - otherwise a customer could complete a purchase minutes after the quoted price expired.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are exotic, but each one is a place a custom build can quietly get wrong, and the mistakes are the expensive kind: a wrong price that either loses margin on every sale or drives away customers who feel overcharged. This is why a live-pricing checkout is realistically a multi-week engineering project, not a quick add-on - even though the end result looks simple to a shopper.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Ready-Made Solution Looks Like
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://fluctum.io/" rel="noopener noreferrer"&gt;Fluctum&lt;/a&gt; is a free, open-source plugin built on top of &lt;a href="https://medusajs.com/" rel="noopener noreferrer"&gt;Medusa&lt;/a&gt;, an open-source e-commerce platform. Because it's a Medusa plugin rather than a separate system, it drops into an existing Medusa store and keeps everything else Medusa already gives you - multi-region and multi-currency support, promotions, customer accounts, taxes, shipping, and payments - while adding live pricing on top. It's installed by a developer once, and after that it runs on its own.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqhclbzjei10nwemi1jxi.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqhclbzjei10nwemi1jxi.png" alt="landing-page.png" width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can try the live demo yourself at &lt;a href="https://fluctum.medusajs.site" rel="noopener noreferrer"&gt;fluctum.medusajs.site&lt;/a&gt; - add a product to the cart, watch the price update, and go through checkout to see the price lock in place.&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn69yjpxabrryl52hwrmr.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn69yjpxabrryl52hwrmr.png" alt="cart.png" width="800" height="384"&gt;&lt;/a&gt;&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi28chf9jer8qy7coamv7.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi28chf9jer8qy7coamv7.png" alt="checkout.png" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you choose?
&lt;/h2&gt;

&lt;p&gt;If you already have engineering resources and want a pricing system that's deeply custom to your business (unusual pricing rules, an existing in-house platform you can't move off of), building it yourself is a legitimate option - just budget for the ongoing maintenance, not only the initial build.&lt;/p&gt;

&lt;p&gt;For most stores, especially ones already running or considering Medusa, starting from a free, working solution and customizing it only where needed is the faster and lower-risk path. Because Fluctum is open-source and built on Medusa, you own the resulting codebase outright rather than licensing access to someone else's product - the same store can be extended to sync with an ERP system, connect to warehouse or inventory systems, switch or add payment providers like Stripe or PayPal, or bolt on whatever custom business logic your store needs later. You're never locked into someone else's roadmap if your store eventually needs something different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Build it yourself&lt;/th&gt;
&lt;th&gt;Use Fluctum (free, open-source)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to launch&lt;/td&gt;
&lt;td&gt;Months of developer time, plus testing&lt;/td&gt;
&lt;td&gt;Installed and configured in hours by a developer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ongoing maintenance&lt;/td&gt;
&lt;td&gt;Your team owns it - feed outages, checkout bugs, every future fix&lt;/td&gt;
&lt;td&gt;Maintained upstream; you install updates when you want them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upfront cost&lt;/td&gt;
&lt;td&gt;Developer time (custom-built, one-off)&lt;/td&gt;
&lt;td&gt;Free - MIT-licensed, no license fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Risk of a checkout pricing mistake&lt;/td&gt;
&lt;td&gt;You design and test the "lock the price at checkout" logic yourself&lt;/td&gt;
&lt;td&gt;Already built to handle the common failure modes (expired prices, stale quotes at payment time)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customization&lt;/td&gt;
&lt;td&gt;Full control, by definition&lt;/td&gt;
&lt;td&gt;Open-source and built on Medusa - you own the code, so you can extend it to ERP, storage/inventory, payments, or custom logic as your store needs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;Static prices and live markets don't mix, but that doesn't mean the fix has to be an expensive custom build. Fluctum gives Medusa stores selling precious metals - or any other spot-priced goods - a free, working starting point, with the option to extend it later if your business needs something more specific.&lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>medusajs</category>
      <category>preciousmetals</category>
    </item>
    <item>
      <title>How to get fixed IP addresses for an AWS ALB with Global Accelerator</title>
      <dc:creator>Paweł Swiridow</dc:creator>
      <pubDate>Wed, 09 Sep 2026 11:00:00 +0000</pubDate>
      <link>https://dev.to/u11d/how-to-get-fixed-ip-addresses-for-an-aws-alb-with-global-accelerator-oh8</link>
      <guid>https://dev.to/u11d/how-to-get-fixed-ip-addresses-for-an-aws-alb-with-global-accelerator-oh8</guid>
      <description>&lt;p&gt;An AWS Application Load Balancer gives you a DNS name, not client-facing addresses you can promise to another company forever. The practical workaround is to publish the two anycast IPv4 addresses from AWS Global Accelerator and route that traffic to the ALB behind the scenes.&lt;/p&gt;

&lt;p&gt;Application Load Balancers are great until someone asks, "What IP addresses should we whitelist?"&lt;/p&gt;

&lt;p&gt;That is where the fun starts. The addresses behind the ALB name can change as AWS scales and maintains the service. Telling a customer to whitelist whatever resolves today is not architecture. It is a calendar reminder for a future outage.&lt;/p&gt;

&lt;p&gt;This keeps the firewall conversation simple without replacing your load balancer, target groups, certificates, listener rules, or the other ALB machinery you already use.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Quick answer&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Use this pattern when the other side only accepts IP-based allowlists, but your service still needs ALB features such as HTTPS listeners, host routing, path routing, target groups, and health checks.&lt;/p&gt;

&lt;p&gt;Implementation checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a Global Accelerator accelerator.&lt;/li&gt;
&lt;li&gt;Add a TCP listener, usually on port 443.&lt;/li&gt;
&lt;li&gt;Create an endpoint group in the same Region as the ALB.&lt;/li&gt;
&lt;li&gt;Register the ALB as the endpoint.&lt;/li&gt;
&lt;li&gt;Send both accelerator IPs to the firewall owner.&lt;/li&gt;
&lt;li&gt;Add a Route 53 alias to the accelerator if clients can connect by name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern 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;Client network
  -&amp;gt; allowlist 2 Global Accelerator IPs
  -&amp;gt; AWS edge location
  -&amp;gt; Global Accelerator listener :443
  -&amp;gt; regional endpoint group
  -&amp;gt; Application Load Balancer
  -&amp;gt; targets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whitelisting, allowlisting, firewall exception, "please make networking stop yelling" - same practical problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why ALB alone does not work&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;An ALB is addressed by DNS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-api-1234567890.eu-west-1.elb.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That DNS name resolves to AWS-managed addresses. They are not yours, they are not fixed, and they are not a contract.&lt;/p&gt;

&lt;p&gt;You can put a friendly name in Route 53:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api.example.com -&amp;gt; my-api-1234567890.eu-west-1.elb.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That helps humans. It does not help a firewall that only accepts destination IPs.&lt;/p&gt;

&lt;p&gt;Do not solve this by resolving the ALB name and sending the current addresses to a customer. Those addresses can change. The customer will eventually block your production traffic and then everyone gets to pretend DNS was the surprising part.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How Global Accelerator gives an ALB fixed inbound IPs&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;AWS Global Accelerator creates a fixed entry point for your application.&lt;/p&gt;

&lt;p&gt;For an IPv4 accelerator, AWS gives you two static anycast IP addresses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;203.0.113.10
203.0.113.11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are the addresses your customer allowlists as outbound destinations. You can point your own DNS name at them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api.example.com -&amp;gt; Global Accelerator alias
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or, when the other side insists on raw IPs, hand them the two accelerator addresses.&lt;/p&gt;

&lt;p&gt;Global Accelerator can use an Application Load Balancer as a standard accelerator endpoint. It also monitors endpoint health and can route to another Region if you add one later.&lt;/p&gt;

&lt;p&gt;The important detail: the two Global Accelerator IPs are client-facing destination IPs. They are not the source IPs your ALB sees. Do not create an ALB security group rule that says "allow inbound from these two Global Accelerator IPs" and expect it to work. That is the common foot-gun, and it wears steel-toed boots.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Configure AWS Global Accelerator for an ALB with Terraform&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Assume you already have an ALB listening on HTTPS.&lt;/p&gt;

&lt;p&gt;The Global Accelerator pieces are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An accelerator&lt;/li&gt;
&lt;li&gt;A listener on TCP 443&lt;/li&gt;
&lt;li&gt;An endpoint group in the ALB Region&lt;/li&gt;
&lt;li&gt;The ALB as an endpoint
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="s2"&gt;"aws_globalaccelerator_accelerator"&lt;/span&gt; &lt;span class="s2"&gt;"api"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api-prod"&lt;/span&gt;
  &lt;span class="nx"&gt;ip_address_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"IPV4"&lt;/span&gt;
  &lt;span class="nx"&gt;enabled&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;attributes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;flow_logs_enabled&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="s2"&gt;"aws_globalaccelerator_listener"&lt;/span&gt; &lt;span class="s2"&gt;"https"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;accelerator_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_globalaccelerator_accelerator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"TCP"&lt;/span&gt;
  &lt;span class="nx"&gt;client_affinity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NONE"&lt;/span&gt;

  &lt;span class="nx"&gt;port_range&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="s2"&gt;"aws_globalaccelerator_endpoint_group"&lt;/span&gt; &lt;span class="s2"&gt;"eu_west_1"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;listener_arn&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_globalaccelerator_listener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;endpoint_group_region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-1"&lt;/span&gt;
  &lt;span class="nx"&gt;traffic_dial_percentage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;

  &lt;span class="nx"&gt;endpoint_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;endpoint_id&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="nx"&gt;weight&lt;/span&gt;                         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;
    &lt;span class="nx"&gt;client_ip_preservation_enabled&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;After&amp;nbsp;&lt;code&gt;terraform apply&lt;/code&gt;, output the addresses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="s2"&gt;"global_accelerator_ips"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_globalaccelerator_accelerator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip_sets&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;ip_addresses&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;global_accelerator_ips&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s2"&gt;"203.0.113.10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"203.0.113.11"&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;These are the addresses to give to the other side for inbound access to your service from their point of view. They allow outbound traffic to those destination IPs. You keep your ALB behind the accelerator.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Add DNS without losing the fixed IPs&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Create an alias record for the accelerator DNS name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="s2"&gt;"aws_route53_record"&lt;/span&gt; &lt;span class="s2"&gt;"api"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;zone_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route53_zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zone_id&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api.example.com"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt;

  &lt;span class="nx"&gt;alias&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_globalaccelerator_accelerator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dns_name&lt;/span&gt;
    &lt;span class="nx"&gt;zone_id&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_globalaccelerator_accelerator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hosted_zone_id&lt;/span&gt;
    &lt;span class="nx"&gt;evaluate_target_health&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;Customers that can allowlist by FQDN can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Customers that require IPs use the two accelerator addresses. Yes, both. Sending only one is how you turn high availability into a coin toss with extra paperwork.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Protect the ALB from direct access&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Global Accelerator solves the fixed destination IP problem. It does not automatically make an internet-facing ALB impossible to reach directly through its own DNS name.&lt;/p&gt;

&lt;p&gt;If the ALB is internet-facing, someone can still try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://my-api-1234567890.eu-west-1.elb.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That bypasses your pretty&amp;nbsp;&lt;code&gt;api.example.com&lt;/code&gt;&amp;nbsp;name and goes straight to the ALB. Security groups and AWS WAF still apply, but the request did not enter through the Global Accelerator IPs.&lt;/p&gt;

&lt;p&gt;The cleaner pattern is to use an internal ALB as the accelerator endpoint. Global Accelerator supports internal Application Load Balancer endpoints and creates elastic network interfaces in your VPC to deliver the traffic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="s2"&gt;"aws_lb"&lt;/span&gt; &lt;span class="s2"&gt;"api"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api-prod"&lt;/span&gt;
  &lt;span class="nx"&gt;internal&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;load_balancer_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"application"&lt;/span&gt;
  &lt;span class="nx"&gt;subnets&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;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnets&lt;/span&gt;
  &lt;span class="nx"&gt;security_groups&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then allow the application port on the ALB security group. With client IP preservation enabled for an ALB endpoint, your application can still read the original client address from&amp;nbsp;&lt;code&gt;X-Forwarded-For&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="s2"&gt;"aws_security_group_rule"&lt;/span&gt; &lt;span class="s2"&gt;"alb_https_in"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ingress"&lt;/span&gt;
  &lt;span class="nx"&gt;security_group_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
  &lt;span class="nx"&gt;from_port&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;
  &lt;span class="nx"&gt;to_port&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That rule may look too open, but the ALB is internal. The point is that the ALB does not have a public route of its own. Traffic arrives through Global Accelerator into the VPC path AWS creates for the endpoint.&lt;/p&gt;

&lt;p&gt;If you must keep the ALB internet-facing, add compensating controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use AWS WAF on the ALB.&lt;/li&gt;
&lt;li&gt;Require valid host headers.&lt;/li&gt;
&lt;li&gt;Require authentication at the application layer.&lt;/li&gt;
&lt;li&gt;Restrict known customer source ranges in the ALB security group if the client list is small and stable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not rely on obscuring the ALB DNS name. Obscurity is not a control. It is a shrug with a certificate.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Keep the real client IP&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For ALB endpoints, Global Accelerator and ALB preserve the original client IP in&amp;nbsp;&lt;code&gt;X-Forwarded-For&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Your application will receive a header like this:&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="err"&gt;X-Forwarded-For: 198.51.100.25
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there are multiple proxies, the header can contain multiple addresses:&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="err"&gt;X-Forwarded-For: 198.51.100.25, 10.0.12.34
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure your app or ingress layer knows which proxy chain it trusts. Blindly trusting every&amp;nbsp;&lt;code&gt;X-Forwarded-For&lt;/code&gt;&amp;nbsp;value from the internet is how logs become fan fiction.&lt;/p&gt;

&lt;p&gt;For nginx behind the ALB, configure trusted proxy ranges for your VPC or ALB layer, not the whole internet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;set_real_ip_from&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="s"&gt;.0.0/8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;real_ip_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;real_ip_recursive&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adjust the CIDR to your actual network.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Verify the fixed IP setup&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Get the accelerator IPs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws globalaccelerator list-accelerators &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Accelerators[?Name==`api-prod`].IpSets[].IpAddresses[]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test HTTPS against each IP while sending the expected host header:&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;-v&lt;/span&gt; &lt;span class="nt"&gt;--resolve&lt;/span&gt; api.example.com:443:203.0.113.10 https://api.example.com/health
curl &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--resolve&lt;/span&gt; api.example.com:443:203.0.113.11 https://api.example.com/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the accelerator health:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws globalaccelerator describe-endpoint-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--endpoint-group-arn&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ENDPOINT_GROUP_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'EndpointGroup.EndpointDescriptions[*].{EndpointId:EndpointId,HealthState:HealthState,Weight:Weight}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check your application logs for the client IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'X-Forwarded-For'&lt;/span&gt; /var/log/app/access.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or query ALB access logs in Athena:&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;client_ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;request_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;target_status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nb"&gt;time&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;alb_logs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;request_url&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%/health%'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Troubleshooting common errors&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If traffic does not reach the ALB, check these first:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Symptom&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Likely cause&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;What to check&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;curl --resolve&lt;/code&gt;&amp;nbsp;times out&lt;/td&gt;
&lt;td&gt;The accelerator endpoint is unhealthy or the ALB security group blocks traffic&lt;/td&gt;
&lt;td&gt;Check&amp;nbsp;&lt;code&gt;describe-endpoint-group&lt;/code&gt;, ALB listener rules, target group health, and security group ingress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS certificate mismatch&lt;/td&gt;
&lt;td&gt;The request reached the ALB with the wrong host name&lt;/td&gt;
&lt;td&gt;Use&amp;nbsp;&lt;code&gt;--resolve api.example.com:443:&amp;lt;accelerator-ip&amp;gt;&lt;/code&gt;&amp;nbsp;and make sure the ALB certificate covers&amp;nbsp;&lt;code&gt;api.example.com&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer allowlisted one IP but traffic still fails sometimes&lt;/td&gt;
&lt;td&gt;Only one Global Accelerator IP was added to the firewall&lt;/td&gt;
&lt;td&gt;Give them both accelerator IPs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ALB is still reachable directly&lt;/td&gt;
&lt;td&gt;The ALB is internet-facing&lt;/td&gt;
&lt;td&gt;Use an internal ALB endpoint or add WAF, host-header checks, authentication, and source restrictions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App logs show a proxy IP instead of the client IP&lt;/td&gt;
&lt;td&gt;The app does not trust the right proxy chain&lt;/td&gt;
&lt;td&gt;Configure trusted proxy ranges and read&amp;nbsp;&lt;code&gt;X-Forwarded-For&lt;/code&gt;&amp;nbsp;correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most common mistake is treating the two Global Accelerator IPs as source IPs for the ALB security group. They are destination IPs for clients. Your ALB will not see requests as coming from those two addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Multi-Region is the obvious next step&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;You do not need multiple Regions to justify Global Accelerator. Fixed ingress IPs are already enough.&lt;/p&gt;

&lt;p&gt;But once it is in place, adding another ALB in another Region is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="s2"&gt;"aws_globalaccelerator_endpoint_group"&lt;/span&gt; &lt;span class="s2"&gt;"us_east_1"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;listener_arn&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_globalaccelerator_listener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;endpoint_group_region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
  &lt;span class="nx"&gt;traffic_dial_percentage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="nx"&gt;endpoint_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;endpoint_id&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_us_east_1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="nx"&gt;weight&lt;/span&gt;                         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;
    &lt;span class="nx"&gt;client_ip_preservation_enabled&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;Start with&amp;nbsp;&lt;code&gt;traffic_dial_percentage = 0&lt;/code&gt;, validate the stack, then dial traffic up when you are ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;traffic_dial_percentage&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful for migrations, blue-green deployments, and disaster recovery plans that contain fewer hopes and prayers.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Things to remember&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Global Accelerator is a good fit when external parties need stable destination IPs for your AWS-hosted service.&lt;/p&gt;

&lt;p&gt;Use this pattern when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clients must allowlist fixed IPs.&lt;/li&gt;
&lt;li&gt;You want an ALB behind those fixed IPs.&lt;/li&gt;
&lt;li&gt;You may want regional failover later.&lt;/li&gt;
&lt;li&gt;You want to avoid managing proxy instances or static Elastic IP hacks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be clear about what it does not do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It does not give the ALB fixed IPs.&lt;/li&gt;
&lt;li&gt;It does not make an internet-facing ALB private.&lt;/li&gt;
&lt;li&gt;It does not let you use the two accelerator IPs as ALB security group sources.&lt;/li&gt;
&lt;li&gt;It does not replace application authentication or WAF.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The short version: if you need fixed IP addresses for an AWS ALB, use AWS Global Accelerator. ALB gives you flexible HTTP load balancing. Global Accelerator gives you two stable anycast IPs in front of it. Put them together when the network team on the other side says, "We need IPs, not DNS," and you would prefer not to build your own little networking sadness machine.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>aws</category>
      <category>infrastructure</category>
      <category>networking</category>
    </item>
    <item>
      <title>Building a Multilingual Shop with Next.js</title>
      <dc:creator>Paweł Sobolewski</dc:creator>
      <pubDate>Tue, 01 Sep 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/u11d/building-a-multilingual-shop-with-nextjs-47ck</link>
      <guid>https://dev.to/u11d/building-a-multilingual-shop-with-nextjs-47ck</guid>
      <description>&lt;p&gt;Adding multiple languages to a web application often starts with a simple step: replacing hardcoded strings with translation keys and adding a language switcher.&lt;/p&gt;

&lt;p&gt;In an e-commerce application, however, internationalization quickly becomes a broader architectural challenge.&lt;/p&gt;

&lt;p&gt;Language affects far more than the text displayed on the page: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It becomes part of the URL,&lt;/li&gt;
&lt;li&gt;it influences routing and redirects,&lt;/li&gt;
&lt;li&gt;it determines which translation resources are loaded,&lt;/li&gt;
&lt;li&gt;it affects server and client rendering,&lt;/li&gt;
&lt;li&gt;it changes how data such as prices and dates should be formatted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, we’ll look at how to approach these problems in a &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; application using the App Router. Rather than focusing on the basics of translating strings, we’ll focus on the technical decisions behind the implementation: how the language is detected, how it’s propagated through the application, how translations are loaded on the server and client, and how localized routes are handled.&lt;/p&gt;

&lt;p&gt;The implementation presented here is based on a small e-commerce shop, with a product page as its main piece of domain content. It supports English, Polish, and German, and it intentionally stays focused on the internationalization layer rather than on building a complete commerce platform.&lt;/p&gt;

&lt;p&gt;The goal is to use this relatively simple application to explore the architecture behind multilingual Next.js applications and the trade-offs involved in choosing and implementing an i18n solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Remarks
&lt;/h2&gt;

&lt;p&gt;Before diving into the implementation, it’s worth noting a few deliberate simplifications in this project.&lt;/p&gt;

&lt;p&gt;The repository is intentionally designed to demonstrate the technical aspects of internationalization in Next.js, not to provide a complete, production-ready e-commerce application. As a result, some parts of the implementation are intentionally simplified.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/u11d-com/blog_nextjs-i18n" rel="noopener noreferrer"&gt;Repository link&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Product data is part of the translation resources
&lt;/h3&gt;

&lt;p&gt;To keep the example self-contained, product data such as names and descriptions is stored alongside the translation resources.&lt;/p&gt;

&lt;p&gt;In a real e-commerce application, this data would typically come from a domain-specific API or commerce backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product page
    ↓
Commerce API
    ↓
Product data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In that setup, the i18n layer would primarily handle UI translations, while localized product content would be part of the product data model.&lt;/p&gt;

&lt;h3&gt;
  
  
  All languages are stored in a single file
&lt;/h3&gt;

&lt;p&gt;The example keeps translations for all supported languages in one file to make the structure easier to explore.&lt;/p&gt;

&lt;p&gt;In a larger application, each language would typically have its own resource files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;languages/
├── en/
│   ├── common.json
│   ├── home.json
│   └── product.json
├── pl/
│   ├── common.json
│   ├── home.json
│   └── product.json
└── de/
    ├── common.json
    ├── home.json
    └── product.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes translation files easier to manage independently and fits better with external localization workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  No external translation management system
&lt;/h3&gt;

&lt;p&gt;The project uses local resources and an API route to demonstrate the resource-loading boundary.&lt;/p&gt;

&lt;p&gt;In a production application, you might instead use a CMS, a translation management platform, or another external service. The &lt;code&gt;resourceLoader&lt;/code&gt; abstraction is intentionally designed so the underlying source can be swapped out without changing the components that consume translations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing an i18n Solution for Next.js
&lt;/h2&gt;

&lt;p&gt;Before implementing internationalization, the first step is choosing the right tool.&lt;/p&gt;

&lt;p&gt;Next.js provides the &lt;a href="https://nextjs.org/docs/app/guides/internationalization" rel="noopener noreferrer"&gt;routing primitives&lt;/a&gt; needed to build a multilingual application, but it doesn’t prescribe a translation library. That leaves several options: building a small custom solution, using a Next.js-focused library such as &lt;a href="https://github.com/amannn/next-intl" rel="noopener noreferrer"&gt;&lt;code&gt;next-intl&lt;/code&gt;&lt;/a&gt;, or adopting the broader i18next ecosystem through &lt;a href="https://github.com/i18next/next-i18next" rel="noopener noreferrer"&gt;&lt;code&gt;next-i18next&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For this project, I chose &lt;code&gt;next-i18next&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That decision wasn’t based solely on the number of translated strings. The application needs an i18n layer that works across Server and Client Components, supports namespaces, and gives us control over how translation resources are loaded. These are important capabilities for a shop where translations may eventually move from local files to an external CMS or another backend service.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;next-i18next&lt;/code&gt; is built on top of &lt;a href="https://www.i18next.com/" rel="noopener noreferrer"&gt;&lt;code&gt;i18next&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://react.i18next.com/" rel="noopener noreferrer"&gt;&lt;code&gt;react-i18next&lt;/code&gt;&lt;/a&gt;, while adding the Next.js-specific integration. The version used in this project supports the App Router, including Server Components, Client Components, and proxy-based language detection*&lt;em&gt;.&lt;/em&gt;* It also provides separate APIs such as &lt;code&gt;getT()&lt;/code&gt; for Server Components and &lt;code&gt;useT()&lt;/code&gt; for Client Components.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not build it ourselves?
&lt;/h3&gt;

&lt;p&gt;A custom implementation might initially look attractive. With a small application, a simple dictionary and a React context can be enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;translations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;en&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="na"&gt;pl&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="na"&gt;de&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is that translation lookup is only one part of the equation.&lt;/p&gt;

&lt;p&gt;Once the application needs server-side translations, client-side hydration, namespaces, fallback languages, language detection, or external translation resources, the custom solution starts accumulating infrastructure that an established i18n library already provides.&lt;/p&gt;

&lt;p&gt;There’s another important advantage: a mature i18n library gives us much more than a simple &lt;code&gt;key → string&lt;/code&gt; lookup.&lt;/p&gt;

&lt;p&gt;For example, i18next provides built-in support for &lt;strong&gt;interpolation&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;welcome&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;John&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;It also supports &lt;a href="https://www.i18next.com/translation-function/plurals" rel="noopener noreferrer"&gt;pluralization&lt;/a&gt; without requiring us to implement language-specific rules ourselves, and &lt;code&gt;react-i18next&lt;/code&gt; provides the &lt;a href="https://react.i18next.com/latest/trans-component" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;Trans&amp;gt;&lt;/code&gt;&lt;/a&gt; component for translations that contain React elements or more complex markup.&lt;/p&gt;

&lt;p&gt;These features may seem like minor conveniences at first, but they quickly become important as the number and complexity of translations grow. Pluralization is particularly difficult to implement correctly because different languages can have completely different grammatical rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Language-Based Routing
&lt;/h2&gt;

&lt;p&gt;Once we have chosen the i18n library, the next decision is where the current language should live.&lt;/p&gt;

&lt;p&gt;For this application, the &lt;strong&gt;language is part of the URL&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/en
/pl
/de
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same applies to nested routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/en/product/1
/pl/product/1
/de/product/1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is implemented using a dynamic segment in the App Router:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
└── [lng]/
    ├── page.tsx
    └── product/
        └── [id]/
            └── page.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;[lng]&lt;/code&gt; segment becomes a route parameter, so a product page receives both the language and the product identifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&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="nx"&gt;lng&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;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A small App Router detail:&lt;/strong&gt; Starting with Next.js 16.3.0, you can also access route parameters via &lt;code&gt;next/root-params&lt;/code&gt;. For example, instead of passing &lt;code&gt;lng&lt;/code&gt; through multiple component levels, a component can use:&lt;/p&gt;


&lt;pre class="highlight jsx"&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;lng&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/root-params&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;p&gt;This can reduce prop drilling when deeper components need the language. It’s especially useful for components that rely on route parameters but don’t otherwise need props from their parent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Why put the language in the URL?
&lt;/h3&gt;

&lt;p&gt;The first benefit is that each language has its own URL.&lt;/p&gt;

&lt;p&gt;A user can navigate directly to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/pl/product/1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the server immediately knows which language to use to render the page.&lt;/p&gt;

&lt;p&gt;The URL is also shareable and bookmarkable. If someone sends a Polish product page to another user, they’re sharing the Polish version explicitly rather than relying on the recipient’s browser preferences.&lt;/p&gt;

&lt;p&gt;There is also a clear relationship between URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/en/product/1
/pl/product/1
/de/product/1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They represent the same resource in different languages.&lt;/p&gt;

&lt;p&gt;This is especially useful for SEO, where language-specific URLs can be indexed and connected using mechanisms such as &lt;code&gt;hreflang&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting the User's Language with Proxy
&lt;/h2&gt;

&lt;p&gt;Putting the language in the URL solves the routing problem, but it raises another question: what should happen when a user visits the application without specifying a language?&lt;/p&gt;

&lt;p&gt;For example:&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="err"&gt;GET /
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no language in the URL, so the application needs a way to determine the most appropriate one.&lt;/p&gt;

&lt;p&gt;In this project, that logic lives in the Next.js Proxy and follows a simple priority order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;URL
 ↓
Cookie
 ↓
Accept-Language
 ↓
Fallback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key point is that these sources are not treated equally.&lt;/p&gt;

&lt;h3&gt;
  
  
  URL comes first
&lt;/h3&gt;

&lt;p&gt;If the requested URL already contains a supported language, we treat it as an explicit user choice.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/pl/product/1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;should always be rendered in Polish, even if the user's browser prefers German.&lt;/p&gt;

&lt;p&gt;The Proxy first extracts the first segment of the pathname and checks whether it is one of the supported languages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&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;urlLanguage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;supportedLanguages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlLanguage&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&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 a valid language is found, the request can continue normally.&lt;/p&gt;

&lt;p&gt;The Proxy also updates the language cookie to remember the user's explicit choice for subsequent visits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cookie as persistent preference
&lt;/h3&gt;

&lt;p&gt;If there is no language in the URL, the next source is the language cookie.&lt;/p&gt;

&lt;p&gt;The cookie allows us to remember a language selected during a previous visit without requiring the user to select it again.&lt;/p&gt;

&lt;p&gt;However, the cookie is deliberately lower in the priority chain than the URL.&lt;/p&gt;

&lt;p&gt;This means that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;URL&lt;/span&gt;:    /&lt;span class="n"&gt;de&lt;/span&gt;
&lt;span class="n"&gt;Cookie&lt;/span&gt;: &lt;span class="n"&gt;pl&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;results in German.&lt;/p&gt;

&lt;p&gt;The URL represents the current request, while the cookie represents a previously stored preference.&lt;/p&gt;

&lt;p&gt;Keeping this distinction prevents a stale cookie from overriding an explicit URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Falling back to &lt;code&gt;Accept-Language&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;If neither the URL nor the cookie provides a supported language, we can use the browser's &lt;code&gt;Accept-Language&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;A browser might send something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pl-PL,pl;q=0.9,en-US;q=0.8,en;q=0.7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application extracts the language codes and looks for the first one it supports.&lt;/p&gt;

&lt;p&gt;For this project, the supported languages are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;en
pl
de
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;so the example header would result in:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;It is important to treat &lt;code&gt;Accept-Language&lt;/code&gt; as a &lt;strong&gt;preference&lt;/strong&gt;, rather than as a guaranteed language selection. The browser may request a language that the application does not support.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finally, use a fallback
&lt;/h3&gt;

&lt;p&gt;There is always a case where none of the detected languages are supported.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;URL&lt;/span&gt;:    /
&lt;span class="n"&gt;Cookie&lt;/span&gt;: &lt;span class="n"&gt;fr&lt;/span&gt;
&lt;span class="n"&gt;Header&lt;/span&gt;: &lt;span class="n"&gt;ja&lt;/span&gt;-&lt;span class="n"&gt;JP&lt;/span&gt;,&lt;span class="n"&gt;ja&lt;/span&gt;;&lt;span class="n"&gt;q&lt;/span&gt;=&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;9&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the application only supports English, Polish and German, there is no match.&lt;/p&gt;

&lt;p&gt;This is where the configured fallback language is used:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Translation Resources and Namespaces
&lt;/h2&gt;

&lt;p&gt;Once the language is established, the next question is how the application retrieves the translations.&lt;/p&gt;

&lt;p&gt;The project uses three namespaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;common
home
product
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rather than keeping every translation in a single large dictionary, namespaces let us group resources by responsibility.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;common
├── navigation
├── language switcher
└── shared UI

home
├── hero
└── product grid

product
├── product details
├── add to cart
└── stock information
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This becomes increasingly useful as the application grows. A single translation file works well at first, but over time it becomes harder to maintain and to understand which part of the application owns a particular key.&lt;/p&gt;

&lt;p&gt;Namespaces also provide a useful loading boundary. A product page doesn’t necessarily need every translation used by the homepage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loading resources through &lt;code&gt;resourceLoader&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The interesting part of this implementation is how those resources are loaded.&lt;/p&gt;

&lt;p&gt;Instead of importing the translation JSON directly into the application, &lt;code&gt;next-i18next&lt;/code&gt; is configured with a custom &lt;code&gt;resourceLoader&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;resourceLoader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;namespace&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;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3000&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/fetchTranslations?lang=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;ns=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;namespace&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="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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`Failed to load translations: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;language&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;namespace&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why introduce an API layer?
&lt;/h3&gt;

&lt;p&gt;For local JSON files, calling an API might seem unnecessary—we could simply import them.&lt;/p&gt;

&lt;p&gt;The API is included here to illustrate a more realistic setup, where translations are managed outside the application bundle.&lt;/p&gt;

&lt;p&gt;Imagine replacing the current implementation with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resourceLoader
     ↓
    CMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resourceLoader
     ↓
translation service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the application can remain unchanged.&lt;/p&gt;

&lt;p&gt;This separation is especially useful for e-commerce applications, where translations are often managed by content or localization teams rather than developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server and Client Components
&lt;/h2&gt;

&lt;p&gt;With the language and translation resources in place, we still need to answer one key question: how do Server Components and Client Components access translations?&lt;/p&gt;

&lt;p&gt;The App Router provides two execution environments, and &lt;code&gt;next-i18next&lt;/code&gt; offers an API for each.&lt;/p&gt;

&lt;p&gt;On the server, translations can be accessed with &lt;code&gt;getT()&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;t&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="nf"&gt;getT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;product&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;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&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;On the client, translations are accessed through &lt;code&gt;useT()&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;common&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&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;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;addToCart&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;/button&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distinction matters because using translations doesn’t automatically mean a component needs to become a Client Component.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep translated content on the server
&lt;/h3&gt;

&lt;p&gt;A product page is a good example.&lt;/p&gt;

&lt;p&gt;There’s usually no reason to move the entire page to the client just because it contains translated text. The product name, description, labels, and other static content can be translated as the page is rendered on the server.&lt;/p&gt;

&lt;p&gt;That way, the application can send translated HTML in the initial response instead of waiting for client-side JavaScript to initialize the translation layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Providing the resources from the layout
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;[lng]/layout.tsx&lt;/code&gt; file is where the server-loaded translation resources are passed into the client-side i18n context.&lt;/p&gt;

&lt;p&gt;The layout initializes the server-side i18n instance and obtains the loaded resources:&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;i18n&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="nf"&gt;getT&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;resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getResources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those resources are then passed to the &lt;strong&gt;provider&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;I18nProvider&lt;/span&gt;
  &lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;resources&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;children&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;/I18nProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The layout therefore establishes the translation context once for the entire language-specific part of the application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this separation matters
&lt;/h3&gt;

&lt;p&gt;With the App Router, we should generally default to Server Components.&lt;/p&gt;

&lt;p&gt;The reason is simple: turning a component into a Client Component moves its logic into the browser and can add JavaScript that isn’t otherwise necessary.&lt;/p&gt;

&lt;p&gt;Internationalization shouldn’t be the reason to do that.&lt;/p&gt;

&lt;p&gt;If a component only needs to render translated content, it can remain a Server Component and use &lt;code&gt;getT()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useT()&lt;/code&gt; should be reserved for components that already need to run on the client — for example, because they handle user interaction, browser events, local state, or other client-side behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Formatting Prices
&lt;/h2&gt;

&lt;p&gt;Internationalization is not limited to translating text. Numbers, dates, currencies, and other values often need to be formatted according to the user's language as well.&lt;/p&gt;

&lt;p&gt;Prices are a simple example.&lt;/p&gt;

&lt;p&gt;The same value:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;can be displayed differently depending on the language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$1,999
1 999 zł
1.999 €
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this project, the language used in the URL is a short code such as &lt;code&gt;en&lt;/code&gt;, &lt;code&gt;pl&lt;/code&gt;, or &lt;code&gt;de&lt;/code&gt;. However, the &lt;code&gt;Intl&lt;/code&gt; APIs work with more specific locale identifiers, such as &lt;code&gt;en-US&lt;/code&gt; or &lt;code&gt;pl-PL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;formatPrice&lt;/code&gt; utility handles this mapping:&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;LOCALE_CURRENCY&lt;/span&gt;&lt;span class="p"&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PLN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;de&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EUR&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;LOCALE_BCP47&lt;/span&gt;&lt;span class="p"&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pl-PL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;de&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;de-DE&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;There are two separate mappings here.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LOCALE_CURRENCY&lt;/code&gt; determines which currency should be displayed, while &lt;code&gt;LOCALE_BCP47&lt;/code&gt; determines the regional formatting conventions used by &lt;code&gt;Intl.NumberFormat&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The actual formatting is then kept in a small utility:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&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="nx"&gt;language&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="kr"&gt;string&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;currency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;LOCALE_CURRENCY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&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;bcp47&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;LOCALE_BCP47&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US&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="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NumberFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bcp47&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;currency&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maximumFractionDigits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&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 gives components a simple API:&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="nf"&gt;formatPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without requiring them to know anything about currencies, BCP 47 locale identifiers, or &lt;code&gt;Intl.NumberFormat&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Localized Navigation
&lt;/h2&gt;

&lt;p&gt;As language becomes part of the URL, navigation is one of the first areas where i18n can start leaking into your application code.&lt;/p&gt;

&lt;p&gt;Without an abstraction, every link needs to be aware of the current language:&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Link&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&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;lng&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/product/&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;`&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;Product&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Link&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works, but it couples the component directly to the routing structure.&lt;/p&gt;

&lt;p&gt;It also gets repetitive: every developer has to remember that the first URL segment represents the current language.&lt;/p&gt;

&lt;p&gt;To avoid this, the project introduces a &lt;code&gt;LocalizedLink&lt;/code&gt; component.&lt;/p&gt;

&lt;h3&gt;
  
  
  LocalizedLink
&lt;/h3&gt;

&lt;p&gt;The idea is simple: components provide a path relative to the language root, and &lt;code&gt;LocalizedLink&lt;/code&gt; adds the current language automatically.&lt;/p&gt;

&lt;p&gt;Instead of:&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Link&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&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;lng&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/product/&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;`&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;Product&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Link&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we can write:&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LocalizedLink&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`/product/&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;`&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;Product&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/LocalizedLink&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the current language is Polish, the resulting URL is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/pl/product/1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For German, it becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/de/product/1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, the component hides a routing detail that’s irrelevant to the component using it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Switching between languages
&lt;/h3&gt;

&lt;p&gt;The language switcher is a slightly different case.&lt;/p&gt;

&lt;p&gt;Here, we want to change the language while preserving the current route.&lt;/p&gt;

&lt;p&gt;For example, if the user is currently on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/pl/product/1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;switching to German should result in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/de/product/1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than simply navigating to &lt;code&gt;/de&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The switcher can therefore treat the current pathname as a resource-independent path and replace only the language segment.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Current URL
/pl/product/1
      │
      │ replace language
      ▼
/de/product/1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is important for product pages because changing the language should not interrupt the user's current context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;p&gt;The implementation covers the core architecture, but a production application would still need to address a few additional concerns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caching translation resources
&lt;/h3&gt;

&lt;p&gt;Translation resources are usually much more stable than application data. Fetching the same translations on every request would therefore be wasteful.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;resourceLoader&lt;/code&gt; is a natural place to introduce caching and revalidation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Component
    ↓
next-i18next
    ↓
resourceLoader
    ↓
cache
    ↓
translation source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact strategy depends on where the translations are stored. With a CMS or external translation service, the application could use Next.js caching and revalidation to avoid unnecessary requests while still allowing updated translations to reach users without rebuilding the entire application.&lt;/p&gt;

&lt;h3&gt;
  
  
  SEO and &lt;code&gt;hreflang&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Language-specific URLs also make SEO an important consideration.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/en/product/1
/pl/product/1
/de/product/1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These URLs represent different language versions of the same page. A production application should expose this relationship to search engines using appropriate &lt;code&gt;hreflang&lt;/code&gt; metadata.&lt;/p&gt;

&lt;p&gt;Canonical URLs should also be generated consistently so that each language version points to itself as the canonical URL, while the &lt;code&gt;hreflang&lt;/code&gt; links connect the available alternatives.&lt;/p&gt;

&lt;p&gt;The routing architecture already provides a clear URL for each language version. SEO metadata can build on top of that structure instead of inferring the language from cookies or client-side state.&lt;/p&gt;

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

&lt;p&gt;Building a multilingual application with Next.js isn’t just about translating strings. Language affects routing, request handling, resource loading, rendering, and navigation, so internationalization quickly becomes an architectural concern.&lt;/p&gt;

&lt;p&gt;The implementation presented here addresses those concerns at well-defined boundaries. The language is part of the URL, Proxy handles language detection, &lt;code&gt;next-i18next&lt;/code&gt; provides the translation layer, and Server and Client Components use different APIs depending on where translations are needed. Application-specific abstractions such as &lt;code&gt;LocalizedLink&lt;/code&gt; then keep language-related details out of regular components.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>nextjs</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
