<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <!-- Source: https://blog.platformatic.dev/rss.xml -->
  <channel>
    <title><![CDATA[Platformatic Blog]]></title>
    <description><![CDATA[Platformatic Blog]]></description>
    <link>https://siftrss.com/f/3XL6Qd79bM</link>
    <image>
      <url>https://cdn.hashnode.com/res/hashnode/image/upload/v1727183405468/9f1d8161-aee0-4422-af77-111e9ea87aef.png</url>
      <title>Platformatic Blog</title>
      <link>https://blog.platformatic.dev</link>
    </image>
    <generator>RSS for Node</generator>
    <lastBuildDate>Sat, 10 Oct 2026 16:53:54 GMT</lastBuildDate>
    <atom:link href="https://siftrss.com/f/3XL6Qd79bM" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <ttl>60</ttl>
    <item>
      <title><![CDATA[Stop Request Stampedes at the Gateway with Platformatic Deduplication]]></title>
      <description><![CDATA[Picture an online store launching a new product and sending out a mailing list campaign. Thousands of users click the same link at once. The product page, built with a Node.js app like Next.js, needs ]]></description>
      <link>https://blog.platformatic.dev/gateway-request-deduplication-nodejs</link>
      <guid isPermaLink="true">https://blog.platformatic.dev/gateway-request-deduplication-nodejs</guid>
      <category><![CDATA[Node.js]]></category>
      <category><![CDATA[api]]></category>
      <category><![CDATA[platformatic]]></category>
      <category><![CDATA[performance]]></category>
      <category><![CDATA[Next.js]]></category>
      <category><![CDATA[caching]]></category>
      <category><![CDATA[Devops]]></category>
      <category><![CDATA[scalability]]></category>
      <dc:creator><![CDATA[Paolo Insogna]]></dc:creator>
      <pubDate>Tue, 30 Jun 2026 14:38:38 GMT</pubDate>
      <enclosure url="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/bf8ab33e-f186-408d-b099-2cff8747d25f.png" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>Picture an online store launching a new product and sending out a mailing list campaign. Thousands of users click the same link at once. The product page, built with a Node.js app like Next.js, needs to fetch the same product details, inventory, recommendations, and pricing for each request.</p>
<p>If the page is cached, everything works smoothly. Problems begin when the cache is empty, expired, or being refreshed. Then, the first group of users all miss the cache together, and every request asks the app to generate the same response. (This same thing would also happen with a trending news story, a search crawler, frontend prefetching, or after a cache reset.)</p>
<p>This situation is known as the “thundering herd” problem. Every user request is valid, but the work gets repeated. Your app uses CPU, database, and network resources to calculate the same result over and over, just when response times are already strained.</p>
<p>Rather than sending all traffic straight to your app, wouldn’t it be great if you could place a gateway in front that spots duplicate in-flight reads and combines them before they hit Node.js?</p>
<p>Platformatic Gateway now does exactly this. With request deduplication, it merges concurrent requests for the same data. Only one goes upstream, while the others wait and then get the same response.</p>
<p>This is not a replacement for caching. Instead, it acts as a short-term coordination layer for requests in progress. The cache handles future requests, while deduplication shields your app while the first response is still being generated.</p>
<p>This is especially important for self-hosted Next.js apps. A popular route can cause heavy server rendering, React Server Component processing, image metadata checks, or backend API calls. When many users hit that route at once, or during cache refreshes, deduplication stops the gateway from sending the same work to Next.js over and over.</p>
<hr />
<h2><strong>A Local Benchmark</strong></h2>
<p>To see how much this helps, we ran a simple local test with a purposely slow route behind a proxy. The upstream route waited 100 ms before responding, simulating a page or API call that needs backend work. The test used the same leader/waiter pattern as gateway deduplication and sent 100 requests at once to the same URL.</p>
<p>These are the median numbers from three runs:</p>
<table style="width:685px"><colgroup><col style="width:142px"></col><col style="width:101px"></col><col style="width:114px"></col><col style="width:114px"></col><col style="width:123px"></col><col style="width:91px"></col></colgroup><tbody><tr><td><p><strong>Scenario</strong></p></td><td><p><strong>Client requests</strong></p></td><td><p><strong>Upstream requests</strong></p></td><td><p><strong>Average latency</strong></p></td><td><p><strong>p99 latency</strong></p></td><td><p><strong>Errors</strong></p></td></tr><tr><td><p>Without deduplication</p></td><td><p>1,000</p></td><td><p>1,000</p></td><td><p>111.31 ms</p></td><td><p>134 ms</p></td><td><p>0</p></td></tr><tr><td><p>With deduplication</p></td><td><p>1,000</p></td><td><p>10</p></td><td><p>104.88 ms</p></td><td><p>127 ms</p></td><td><p>0</p></td></tr><tr><td><p>Without deduplication</p></td><td><p>10,000</p></td><td><p>10,000</p></td><td><p>104.80 ms</p></td><td><p>122 ms</p></td><td><p>0</p></td></tr><tr><td><p>With deduplication</p></td><td><p>10,000</p></td><td><p>100</p></td><td><p>102.91 ms</p></td><td><p>106 ms</p></td><td><p>0</p></td></tr></tbody></table>

<p>The key result isn’t the slight change in average latency, but the number of upstream requests. With deduplication, the proxy still answers every client, but the upstream app only processes one response per wave of requests. In a test with 10,000 requests, that meant just 100 upstream responses instead of 10,000. In real situations, this can mean the difference between a burst that overwhelms your app and one that the gateway handles smoothly.</p>
<hr />
<h2><strong>How It Works</strong></h2>
<p>Gateway deduplication uses a leader/waiter model that is easy to reason about in production.</p>
<p>The first matching request becomes the leader. It acquires a lock, goes to the upstream application, buffers the response, stores it for a short time, and notifies any waiters. Concurrent requests with the same key become waiters. They do not call the upstream service immediately; instead, they wait for the leader response and replay it when it becomes available.</p>
<p>In a production gateway, deduplication often sits next to caching. You can use separate Valkey instances or separate key prefixes in the same Valkey deployment, but the two stores serve different purposes: cache storage keeps reusable responses, while deduplication storage keeps short-lived locks and response buffers for in-flight requests.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/87504c18-e5e8-4763-a428-95b8508848ea.png" alt="" style="display:block;margin:0 auto" />

<p>To prevent deadlocks, every coordination point has an expiration. The leader lock uses a <code>lockTtl</code>, waiters have a <code>timeout</code>, and retries are limited. If the leader fails, the lock expires, a waiter times out, or retries run out, the request switches back to normal proxying. This fallback is intentional—deduplication is meant to lower load, not cause requests to get stuck during traffic spikes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/9fe7db00-4342-4464-b99c-f16306f36587.png" alt="" style="display:block;margin:0 auto" />

<p>For a single request, the decision path looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/60845246-b8d2-4ce5-8d12-2707ea319098.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2><strong>The Simplest Configuration</strong></h2>
<p>Enable deduplication globally under <code>gateway.deduplication:</code></p>
<pre><code class="language-json">{
  "gateway": {
    "deduplication": {
      "enabled": true
    },
    "applications": [
      {
        "id": "frontend",
        "proxy": {
          "prefix": "/"
        }
      }
    ]
  }
}
</code></pre>
<p>By default, deduplication works for <code>GET</code> and <code>HEAD</code> requests and uses <code>memory</code> for storage.</p>
<p>This default is intentionally cautious. <code>GET</code> and <code>HEAD</code> are the safest types of requests to deduplicate. Write requests often need custom rules before they can be safely coordinated.</p>
<hr />
<h2><strong>Per-Application Overrides</strong></h2>
<p>You can also configure deduplication per proxied application with <code>gateway.applications[].proxy.deduplication</code>. Application-level options override the global options.</p>
<pre><code class="language-json">{
 "gateway": {
   "deduplication": {
     "enabled": true,
     "methods": ["GET"]
   },
   "applications": [
     {
       "id": "frontend",
       "proxy": {
         "prefix": "/",
         "deduplication": {
           "enabled": true,
           "routes": [{ "method": "GET", "path": "/blog/*" }]
         }
       }
     }
   ]
 }
}
</code></pre>
<p>This approach lets you begin where the benefits are clear. You can deduplicate public catalogue pages, blog posts, product details, or framework prefetch routes, while keeping endpoints with strict per-user behaviour unchanged.</p>
<hr />
<h2><strong>Choosing The Deduplication Key</strong></h2>
<p>The default key is computed from:</p>
<ul>
<li><p>the configured application origin</p>
</li>
<li><p>the HTTP method</p>
</li>
<li><p>the rewritten proxy URL, including the query string</p>
</li>
<li><p>selected request headers</p>
</li>
</ul>
<p>The default headers are:</p>
<pre><code class="language-plaintext">["authorization", "cookie", "accept", "accept-language"]
</code></pre>
<p>Including headers matters because many read responses are not only a function of the URL. A localized page can depend on accept-language. A user-specific page can depend on <code>cookie</code> or <code>authorization</code>. If those headers were ignored, unrelated callers could incorrectly share a response.</p>
<p>You can adjust the headers included in the key for a deduplication configuration:</p>
<pre><code class="language-json">{
 "gateway": {
   "deduplication": {
     "enabled": true,
     "headers": ["authorization", "cookie", "x-tenant-id"]
   }
 }
}
</code></pre>
<p>Currently, you can’t set headers per route. If you need different header behaviour for different routes, use separate deduplication settings for each application or create a custom key function.</p>
<p>For full control, provide a synchronous key function:</p>
<pre><code class="language-json">{
 "gateway": {
   "deduplication": {
     "enabled": true,
     "key": "./deduplication-key.js"
   }
 }
}
</code></pre>
<pre><code class="language-javascript">export function computeDeduplicationKey(request, context) {
 return `${context.origin}:${context.method}:${context.url}`
}
</code></pre>
<p>The function receives the request and a context object containing the origin, method, rewritten URL, parsed query, selected headers, and application configuration. It must return the key synchronously.</p>
<hr />
<h2><strong>Route Whitelisting</strong></h2>
<p>For tighter control, configure a route whitelist. Routes use <a href="https://github.com/delvedor/find-my-way">find-my-way</a> syntax.</p>
<pre><code class="language-json">{
 "gateway": {
   "deduplication": {
     "enabled": true,
     "routes": [
       { "method": "GET", "path": "/blog/*" },
       { "methods": ["GET", "HEAD"], "path": "/products/:id" }
     ]
   }
 }
}
</code></pre>
<p>When <code>routes</code> are configured, route matching decides whether deduplication applies. When <code>routes</code> are not configured, the <code>methods</code> list decides.</p>
<hr />
<h2><strong>Storage: Memory Or Valkey</strong></h2>
<p>TBy default, memory is used for storage. It handles duplicate requests within a single gateway instance and doesn’t need any external service. This setup is great for local development, single-instance deployments, and easy rollouts.</p>
<pre><code class="language-json">{
 "gateway": {
   "deduplication": {
     "enabled": true,
     "storage": {
       "adapter": "memory"
     }
   }
 }
}
</code></pre>
<p>For deployments that scale horizontally, use the valkey adapter. It stores locks, response pointers, and buffered responses in a Redis-compatible Valkey server, allowing multiple gateway workers, instances, or pods to coordinate. This way, you get the same deduplication benefits even when traffic is spread across several replicas.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/860d66e5-8f7d-4f7a-a2e3-c04094af5abf.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-json">{
 "gateway": {
   "deduplication": {
     "enabled": true,
     "storage": {
       "adapter": "valkey",
       "url": "redis://127.0.0.1:6379",
       "prefix": "my-application"
     }
   }
 }
}
</code></pre>
<p>Use a <code>prefix</code> if several applications share the same Valkey instance and need separate key spaces.</p>
<hr />
<h2><strong>Operational Behavior</strong></h2>
<p>Deduplication is a best-effort feature, not a guarantee of exactly-once processing.</p>
<p>Duplicate upstream requests can still occur if the in-flight lock expires before the upstream response is ready, if a gateway instance fails while handling the leader request, if a waiter times out, or if retries run out. In these cases, the gateway just switches back to normal proxying.</p>
<p>The main timing options are:</p>
<ul>
<li><p><code>timeout</code>: how long a duplicate request waits for the leader response before retrying lock acquisition</p>
</li>
<li><p><code>retries</code>: how many additional deduplication attempts are made before falling back to normal proxying</p>
</li>
<li><p><code>ttl</code>: how long stored responses remain available for waiting requests</p>
</li>
<li><p><code>lockTtl</code>: how long an in-flight lock can live before it expires</p>
</li>
</ul>
<p>Defaults are:</p>
<pre><code class="language-json">{
 "timeout": 1000,
 "retries": 3,
 "ttl": 10000,
 "lockTtl": 500
}
</code></pre>
<p>Responses are fully buffered before being replayed. This works well for short bursts of duplicate reads, but large responses can use more gateway memory and, with Valkey, add some storage overhead. It’s best to start with routes where responses are small and where repeated upstream work is already costing you in money, speed, or capacity.</p>
<hr />
<h2><strong>Custom Gateway Handlers</strong></h2>
<p>Deduplication comprises custom gateway handlers. When both a custom handler and deduplication are configured, <code>deduplication</code> runs first, and the leader request is delegated to the handler.</p>
<p>Handlers that use <code>reply.from()</code> do not need special handling. Platformatic Gateway uses <code>reply.from()</code> from <a href="https://github.com/fastify/fastify-reply-from">@fastify/reply-from</a> to proxy upstream requests.</p>
<pre><code class="language-javascript">export function handler(request, reply, dest, options) {
 return reply.from(dest, options)
}
</code></pre>
<p>If a handler overrides <code>onResponse</code> or <code>onError</code>, it can call the helper functions provided in <code>options</code>, so waiting requests still receive the correct signal:</p>
<pre><code class="language-javascript">export function handler(request, reply, dest, options) {
 return reply.from(dest, {
   ...options,
   async onResponse(request, reply, res) {
     reply.header('x-custom-handler', 'true')
     return options.deduplicateResponse(request, reply, res)
   },
   async onError(reply, error) {
     return options.deduplicateError(reply, error)
   }
 })
}
</code></pre>
<p>Handlers that send responses directly without <code>reply.from()</code> cannot be replayed by gateway deduplication.</p>
<hr />
<h2><strong>Metrics</strong></h2>
<p>The feature also adds Gateway metrics so you can prove whether deduplication is helping in production:</p>
<ul>
<li><p><code>gateway_deduplication_leader_count</code></p>
</li>
<li><p><code>gateway_deduplication_waiter_count</code></p>
</li>
<li><p><code>gateway_deduplication_replay_count</code></p>
</li>
<li><p><code>gateway_deduplication_fallback_count</code></p>
</li>
<li><p><code>gateway_deduplication_error_count</code></p>
</li>
</ul>
<p>These counters help answer real-world questions: how many requests became leaders, how many waited, how many were replayed, and how often the gateway had to switch back to normal proxying.</p>
<hr />
<h2><strong>Conclusion</strong></h2>
<p>Gateway deduplication works best when lots of clients request the same resource at once, and the upstream response can be safely reused for matching keys. This is just like the product launch or hot-news example from earlier: many users show up at once, the cache is cold or refreshing, and your app is about to generate the same page over and over.</p>
<p>The best places to start are public, read-heavy routes, framework prefetch endpoints, cache refresh paths, and expensive upstream reads with limited response sizes. Turn it on for a small set of routes first. Keep an eye on the leader, waiter, replay, and fallback metrics. Then, expand to other routes where you see duplicate work causing the most trouble.</p>
<p>You’ll see results right away: the gateway handles traffic spikes, your services do less repeated work, and users get more consistent response times during busy periods.</p>
<p>This kind of optimization adds up over time. You protect your upstream resources without changing your app code. You cut down on unnecessary backend load before it hits your databases, APIs, or rendering services. You also get metrics to see if the feature is delivering value. And if deduplication can’t help in a certain case, the request just goes through as usual.</p>
<p>For teams using Platformatic Gateway in front of modern web apps, especially self-hosted Next.js apps, request deduplication is a practical way to make read-heavy traffic more manageable. It gives you a safety net for traffic bursts, an easier way to scale with Valkey, and a rollout approach that doesn’t require changing your backend services.</p>
]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Run Medusa on Kubernetes with Watt
as a Monorepo]]></title>
      <description><![CDATA[Medusa stands out as a flexible open source commerce platform for Node.js. It offers teams a customizable backend, admin tools, and a modern storefront, all without locking you into a strict SaaS mode]]></description>
      <link>https://blog.platformatic.dev/run-medusa-kubernetes-watt-monorepo</link>
      <guid isPermaLink="true">https://blog.platformatic.dev/run-medusa-kubernetes-watt-monorepo</guid>
      <category><![CDATA[medusa]]></category>
      <category><![CDATA[Node.js]]></category>
      <category><![CDATA[Kubernetes]]></category>
      <category><![CDATA[platformatic]]></category>
      <category><![CDATA[monorepo]]></category>
      <category><![CDATA[Next.js]]></category>
      <category><![CDATA[watt]]></category>
      <dc:creator><![CDATA[Paolo Insogna]]></dc:creator>
      <pubDate>Tue, 28 Apr 2026 14:30:00 GMT</pubDate>
      <enclosure url="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/03fd7b26-e637-4b53-bf00-378d94dbf22d.png" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p><a href="https://medusajs.com">Medusa</a> stands out as a flexible open source commerce platform for Node.js. It offers teams a customizable backend, admin tools, and a modern storefront, all without locking you into a strict SaaS model. This makes it ideal for teams who want to move quickly and keep control over their architecture.</p>
<p>Running Medusa in production is more than just starting a single process. The real challenge is keeping the entire commerce stack fast, organized, and easy to update, especially when you have a backend, storefront, admin UI, image optimization, internal networking, and Kubernetes involved.</p>
<p>This is where using a Watt monorepo really helps.</p>
<p><a href="https://www.platformatichq.com/watt">Watt</a> is Platformatic’s tool for combining multiple Node.js apps into one deployable unit by running them as worker threads under a single process.</p>
<p>Medusa can be deployed in a Kubernetes environment. To manage, monitor, and optimize your application in this setting, you can use the <a href="https://icc.platformatic.dev">Intelligent Command Center (ICC)</a>. ICC is a sophisticated cloud control plane that provides intelligent management, monitoring, and optimization of cloud-native applications deployed in Kubernetes environments. ICC offers enterprise-grade features for application lifecycle management, intelligent autoscaling, compliance monitoring, and comprehensive observability.</p>
<p>For basic deployment, simply running Watt on Kubernetes is sufficient.</p>
<p>Rather than spreading complexity across multiple repos, custom Dockerfiles, and manual service connections, you can keep everything in one workspace and let Watt manage it as a single platform. This gives you one dependency graph, one build process, one deployment artifact, and a single place to manage the rules that keep your system running smoothly.</p>
<p>In this post, we will look at a working <a href="https://medusajs.com">Medusa</a> setup deployed on <a href="https://icc.platformatic.dev">ICC</a> with:</p>
<ul>
<li><p><code>web/backend</code>: Medusa backend via <code>@platformatic/node</code></p>
</li>
<li><p><code>web/frontend</code>: Medusa Next.js starter via <code>@platformatic/next</code></p>
</li>
<li><p><code>web/gateway</code>: public routing via <code>@platformatic/gateway</code></p>
</li>
<li><p><code>image-server</code>: a dedicated <code>@platformatic/next</code> image optimizer application that reuses the same codebase as <code>web/frontend</code></p>
</li>
</ul>
<p>This set-up can be both far easier to manage <em>and</em> more performant. Let’s explore.</p>
<h2><strong>Why a monorepo is a good fit for Medusa</strong></h2>
<p>Medusa already pushes you toward a multi-application architecture. Even in a relatively standard deployment, you are dealing with:</p>
<ul>
<li><p>a backend API</p>
</li>
<li><p>an admin UI</p>
</li>
<li><p>a storefront</p>
</li>
<li><p>image optimization</p>
</li>
<li><p>environment variables shared across services</p>
</li>
<li><p>public and internal URLs that must stay aligned</p>
</li>
</ul>
<p>You can spread these parts across different repositories and deployment pipelines, but as soon as you do, even simple changes become complicated.</p>
<p>For example, changing a base path means updating several repos. Keeping React versions consistent gets harder. Coordinating Docker changes turns into a big release task. Even figuring out if the storefront is calling the right backend can take more effort than it should.</p>
<p>With Watt, the monorepo becomes the control plane for the whole stack.</p>
<ul>
<li><p>Each application stays isolated as a worker thread with Watt.</p>
</li>
<li><p>The whole platform is configured in one place.</p>
</li>
<li><p>Internal service discovery comes for free.</p>
</li>
<li><p>Deployment stays a single build and a single runtime entry point.</p>
</li>
</ul>
<p>This approach gives you the best of both worlds: separation where it matters, and simplicity where you want it.</p>
<h2><strong>The workspace layout</strong></h2>
<p>The sample project is structured like this:</p>
<pre><code class="language-plaintext">.
|-- package.json
|-- pnpm-workspace.yaml
|-- watt.json
`-- web
    |-- backend
    |   |-- medusa-config.ts
    |   |-- package.json
    |   |-- url-handler.js
    |   `-- watt.json
    |-- frontend
    |   |-- next.config.js
    |   |-- package.json
    |   |-- watt.image-optimizer.json
    |   |-- watt.json
    |   `-- src
    `-- gateway
        |-- package.json
        `-- watt.json
</code></pre>
<p>At the root, <code>watt.json</code> autoloads the <code>web/*</code> applications, sets <code>gateway</code> as the public entrypoint, and adds an extra application called <code>image-server</code> that reuses the frontend codebase with a different config.</p>
<p>This is where the monorepo model really shines. You can easily reuse the same codebase for different runtime roles. There’s no need to create a second Next.js project just to separate <code>/_next/image</code>. Instead, you keep one frontend codebase and let Watt run it in two different ways.</p>
<h2><strong>pnpm workspace setup: one dependency graph, fewer surprises</strong></h2>
<p>If you use pnpm, make the workspace explicit with <code>pnpm-workspace.yaml</code>:</p>
<pre><code class="language-plaintext">packages:
 - web/*
</code></pre>
<p>Then pin the React family at the root in <code>package.json</code>:</p>
<pre><code class="language-json">{
 "pnpm": {
   "overrides": {
     "react": "19.0.4",
     "react-dom": "19.0.4",
     "@types/react": "19.0.4",
     "@types/react-dom": "19.0.4"
   }
 }
}
</code></pre>
<p>This is a clear reason why using a monorepo matters. The Medusa storefront, Next.js, and related tools all rely on React. In a multi-repo setup, versions can easily get out of sync. With a Watt monorepo, you set the version once at the root, and every app benefits right away.</p>
<p>This makes building more predictable and keeps maintenance costs much lower.</p>
<h2><strong>One .env, clear public and internal boundaries</strong></h2>
<p>The root <code>.env</code> needs a few shared values:</p>
<ul>
<li><p><code>REDIS_HOST</code></p>
</li>
<li><p><code>MEDUSA_PUBLIC_BACKEND_URL</code></p>
</li>
<li><p><code>MEDUSA_BACKEND_URL</code></p>
</li>
</ul>
<p>The key distinction is this:</p>
<ul>
<li><p><code>MEDUSA_PUBLIC_BACKEND_URL</code> is for the externally visible backend URL</p>
</li>
<li><p><code>MEDUSA_BACKEND_URL</code> is for server-side calls from the frontend</p>
</li>
</ul>
<p>On ICC, this is the ideal setup:</p>
<pre><code class="language-plaintext">MEDUSA_PUBLIC_BACKEND_URL=https://medusa.plt/backend
MEDUSA_BACKEND_URL=http://backend.plt.local
</code></pre>
<p>Why it matters:</p>
<ul>
<li><p>browsers and the admin UI use the public backend URL</p>
</li>
<li><p>The frontend server uses <code>http://backend.plt.local</code> and stays on the Platformatic mesh.</p>
</li>
</ul>
<p>It’s worth emphasizing that second point, since it provides both great DevEx and a substantial performance boost. Thanks to Watt and inter-thread communication, server-side requests skip the public gateway and stay within the process’s internal network.</p>
<p>Once again, the monorepo helps here. The internal service name and public URL strategy are side-by-side in the same workspace, making them much harder to misconfigure.</p>
<h2><strong>Backend: run Medusa as a Watt application</strong></h2>
<p>In <code>web/backend/package.json</code>, add <code>@platformatic/node</code>:</p>
<pre><code class="language-json">{
 "dependencies": {
   "@platformatic/node": "^3.44.0"
 }
}
</code></pre>
<p>Then configure <code>web/backend/watt.json</code>:</p>
<pre><code class="language-json">{
 "$schema": "https://schemas.platformatic.dev/@platformatic/node/3.44.0.json",
 "application": {
   "basePath": "/backend",
   "commands": {
     "development": "npm run dev",
     "build": "npm run build",
     "production": "npm run start"
   },
   "changeDirectoryBeforeExecution": false,
   "entrypointPort": 3000
 },
 "node": {
   "disableBuildInDevelopment": true,
   "dispatchViaHttp": true,
   "absoluteUrl": true
 },
 "watch": false
}
</code></pre>
<p>This setup gives Medusa a clear application boundary within the workspace, while still allowing the gateway to publish it under <code>/backend</code>.</p>
<p>The companion change in <code>web/backend/medusa-config.ts</code> is just as important:</p>
<pre><code class="language-typescript">import { defineConfig, loadEnv } from '@medusajs/framework/utils'

loadEnv(process.env.NODE_ENV || 'development', process.cwd())

module.exports = defineConfig({
 projectConfig: {
   databaseUrl: process.env.DATABASE_URL,
   http: {
     storeCors: process.env.STORE_CORS!,
     adminCors: process.env.ADMIN_CORS!,
     authCors: process.env.AUTH_CORS!,
     jwtSecret: process.env.JWT_SECRET || 'supersecret',
     cookieSecret: process.env.COOKIE_SECRET || 'supersecret'
   },
   cookieOptions: {
     sameSite: 'lax',
     secure: false
   }
 },
 admin: {
   path: (new URL(process.env.MEDUSA_PUBLIC_BACKEND_URL!).pathname + '/app') as `/string`,
   backendUrl: process.env.MEDUSA_PUBLIC_BACKEND_URL,
   vite: config =&gt; {
     config.server.allowedHosts ??= []
     config.server.allowedHosts.push('.plt.local')
   }
 }
})
</code></pre>
<p>The admin path comes from the public backend URL. So, if ICC publishes the backend at <code>/backend</code>, the admin will automatically be available at <code>/backend/app</code>.</p>
<p>You should also keep <code>web/backend/url-handler.js</code> in place. Medusa’s API and admin UI do not behave identically when you put them behind a prefixed public path, so Watt’s gateway uses this file to rewrite requests correctly.</p>
<p>The implementation used in the sample project looks like this:</p>
<pre><code class="language-javascript">const basePath = process.env.PLT_BASE_PATH ?? ''
const adminPath = new URL(process.env.MEDUSA_PUBLIC_BACKEND_URL).pathname.replace(/\/$/, '')
const adminUiPath = adminPath + '/app'
const adminMatcher = new RegExp(`^${adminPath}`)

export default {
 preRewrite(url) {
   if (basePath &amp;&amp; !url.startsWith(basePath)) {
     url = `${basePath}${url}`
   }

   url = url.startsWith(adminUiPath) ? url : url.replace(adminMatcher, '')
   return url
 }
}
</code></pre>
<p>This file may be small, but it does important work. It keeps the admin UI path intact while removing the backend prefix for API routes that Medusa expects to serve from the root.</p>
<h2><strong>Frontend: one codebase, two runtime roles</strong></h2>
<p>In <code>web/frontend/package.json</code>, add <code>@platformatic/next</code>:</p>
<pre><code class="language-json">{
 "dependencies": {
   "@platformatic/next": "^3.44.0"
 }
}
</code></pre>
<p>The standard frontend config in <code>web/frontend/watt.json</code> is simple:</p>
<pre><code class="language-json">{
 "$schema": "https://schemas.platformatic.dev/@platformatic/next/3.44.0.json",
 "application": {
   "basePath": "{PLT_BASE_PATH}",
   "changeDirectoryBeforeExecution": true
 },
 "next": {
   "trailingSlash": true
 }
}
</code></pre>
<p>And in <code>web/frontend/next.config.js</code>, set:</p>
<pre><code class="language-javascript">const nextConfig = {
 reactStrictMode: true,
 logging: {
   fetches: {
     fullUrl: true
   }
 },
 eslint: {
   ignoreDuringBuilds: true
 },
 typescript: {
   ignoreBuildErrors: true
 }
}
</code></pre>
<p>Here’s where it gets interesting: the monorepo lets you reuse the same frontend codebase as a dedicated image optimization service, with almost no extra work.</p>
<h2><strong>Split image optimization without splitting the repo</strong></h2>
<p>We recently covered why this architecture matters in our post on <a href="https://blog.platformatic.dev/scale-nextjs-image-optimization-platformatic">scaling Next.js image optimization with a dedicated Platformatic application</a>: image optimization is CPU-heavy and can become a noisy neighbour for SSR traffic.</p>
<p>That is exactly why this Medusa setup runs <code>/_next/image</code> separately.</p>
<p>Create <code>web/frontend/watt.image-optimizer.json</code>:</p>
<pre><code class="language-json">{
 "$schema": "https://schemas.platformatic.dev/@platformatic/next/3.44.0.json",
 "logger": {
   "level": "trace"
 },
 "application": {
   "basePath": "/",
   "changeDirectoryBeforeExecution": true
 },
 "next": {
   "trailingSlash": true,
   "imageOptimizer": {
     "enabled": true,
     "fallback": "frontend",
     "timeout": 30000,
     "ttl": 3600000,
     "maxAttempts": 3,
     "storage": {
       "type": "valkey",
       "url": "{REDIS_HOST}"
     }
   }
 }
}
</code></pre>
<p>This is a great example of why Watt monorepos work so well.</p>
<ul>
<li><p>You reuse the same frontend app.</p>
</li>
<li><p>You keep one source tree.</p>
</li>
<li><p>You give it a second runtime role.</p>
</li>
<li><p>You isolate a CPU-heavy path without creating a second frontend project.</p>
</li>
</ul>
<p>This setup improves both maintainability and performance, which is exactly what you want from your platform architecture.</p>
<p>The <code>fallback: "frontend"</code> setting is especially nice here: relative image URLs are resolved through the main storefront service over the runtime network, so the optimizer stays tightly integrated without being coupled to the frontend worker pool.</p>
<h2><strong>Next.js build-time pragmatism: force dynamic where it helps</strong></h2>
<p>Because the Medusa backend is not available during the <code>wattpm build</code>, the storefront cannot pre-generate some pages safely.</p>
<p>For these files:</p>
<ul>
<li><p><code>web/frontend/src/app/[countryCode]/(main)/products/[handle]/page.tsx</code></p>
</li>
<li><p><code>web/frontend/src/app/[countryCode]/(main)/categories/[...category]/page.tsx</code></p>
</li>
<li><p><code>web/frontend/src/app/[countryCode]/(main)/collections/[handle]/page.tsx</code></p>
</li>
</ul>
<p>comment out <code>generateStaticParams</code> and add:</p>
<pre><code class="language-javascript">export const dynamic = 'force-dynamic'
</code></pre>
<p>This uses Next.js <a href="https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config">Route Segment Config</a> to force runtime rendering instead of static generation.</p>
<p>In a typical Next.js app, this might seem like a compromise. But in this setup, it’s the right choice. The storefront relies on live Medusa data, and Watt provides that backend at runtime.</p>
<p>This is another area where the monorepo helps. The build behaviour is clear because the backend and frontend are in the same workspace, and their dependencies are easy to see.</p>
<h2><strong>Gateway: one public surface for the whole stack</strong></h2>
<p>Add <code>@platformatic/gateway</code> in <code>web/gateway/package.json</code>:</p>
<pre><code class="language-json">{
 "dependencies": {
   "@platformatic/gateway": "^3.44.0"
 }
}
</code></pre>
<p>Then define <code>web/gateway/watt.json</code> like this:</p>
<pre><code class="language-json">{
 "$schema": "https://schemas.platformatic.dev/@platformatic/gateway/3.44.0.json",
 "gateway": {
   "applications": [
     {
       "id": "backend",
       "proxy": {
         "prefix": "/backend",
         "custom": {
           "path": "../backend/url-handler.js"
         }
       }
     },
     {
       "id": "frontend",
       "proxy": {
         "prefix": "/"
       }
     },
     {
       "id": "image-server",
       "proxy": {
         "prefix": "/",
         "routes": ["/_next/image", "/_next/image/*"],
         "methods": ["GET"]
       }
     }
   ]
 }
}
</code></pre>
<p>This is where the monorepo approach really starts to feel smooth and efficient.</p>
<ul>
<li><p><code>/backend</code> goes to Medusa</p>
</li>
<li><p><code>/</code> goes to the storefront</p>
</li>
<li><p><code>GET /_next/image</code> goes to the image optimizer</p>
</li>
</ul>
<p>Thanks to <code>@platformatic/gateway</code>, you get one public entry point, but the traffic still lands on the right internal application.</p>
<p>This setup is easier to understand, change, and scale than trying to connect separate services outside the repo.</p>
<h2><strong>A small middleware detail that improves the experience</strong></h2>
<p>There is another subtle optimization in the storefront middleware <code>(web/frontend/src/middleware.ts)</code>.</p>
<p>When the request already contains a country code in the URL but does not yet have the <code>medusacache_id</code> cookie, the middleware sets that cookie and returns <code>NextResponse.next()</code> instead of forcing another redirect.</p>
<p>It’s a small detail, but it’s the kind of optimization that’s easier to maintain in a monorepo. Storefront routing, Medusa region lookups, and platform-level caching thanks to Watt HTTP caching handling are all managed together.</p>
<p>In practice, this helps the storefront set up its region-aware state smoothly, without extra steps.</p>
<p>The change is small enough to think of as a focused patch:</p>
<pre><code class="language-typescript"> if (urlHasCountryCode &amp;&amp; !cacheIdCookie) {
+    const response = NextResponse.next()

   response.cookies.set('_medusa_cache_id', cacheId, {
     maxAge: 60 * 60 * 24
   })

   return response
 }
</code></pre>
<p>This is the kind of practical improvement that’s easier to maintain when routing logic, storefront behaviour, and platform deployment are all in the same repo.</p>
<h2><strong>ICC environment values</strong></h2>
<p>In <code>.env.icc</code>, the main settings to align are:</p>
<pre><code class="language-plaintext">MEDUSA_PUBLIC_BACKEND_URL=https://medusa.plt/backend
STORE_CORS=https://docs.medusajs.com,https://medusa.plt
ADMIN_CORS=https://docs.medusajs.com,https://medusa.plt
AUTH_CORS=https://docs.medusajs.com,https://medusa.plt
NEXT_PUBLIC_BASE_URL=https://medusa.plt
</code></pre>
<p>They all reflect the same core rule: the whole application is published under <code>/medusa</code>, so both Medusa and Next.js need to agree on that public shape.</p>
<p>Since these settings are in one workspace and one deployment artifact, keeping them in sync is much easier than with a split-repo setup.</p>
<h2><strong>The Docker build is simple because the repo is simple</strong></h2>
<p>The container image is straightforward:</p>
<pre><code class="language-plaintext">FROM node:22-alpine

# Environment setup
ENV APP_HOME=/home/app/node/
ENV PLT_BASE_PATH="/medusa"
ENV PLT_ICC_URL="http://icc.platformatic.svc.cluster.local"
WORKDIR $APP_HOME

# Install dependencies
RUN npm install -g pnpm wattpm-utils "@platformatic/watt-extra@latest"
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml $APP_HOME
RUN pnpm install --frozen-lockfile --node-linker=hoisted

# Copy application
COPY web $APP_HOME/web
COPY .env.icc watt.json $APP_HOME
RUN mv .env.icc .env
RUN pnpm run build

# Final setup
EXPOSE 3042
EXPOSE 9090
CMD ["watt-extra", "start"]
</code></pre>
<p>There are two details worth mentioning.</p>
<p>First, using <code>--node-linker=hoisted</code> with pnpm installs dependencies in a flatter layout, instead of the usual symlink-heavy structure. In a workspace with Medusa, Next.js, shared React versions, and several Watt apps, this makes module resolution more predictable and helps avoid compatibility issues during container builds.</p>
<p>Second, <code>@platformatic/watt-extra</code> is a helper CLI that starts Watt smoothly in container environments like ICC. It adds the operational support you need at runtime, so your container entrypoint remains simple.</p>
<p>This is another area where the monorepo pays off right away: you have one install step, one build step, and one runtime command.</p>
<h2><strong>Why does this feel better to maintain</strong></h2>
<p>The main advantage of this Medusa setup isn’t any single config file. It’s the overall structure:</p>
<ul>
<li><p>One repo for backend, frontend, gateway, and optimizer</p>
</li>
<li><p>One dependency strategy</p>
</li>
<li><p>One place to define public and internal URLs</p>
</li>
<li><p>One deployment artifact for Kubernetes and ICC</p>
</li>
<li><p>One runtime that still preserves application boundaries</p>
</li>
</ul>
<p>Since Watt sees the platform as a group of coordinated apps, you can make performance improvements without making the system harder to manage.</p>
<p>You can send image optimization to a dedicated service, keep frontend-to-backend calls on the mesh network, mount everything under a base path, and update all these rules in one place.</p>
<p>That’s the real value of running Medusa in a Watt monorepo on ICC: convenience and performance work together, instead of getting in each other’s way. Because ICC provides a <strong>Kubernetes (K8S)</strong>-native environment, your monorepo and its services benefit from K8s's inherent scalability, resilience, and orchestration capabilities. This integration ensures that deploying and managing Medusa within the Watt monorepo is seamless, leveraging the enterprise-grade infrastructure of ICC (which is built on K8S) for optimal operational efficiency.</p>
<p>If you’re building commerce systems with lots of moving parts, this is the kind of platform setup you want.</p>
]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Introducing Regina: Stateful AI Agent Orchestration for Platformatic Watt]]></title>
      <description><![CDATA[We’re excited to share Regina, a production-ready agent orchestration layer built on Platformatic Watt.
Regina lets you go from single-agent demos to real systems you can run and scale confidently. Yo]]></description>
      <link>https://blog.platformatic.dev/introducing-regina-stateful-ai-agent-orchestration-watt</link>
      <guid isPermaLink="true">https://blog.platformatic.dev/introducing-regina-stateful-ai-agent-orchestration-watt</guid>
      <category><![CDATA[AI]]></category>
      <category><![CDATA[ai agents]]></category>
      <category><![CDATA[Node.js]]></category>
      <category><![CDATA[platformatic]]></category>
      <dc:creator><![CDATA[Paolo Insogna]]></dc:creator>
      <pubDate>Tue, 14 Apr 2026 14:30:00 GMT</pubDate>
      <enclosure url="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/b492a6aa-6050-460c-be39-465c2c437bad.png" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>We’re excited to share Regina, a production-ready agent orchestration layer built on <a href="https://docs.platformatic.dev/">Platformatic Watt</a>.</p>
<p>Regina lets you go from single-agent demos to real systems you can run and scale confidently. You define agents in Markdown, start instances over HTTP, and get built-in lifecycle management, persistence, and recovery, all by running and managing agents in Watt as isolated worker threads.</p>
<h2>Why Regina, why now</h2>
<p>Most AI projects hit the same wall after the first demo:</p>
<ul>
<li><p>Prompts are not versioned in a clean, operational way.</p>
</li>
<li><p>Sessions disappear on restart.</p>
</li>
<li><p>Scaling introduces routing and state headaches.</p>
</li>
<li><p>Tool-heavy workflows are hard to observe and control.</p>
</li>
</ul>
<p>Regina solves these problems directly, so your team can focus on building your product and not re-inventing the wheel when it comes to complex orchestration and state management.</p>
<h2>What you get on day one</h2>
<p>Regina comes as three packages:</p>
<ul>
<li><p><code>@platformatic/regina</code>: per-pod agent manager</p>
</li>
<li><p><code>@platformatic/regina-agent</code>: per-agent runtime</p>
</li>
<li><p><code>@platformatic/regina-storage</code>: pluggable backup adapters (<code>fs, s3, redis</code>)</p>
</li>
</ul>
<p>With this stack, you’ll have:</p>
<ul>
<li><p>stateful agent instances with per-instance SQLite VFS <em>(“Virtual File System”)</em></p>
</li>
<li><p>suspend/resume lifecycle management with idle timeout control</p>
</li>
<li><p>NDJSON streaming events for full run visibility</p>
</li>
<li><p>steerable agentic loops via <code>POST /instances/:id/steer</code></p>
</li>
<li><p>storage-backed restore for resilient multi-pod operation</p>
</li>
</ul>
<h2><strong>Markdown-native agent definitions</strong></h2>
<p>Regina uses Markdown with YAML frontmatter as the main source for each agent.</p>
<pre><code class="language-plaintext">---
name: support-agent
description: Customer support assistant
model: anthropic/claude-sonnet-4-5
provider: vercel-gateway
tools:
 - ./tools/search-docs.ts
temperature: 0.3
maxSteps: 10
---
You are a helpful support agent.
</code></pre>
<p>This setup keeps prompt and runtime configuration together, so it’s easy to review in pull requests and update across teams.</p>
<h2><strong>Built for real runtime behaviour</strong></h2>
<p>Regina keeps management and execution separate:</p>
<ul>
<li><p><code>@platformatic/regina</code> discovers definitions, spawns instances, and proxies instance APIs</p>
</li>
<li><p><code>@platformatic/regina-agent</code> runs each instance in isolation</p>
</li>
<li><p>message history is persisted at <code>/.session/messages.jsonl</code> in each instance VFS</p>
</li>
</ul>
<p>This design gives you reliable performance, even under heavy load:</p>
<ul>
<li><p><strong>Idle suspension</strong> to free resources automatically</p>
</li>
<li><p><strong>Auto-resume</strong> on next request</p>
</li>
<li><p><strong>State continuity</strong> across restarts</p>
</li>
<li><p><strong>Rich streaming</strong> <code>(text-delta, tool-call, tool-result, step-finish</code>)</p>
</li>
</ul>
<p>In practice, running your agents with Regina and Watt gives you agents that act like durable workflows backed by persistent state instead of ephemeral, one-off chat sessions.</p>
<h2><strong>Start simple and scale smoothly</strong></h2>
<p>Regina works great in single-pod mode with no Redis, no external storage, and minimal setup.</p>
<p>As your traffic grows, you can add Redis or Valkey for member and instance mapping, and add shared storage for state restore if needed. The API stays the same, so clients don’t need to change as your setup evolves.</p>
<h2><strong>Getting started</strong></h2>
<p>The Regina demo app is small on purpose, but it shows the full production pattern in a single repo:</p>
<ul>
<li><p><code>watt.json</code> at the root defines a single entrypoint service for Regina</p>
</li>
<li><p><code>services/regina/watt.json</code> enables <code>@platformatic/regina</code> and points to the shared <code>agents/</code> directory</p>
</li>
<li><p>Each file in <code>agents/</code> is a full agent definition <em>(prompt + model + provider + tools)</em></p>
</li>
<li><p>Custom tools sit alongside agents in <code>agents/tools/*</code></p>
</li>
</ul>
<p>Here’s how the demo app is set up.</p>
<p>Root <code>watt.json</code>:</p>
<pre><code class="language-json">{
 "$schema": "https://schemas.platformatic.dev/wattpm/3.50.0.json",
 "server": {
   "port": 3042
 },
 "management": true,
 "entrypoint": "regina",
 "services": [
   {
     "id": "regina",
     "path": "./services/regina",
     "management": {
       "operations": ["addApplications", "removeApplications", "getApplications", "getApplicationDetails", "inject"]
     }
   }
 ]
}
</code></pre>
<p>Service <code>services/regina/watt.json</code>:</p>
<pre><code class="language-json">{
 "$schema": "https://schemas.platformatic.dev/@platformatic/regina/0.1.0.json",
 "regina": {
   "agentsDir": "../../agents"
 }
}
</code></pre>
<p>Example agent definition (<code>agents/assistant.md</code>):</p>
<pre><code class="language-plaintext">---
name: assistant
description: A general-purpose assistant with file and shell access
model: anthropic/claude-sonnet-4-5
provider: vercel-gateway
greeting: "Hi! I'm a general-purpose assistant. I can read and write files, run commands, and help with any task."
temperature: 0.3
maxSteps: 15
---
You are a helpful assistant. You can read, write, and edit files, run bash commands, and help with any task.
</code></pre>
<p>Here’s a typical flow in the demo:</p>
<ol>
<li><p>Start Watt (<code>wattpm start</code>).</p>
</li>
<li><p>Create an instance from an agent definition (<code>POST /agents/:defId/instances</code>).</p>
</li>
<li><p>Chat with that instance (<code>POST /instances/:instanceId/chat or /chat/stream</code>).</p>
</li>
<li><p>Resume the same instance later with history already available.</p>
</li>
</ol>
<p>This is important because it shows Regina’s core value from start to finish: agents are defined as code, run as managed instances, and keep their state across requests without extra orchestration work.</p>
<h2><strong>Storage options for state backup</strong></h2>
<p>For multi-pod setups, configure <a href="http://regina.storage">regina.storage</a> so you can restore on another pod.</p>
<p><strong>Filesystem (</strong><code>fs</code><strong>)</strong></p>
<pre><code class="language-json">{
 "module": "@platformatic/regina",
 "regina": {
   "storage": {
     "type": "fs",
     "basePath": "/mnt/shared/regina-state"
   }
 }
}
</code></pre>
<p><strong>Object storage (</strong><code>s3</code><strong>)</strong></p>
<pre><code class="language-json">{
 "module": "@platformatic/regina",
 "regina": {
   "storage": {
     "type": "s3",
     "bucket": "regina-state",
     "prefix": "backups/",
     "endpoint": "https://s3.amazonaws.com"
   }
 }
}
</code></pre>
<p><strong>Redis (</strong><code>redis</code><strong>)</strong></p>
<pre><code class="language-json">{
 "module": "@platformatic/regina",
 "regina": {
   "redis": "redis://valkey:6379",
   "storage": {
     "type": "redis"
   }
 }
}
</code></pre>
<p>All adapters use the same interface (<code>put, get, delete, list, close</code>), so you can switch backends without changing how your clients work.</p>
<h2><strong>Get started</strong></h2>
<ul>
<li><p><a href="https://github.com/platformatic/regina">Regina repository</a></p>
</li>
<li><p><a href="https://github.com/platformatic/regina/tree/main/packages/regina">Regina package docs</a></p>
</li>
<li><p><a href="https://github.com/platformatic/regina/tree/main/packages/regina-agent">Agent runtime docs</a></p>
</li>
<li><p><a href="https://github.com/platformatic/regina/tree/main/packages/regina-storage">Storage adapters docs</a></p>
</li>
</ul>
<p>Regina is built for teams shipping serious AI systems on Node.js. If you need agents that are reliable, observable, and stateful in production, Regina is ready for you.</p>
]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[@platformatic/kafka Now Supports Confluent Schema Registry ]]></title>
      <description><![CDATA[If you run Kafka in production, you can’t skip schema evolution. Teams need clear data types, compatibility checks, and a safe way to update contracts without breaking consumers or downstream services]]></description>
      <link>https://blog.platformatic.dev/platformatic-kafka-confluent-schema-registry-support</link>
      <guid isPermaLink="true">https://blog.platformatic.dev/platformatic-kafka-confluent-schema-registry-support</guid>
      <category><![CDATA[kafka]]></category>
      <category><![CDATA[Node.js]]></category>
      <category><![CDATA[Devops]]></category>
      <category><![CDATA[json]]></category>
      <dc:creator><![CDATA[Paolo Insogna]]></dc:creator>
      <pubDate>Tue, 07 Apr 2026 14:30:00 GMT</pubDate>
      <enclosure url="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/ef50f002-fb0e-47aa-aeac-23d7be4891f5.png" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>If you run Kafka in production, you can’t skip schema evolution. Teams need clear data types, compatibility checks, and a safe way to update contracts without breaking consumers or downstream services.</p>
<p>Before now, using <code>@platformatic/kafka</code> with Confluent Schema Registry meant writing extra code to connect the pieces. With <code>@platformatic/kafka</code> <strong>v1.27.0</strong>, that’s no longer needed.</p>
<p><code>@platformatic/kafka</code> now has built-in support for Confluent Schema Registry, including:</p>
<ul>
<li><p>AVRO</p>
</li>
<li><p>Protocol Buffers</p>
</li>
<li><p>JSON Schema</p>
</li>
<li><p>Basic and Bearer authentication</p>
</li>
<li><p>Automatic schema fetch and caching</p>
</li>
<li><p>Integrated Producer and Consumer hooks</p>
</li>
</ul>
<p>You get schema-aware messaging, and the project still focuses on being fast and predictable for Node.js Kafka clients.</p>
<h2><strong>Why This Matters</strong></h2>
<p>Most schema registry integrations add complexity where you don’t want it: in the message serialization and deserialization paths. Fetching remote schemas is asynchronous, but encoding and decoding should stay synchronous for speed and consistency.</p>
<p>Put simply, network I/O and cache coordination should happen before the main data processing, not during it. Keeping these steps separate helps maintain stable throughput and latency as traffic increases.</p>
<p>This release introduces a two-layer architecture to keep that separation clear:</p>
<ol>
<li><p><strong>Low-level hooks</strong> for async pre-processing:</p>
<ul>
<li><p><a href="https://github.com/platformatic/kafka/blob/main/docs/producer.md">beforeSerialization</a></p>
</li>
<li><p><a href="https://github.com/platformatic/kafka/blob/main/docs/consumer.md">beforeDeserialization</a></p>
</li>
</ul>
</li>
<li><p><strong>High-level registry API</strong> via <a href="https://github.com/platformatic/kafka/blob/main/docs/confluent-schema-registry.md">ConfluentSchemaRegistry</a></p>
</li>
</ol>
<p>In practice, this means schemas are fetched and cached before encode/decode happens, so your serializers and deserializers stay synchronous when messages are processed.</p>
<p>This gives application teams a simpler way to think about things: do the asynchronous prep first, then keep codec behavior predictable during main processing.</p>
<p>At a high level, the flow is:</p>
<ul>
<li><p>Extract schema ID from message metadata (producer) or wire payload (consumer).</p>
</li>
<li><p>Resolve schema from local cache when available.</p>
</li>
<li><p>On cache miss, fetch asynchronously via <code>beforeSerialization/beforeDeserialization</code> hooks and cache the schema.</p>
</li>
<li><p>Run synchronous serialization/deserialization with the resolved schema.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/043ca54f-3808-4811-8e47-b772feb30666.png" alt="" style="display:block;margin:0 auto" />

<p>In multi-instance deployments, that cache layer can be backed by Redis or Valkey, so workers share schema state across nodes while keeping encode/decode synchronous in the hot path.</p>
<h2><strong>What You Can Do Now</strong></h2>
<p>You can connect a registry directly to both the Producer and Consumer, letting <code>@platformatic/kafka</code> handle schema-aware serialization from start to finish.</p>
<p>This is especially helpful when several services publish and consume the same topics on different deployment cycles, since consistent schema handling is a must.</p>
<pre><code class="language-javascript">import { Consumer, Producer } from '@platformatic/kafka'
import { ConfluentSchemaRegistry } from '@platformatic/kafka/registries'

const registry = new ConfluentSchemaRegistry({
  url: 'http://localhost:8081'
})

const producer = new Producer({
  clientId: 'orders-producer',
  bootstrapBrokers: ['localhost:9092'],
  registry
})

const consumer = new Consumer({
  groupId: 'orders-consumers',
  clientId: 'orders-consumer',
  bootstrapBrokers: ['localhost:9092'],
  registry
})
</code></pre>
<p>When producing, pass schema IDs in message metadata:</p>
<pre><code class="language-javascript">await producer.send({
  messages: [
    {
      topic: 'orders',
      key: { orderId: 101 },
      value: { customerId: 'cust-44', total: 129.99 },
      metadata: {
        schemas: {
          key: 10,
          value: 11
        }
      }
    }
  ]
})
</code></pre>
<p>When consuming, payloads are automatically decoded with the cached schema. If a schema isn’t found, the registry fetches it before deserialization continues.</p>
<p>This makes it easy to move from custom codec code to a single registry integration in your client setup.</p>
<h2><strong>Authentication and Enterprise Scenarios</strong></h2>
<p>Schema Registry deployments are often protected. The new integration includes:</p>
<ul>
<li><p>Basic auth (<code>username</code> + <code>password</code>)</p>
</li>
<li><p>Bearer token auth (<code>token</code>)</p>
</li>
<li><p>Dynamic credentials via providers</p>
</li>
</ul>
<p>This makes it easier to connect to managed or secured registry instances without writing custom transport code. It also makes credential rotation simpler when you use providers.</p>
<p>If your setup uses short-lived credentials, provider functions let you refresh tokens and secrets without having to rebuild your producer or consumer logic.</p>
<h2><strong>Performance and Reliability Considerations</strong></h2>
<p>One main design goal was to avoid unnecessary overhead to message processing.</p>
<p>The implementation focuses on cache locality and step-by-step pre-processing:</p>
<ul>
<li><p>Schema IDs are extracted from the wire format <em>(or message metadata).</em></p>
</li>
<li><p>Unknown schemas are fetched once and cached.</p>
</li>
<li><p>Repeated schema IDs in a batch are resolved from the cache.</p>
</li>
<li><p>Encode/decode continues in synchronous paths.</p>
</li>
</ul>
<p>This setup cuts down on unnecessary async work while still supporting remote schema registries safely. It also helps keep throughput and performance steady, as you’d expect from a Node.js client.</p>
<p>Operationally, this also makes failures easier to understand. Schema resolution errors happen during fetch or preparation, while codec errors are still linked to payload and schema compatibility.</p>
<h2><strong>Also Included in This Release</strong></h2>
<p>The v1.27.0 release also shipped quality improvements around consumer behaviour and protocol handling, with broad test coverage and new playground clients for:</p>
<ul>
<li><p>AVRO</p>
</li>
<li><p>Protobuf</p>
</li>
<li><p>JSON Schema</p>
</li>
<li><p>Authenticated Schema Registry setups</p>
</li>
</ul>
<p>The end result is a production-ready integration you can try out quickly, starting in local development and moving to secure production registries.</p>
<h2><strong>Experimental API Notice</strong></h2>
<p><code>ConfluentSchemaRegistry</code> and its related hooks are currently <strong>experimental</strong>. They may change in minor or patch releases as we keep improving them based on real-world use and feedback.</p>
<p>If you plan to use this in production, make sure to pin your versions and check the release notes. We’ll keep refining the API based on feedback from real deployments.</p>
<p>If your team is rolling this out, here’s a practical way to start:</p>
<ol>
<li><p>Start with one topic and one schema format <em>(typically AVRO or JSON Schema)</em></p>
</li>
<li><p>Validate serialization/deserialization behaviour in staging with real payloads.</p>
</li>
<li><p>Expand topic coverage and introduce auth/credential providers as needed.</p>
</li>
</ol>
<h2><strong>Getting Started</strong></h2>
<p>Install the package:</p>
<pre><code class="language-plaintext">npm install @platformatic/kafka
</code></pre>
<p>For Protobuf support, also install:</p>
<pre><code class="language-plaintext">npm install protobufjs
</code></pre>
<p>Next, follow the full integration guide in the documentation:</p>
<ul>
<li><p><a href="https://github.com/platformatic/kafka/blob/main/docs/confluent-schema-registry.md">Confluent Schema Registry docs</a></p>
</li>
<li><p><a href="https://github.com/platformatic/kafka/releases/tag/v1.27.0">v1.27.0 release notes</a></p>
</li>
</ul>
<p>If you give it a try, we’d love to hear your feedback at <a href="mailto:hello@platformatic.dev">hello@platformatic.dev</a>. Real-world schema workflows will help shape the next version of this API and guide our priorities for future improvements.</p>
<p>Thanks for building with us! 🚀</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
