<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged nodejs</title>
<link>https://coderlegion.com/tag/nodejs</link>
<description>Powered by Question2Answer</description>
<item>
<title>Building a static blog with Sitelo, one function per page</title>
<link>https://coderlegion.com/33109/building-a-static-blog-with-sitelo-one-function-per-page</link>
<description>&lt;p&gt;In my &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/paulbrowne/javascript-in-html-out-building-a-simpler-static-site-generator-5ln&quot;&gt;last post on DEV&lt;/a&gt; I explained why I built Sitelo: most websites are documents, and documents shouldn't need a JavaScript application to reach the browser. That post was the argument. This one is the practice.&lt;/p&gt;
&lt;p&gt;We'll build a small blog from an empty folder: a home page, post pages generated from JSON files, and a comments section that stays fresh on an otherwise static page. Along the way I'll point out what Sitelo is doing at each step, and what it deliberately doesn't do.&lt;/p&gt;
&lt;p&gt;The whole framework fits in one sentence, and it's worth keeping in mind as we go:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Files are routes. Functions return HTML. Browser code is explicit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Everything below is that sentence, applied.&lt;/p&gt;
&lt;h3 id=&quot;step-1-one-page-is-one-function&quot;&gt;Step 1: one page is one function&lt;/h3&gt;
&lt;p&gt;Start with an empty folder and install Sitelo. It needs Node 20.19+ or 22.12+, and Vite is bundled, so there's nothing else to install or configure.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm init -y
npm pkg set type=module
npm install -D sitelo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add two scripts to &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;scripts&quot;: {
  &quot;dev&quot;: &quot;sitelo&quot;,
  &quot;build&quot;: &quot;sitelo build&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the first page. Any file in &lt;code&gt;src/&lt;/code&gt; ending in &lt;code&gt;.ht.js&lt;/code&gt; is a page, and its default export returns HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/index.ht.js
export default () =&amp;gt; `
  &amp;lt;html lang=&quot;en&quot;&amp;gt;
    &amp;lt;head&amp;gt;
      &amp;lt;title&amp;gt;My blog&amp;lt;/title&amp;gt;
      &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;/styles.css&quot;&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
      &amp;lt;h1&amp;gt;Hello world&amp;lt;/h1&amp;gt;
    &amp;lt;/body&amp;gt;
  &amp;lt;/html&amp;gt;
`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a &lt;code&gt;src/styles.css&lt;/code&gt; with a few rules, then run &lt;code&gt;npm run build&lt;/code&gt; and you get:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dist/
  index.html   ← your HTML, with &amp;lt;!DOCTYPE html&amp;gt; added
  styles.css   ← bundled and minified, because the page linked it
  404.html     ← a default one, until you write src/404.ht.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function ran once, at build time, in Node. The browser gets the string it returned and nothing else: no runtime, no hydration, no bundle of page code. If you want JavaScript in the browser, you add a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag, the same as you would in a hand-written HTML file.&lt;/p&gt;
&lt;p&gt;Template literals aren't the only option. A page can also return JSX from a &lt;code&gt;.ht.jsx&lt;/code&gt; or &lt;code&gt;.ht.tsx&lt;/code&gt; file, which is rendered to a static string with &lt;code&gt;react-dom/server&lt;/code&gt;. Or you can use my companion library &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/javascript-to-html&quot;&gt;javascript-to-html&lt;/a&gt;, where every element is a function. I'll stick with template literals here because they need no explanation.&lt;/p&gt;
&lt;h3 id=&quot;step-2-a-layout-is-just-another-function&quot;&gt;Step 2: a layout is just another function&lt;/h3&gt;
&lt;p&gt;A blog needs the same &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; and header on every page. There's no layout system to learn for this, because a layout is a function that takes some HTML and wraps it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/lib/layout.js
export const layout = ({ title, children }) =&amp;gt; `
  &amp;lt;html lang=&quot;en&quot;&amp;gt;
    &amp;lt;head&amp;gt;
      &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
      &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&amp;gt;
      &amp;lt;title&amp;gt;${title} · My blog&amp;lt;/title&amp;gt;
      &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;/styles.css&quot;&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
      &amp;lt;header&amp;gt;&amp;lt;a href=&quot;/&quot;&amp;gt;My blog&amp;lt;/a&amp;gt; · &amp;lt;a href=&quot;/blog&quot;&amp;gt;Posts&amp;lt;/a&amp;gt;&amp;lt;/header&amp;gt;
      &amp;lt;main&amp;gt;${children}&amp;lt;/main&amp;gt;
    &amp;lt;/body&amp;gt;
  &amp;lt;/html&amp;gt;
`
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/index.ht.js
import { layout } from './lib/layout.js'

export default () =&amp;gt; layout({
  title: 'Home',
  children: '&amp;lt;h1&amp;gt;Hello world&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Welcome to my blog.&amp;lt;/p&amp;gt;',
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;layout.js&lt;/code&gt; doesn't end in &lt;code&gt;.ht.js&lt;/code&gt;, so it isn't a page. It also never ends up in &lt;code&gt;dist/&lt;/code&gt;. Sitelo only emits a JavaScript or TypeScript file when your HTML references it with a &lt;code&gt;&amp;lt;script src&amp;gt;&lt;/code&gt;; a module you only import from page code stays on the build side. That rule matters more later, when the helpers you import hold API keys or database code. They can't leak into the browser by accident.&lt;/p&gt;
&lt;p&gt;The same rule works in the other direction. Write &lt;code&gt;&amp;lt;script type=&quot;module&quot; src=&quot;/main.ts&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt; and Sitelo bundles &lt;code&gt;src/main.ts&lt;/code&gt; with esbuild, emits &lt;code&gt;main.js&lt;/code&gt;, and rewrites the tag to match. Browser code exists only where you asked for it.&lt;/p&gt;
&lt;h3 id=&quot;step-3-posts-from-json-files&quot;&gt;Step 3: posts from JSON files&lt;/h3&gt;
&lt;p&gt;Now for the posts. I'll keep them in the repo as JSON, one file per post, outside &lt;code&gt;src/&lt;/code&gt; because they're content rather than code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// data/posts/hello-world.json
{
  &quot;title&quot;: &quot;Hello, world&quot;,
  &quot;date&quot;: &quot;2026-10-01&quot;,
  &quot;body&quot;: &quot;&amp;lt;p&amp;gt;My first post.&amp;lt;/p&amp;gt;&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The post page lives at &lt;code&gt;src/blog/[slug].ht.js&lt;/code&gt;. The square brackets make it a dynamic route, and it exports two more functions next to the page itself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;generateStaticParams()&lt;/code&gt; lists the pages to build, one per post.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data()&lt;/code&gt; loads what one page needs. Whatever it returns arrives in the page function as &lt;code&gt;data&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/blog/[slug].ht.js
import { readJsonCollection } from 'sitelo/data'
import { layout } from '../lib/layout.js'

const posts = () =&amp;gt; readJsonCollection('data/posts', { sort: '-date' })

export async function generateStaticParams() {
  return (await posts()).map((post) =&amp;gt; ({ slug: post.slug }))
}

export async function data({ params }) {
  return (await posts()).find((post) =&amp;gt; post.slug === params.slug)
}

export default ({ data: post }) =&amp;gt; layout({
  title: post.title,
  children: `
    &amp;lt;article&amp;gt;
      &amp;lt;h1&amp;gt;${post.title}&amp;lt;/h1&amp;gt;
      &amp;lt;time datetime=&quot;${post.date}&quot;&amp;gt;${post.date}&amp;lt;/time&amp;gt;
      ${post.body}
    &amp;lt;/article&amp;gt;
  `,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;readJsonCollection&lt;/code&gt; reads every &lt;code&gt;.json&lt;/code&gt; file in the folder and takes each slug from its file name. It parses each file once per build, however many pages ask for it, and two posts with the same slug fail the build instead of one silently overwriting the other.&lt;/p&gt;
&lt;p&gt;The index page is the same idea without the brackets:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/blog/index.ht.js
import { readJsonCollection } from 'sitelo/data'
import { layout } from '../lib/layout.js'

export const data = () =&amp;gt; readJsonCollection('data/posts', { sort: '-date' })

export default ({ data: posts }) =&amp;gt; layout({
  title: 'Posts',
  children: `
    &amp;lt;h1&amp;gt;Posts&amp;lt;/h1&amp;gt;
    &amp;lt;ul&amp;gt;
      ${posts.map((post) =&amp;gt; `&amp;lt;li&amp;gt;&amp;lt;a href=&quot;/blog/${post.slug}&quot;&amp;gt;${post.title}&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;`).join('')}
    &amp;lt;/ul&amp;gt;
  `,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A build now writes &lt;code&gt;dist/blog/index.html&lt;/code&gt; and one &lt;code&gt;dist/blog/&amp;lt;slug&amp;gt;/index.html&lt;/code&gt; per post. In the dev server (&lt;code&gt;npm run dev&lt;/code&gt;) pages render on request through Vite, so editing a JSON file reloads the browser, and an error points at the exact line of your page in the terminal.&lt;/p&gt;
&lt;p&gt;If your posts live in a CMS instead, &lt;code&gt;data()&lt;/code&gt; can &lt;code&gt;fetch&lt;/code&gt; them. Sitelo exports &lt;code&gt;fetchWithCache&lt;/code&gt;, a drop-in for &lt;code&gt;fetch&lt;/code&gt; that caches responses during the build. A 500-page site then doesn't make 500 identical requests to your API.&lt;/p&gt;
&lt;h3 id=&quot;step-4-fresh-comments-on-a-static-page&quot;&gt;Step 4: fresh comments on a static page&lt;/h3&gt;
&lt;p&gt;Comments are the classic reason static blogs reach for a client-side widget or give up and go server-rendered. Sitelo's answer is a server island: the page stays static, and one marked region is rendered on the server when someone views it.&lt;/p&gt;
&lt;p&gt;An island is a module in &lt;code&gt;src/islands/&lt;/code&gt; whose default export returns HTML. Islands are fragments rather than pages, so the file is plain &lt;code&gt;.js&lt;/code&gt;, not &lt;code&gt;.ht.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/islands/comments.js
import { getComments } from '../lib/comments.js'

const escape = (s) =&amp;gt; String(s).replace(/[&amp;amp;&amp;lt;&amp;gt;&quot;']/g, (c) =&amp;gt; `&amp;amp;#${c.charCodeAt(0)};`)

export default async function comments({ props }) {
  const list = await getComments(props.postId)
  if (!list.length) return '&amp;lt;p&amp;gt;No comments yet.&amp;lt;/p&amp;gt;'
  return `&amp;lt;ul&amp;gt;${list.map((c) =&amp;gt; `&amp;lt;li&amp;gt;&amp;lt;b&amp;gt;${escape(c.author)}&amp;lt;/b&amp;gt; ${escape(c.text)}&amp;lt;/li&amp;gt;`).join('')}&amp;lt;/ul&amp;gt;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;getComments&lt;/code&gt; is whatever your comment store is: a database query, a call to a hosted service. Comments are user input, hence the &lt;code&gt;escape&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Place the island in the post page with &lt;code&gt;island()&lt;/code&gt;. The third argument is the fallback HTML that ships in the static build, and &lt;code&gt;when: 'visible'&lt;/code&gt; waits until the reader scrolls near the comments before making the request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/blog/[slug].ht.js (the page function)
import { island } from 'sitelo/islands'

export default ({ params, data: post }) =&amp;gt; layout({
  title: post.title,
  children: `
    &amp;lt;article&amp;gt;…&amp;lt;/article&amp;gt;
    ${island('comments', { postId: params.slug }, '&amp;lt;p&amp;gt;Loading comments…&amp;lt;/p&amp;gt;', { when: 'visible' })}
    &amp;lt;script type=&quot;module&quot; src=&quot;/islands.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  `,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script tag loads a two-line client that finds the placeholders and fetches them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/islands.js
import { mountIslands } from 'sitelo/islands/client'

mountIslands()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;npm run dev&lt;/code&gt; and &lt;code&gt;sitelo preview&lt;/code&gt; this already works: Sitelo serves islands at &lt;code&gt;/_sitelo/islands/&amp;lt;name&amp;gt;&lt;/code&gt;. In production your static host serves the pages, and you mount a small handler wherever you can run server code. &lt;code&gt;createIslandsHandler&lt;/code&gt; from &lt;code&gt;sitelo/islands/server&lt;/code&gt; takes a Web &lt;code&gt;Request&lt;/code&gt; and returns a &lt;code&gt;Response&lt;/code&gt;, so it fits a serverless or edge function. The repo has &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Paul-Browne/sitelo/tree/main/examples/islands&quot;&gt;Netlify and Vercel versions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A few details I cared about getting right:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Props are untrusted.&lt;/strong&gt; They're embedded in the page and sent back on the request, so anyone can edit them. Set &lt;code&gt;SITELO_ISLANDS_SECRET&lt;/code&gt; and the build signs every placeholder with HMAC-SHA256, and the handler rejects edited props with a 403.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Islands are server-only.&lt;/strong&gt; The rule from Step 2 applies: nothing references &lt;code&gt;comments.js&lt;/code&gt; from HTML, so it never reaches &lt;code&gt;dist/&lt;/code&gt;, and neither does your database code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failure is graceful.&lt;/strong&gt; If the island endpoint is down or slow (10 seconds by default), the fallback HTML stays where it was. The page still works.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;step-5-let-the-build-check-your-work&quot;&gt;Step 5: let the build check your work&lt;/h3&gt;
&lt;p&gt;The blog works. The last step is a config file, and it's where Sitelo earns its keep on a real site:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// sitelo.config.js
export default {
  site: 'https://myblog.example',
  rss: {
    site: 'https://myblog.example',
    title: 'My blog',
    description: 'Latest posts',
    routePrefix: '/blog',
  },
  images: true,
  linkCheck: 'error',
  lighthouse: {
    thresholds: { accessibility: 100, 'best-practices': 95, seo: 100 },
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's what each line buys you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;site&lt;/code&gt;&lt;/strong&gt; generates &lt;code&gt;dist/sitemap.xml&lt;/code&gt; from every route, including the ones &lt;code&gt;generateStaticParams&lt;/code&gt; produced.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;rss&lt;/code&gt;&lt;/strong&gt; writes &lt;code&gt;dist/rss.xml&lt;/code&gt; with an item for every page under &lt;code&gt;/blog&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;images&lt;/code&gt;&lt;/strong&gt; turns each &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; into a resized, WebP &lt;code&gt;srcset&lt;/code&gt; with &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; set, so a 3000px hero photo no longer ships to phones. It needs &lt;code&gt;sharp&lt;/code&gt; installed, and it works in the dev server too, so what you see is what you ship.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;linkCheck: 'error'&lt;/code&gt;&lt;/strong&gt; resolves every internal &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; against the files the build actually wrote, the way a static host would. A typo like &lt;code&gt;/blgo&lt;/code&gt; fails the build with the page it's on.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;lighthouse&lt;/code&gt;&lt;/strong&gt; gives &lt;code&gt;sitelo lighthouse&lt;/code&gt; thresholds to enforce. Any score under its threshold exits non-zero, so it can gate CI. Like sharp, Lighthouse is an optional install.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I left &lt;code&gt;performance&lt;/code&gt; out of the thresholds on purpose. It depends on the machine running the audit, and a page that scores 100 on a laptop can score around 60 on a shared CI runner. Accessibility, best practices and SEO are properties of the HTML itself, so they score the same anywhere.&lt;/p&gt;
&lt;p&gt;One check needs no config. If a page links a script or stylesheet that doesn't exist in &lt;code&gt;src/&lt;/code&gt; or &lt;code&gt;public/&lt;/code&gt;, the build fails and lists the paths it tried. That's why Step 1 asked you to create &lt;code&gt;styles.css&lt;/code&gt; first.&lt;/p&gt;
&lt;p&gt;Every build ends with a report of what you're shipping: file counts and sizes per type, the largest files, and how long each phase took. On a static site, &quot;how much JavaScript am I sending?&quot; should be a number you see every time, not something you go looking for.&lt;/p&gt;
&lt;h3 id=&quot;building-it-with-a-coding-agent&quot;&gt;Building it with a coding agent&lt;/h3&gt;
&lt;p&gt;I designed Sitelo with coding agents in mind, and the small surface area matters even more for them than for people. Ask an agent to add a page to an unfamiliar framework and it reaches for whatever it has seen most: React components, Next.js conventions, Astro frontmatter. With Sitelo there's less to get wrong, because there's less of everything. A page is a function, a route is a file, and browser code is a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;
&lt;p&gt;To close the remaining gap, Sitelo ships two &lt;a rel=&quot;nofollow&quot; href=&quot;https://agentskills.io&quot;&gt;Agent Skills&lt;/a&gt;: one for the framework (pages, routing, data, islands, deployment) and one for its component library. Install them into your project and Claude Code, Cursor, Codex and other agents load them when a task needs them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx skills add Paul-Browne/sitelo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The skills also ship inside the npm package, matching the version you installed. Sitelo's test suite checks every API the skills mention against the package's real exports, so a renamed or removed API fails the tests before it reaches a release. For agents that prefer to read a URL, the docs publish &lt;a rel=&quot;nofollow&quot; href=&quot;https://sitelo.dev/llms.txt&quot;&gt;sitelo.dev/llms.txt&lt;/a&gt;, plus a single-file &lt;code&gt;llms-full.txt&lt;/code&gt; with every guide in Markdown.&lt;/p&gt;
&lt;p&gt;The build checks from Step 5 help here too. An agent that invents a stylesheet path or links to a page that doesn't exist gets a failed build naming the problem, not a site that quietly ships broken.&lt;/p&gt;
&lt;h3 id=&quot;what-we-built-and-when-not-to-use-it&quot;&gt;What we built, and when not to use it&lt;/h3&gt;
&lt;p&gt;In five steps we went from an empty folder to a blog with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a home page, a post index and one page per JSON file&lt;/li&gt;
&lt;li&gt;comments rendered on the server at view time, on otherwise static pages&lt;/li&gt;
&lt;li&gt;a sitemap, an RSS feed and responsive images&lt;/li&gt;
&lt;li&gt;a build that fails on broken links and missing assets, plus a Lighthouse gate for accessibility and SEO&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The browser receives HTML, one stylesheet and a 1.7 kB island loader. That's all, because those are the only things we asked for.&lt;/p&gt;
&lt;p&gt;Sitelo isn't the right tool for everything. JSX pages render to static HTML only, so &lt;code&gt;onClick&lt;/code&gt; handlers and hooks do nothing in the browser. If you're building an application, with client-side state and routing, use an application framework. Astro, Next.js and Eleventy are all excellent at what they do. Sitelo is for docs, blogs, marketing sites and content sites, where &quot;view source&quot; should show what you wrote.&lt;/p&gt;
&lt;p&gt;If that sounds like your next project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://sitelo.dev/docs&quot;&gt;sitelo.dev/docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Code (MIT licensed): &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Paul-Browne/sitelo&quot;&gt;github.com/Paul-Browne/sitelo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Package: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/sitelo&quot;&gt;npmjs.com/package/sitelo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The repo includes a complete &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Paul-Browne/sitelo/tree/main/examples/blog&quot;&gt;blog example&lt;/a&gt; that goes further than this post, with Markdown posts. And if you find something that makes Sitelo more complicated than it needs to be, please open an issue. Keeping it boring is the whole point.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33109/building-a-static-blog-with-sitelo-one-function-per-page</guid>
<pubDate>Fri, 30 Oct 2026 14:35:56 +0000</pubDate>
</item>
<item>
<title>Demystifying the Facade Pattern in Node.js: Clean APIs Made Simple</title>
<link>https://coderlegion.com/32803/demystifying-the-facade-pattern-in-node-js-clean-apis-made-simple</link>
<description>&lt;h3 id=&quot;what-is-the-facade-pattern-&quot;&gt;What Is the Facade Pattern?&lt;/h3&gt;
&lt;p&gt;The facade pattern is a structural software design pattern that provides an uncomplicated, unified interface in front of a complex collection of classes, libraries, or APIs. Instead of forcing your application code to interact with dozens of intricate internal functions, the facade exposes a single, high-level method that coordinates the messy work behind the scenes. In short, it gives a sprawling, complicated subsystem a clean and welcoming front door.&lt;/p&gt;
&lt;h3 id=&quot;the-real-world-analogy-the-home-theater-remote&quot;&gt;The Real-World Analogy: The Home Theater Remote&lt;/h3&gt;
&lt;p&gt;Imagine you want to watch a movie on a high-end home entertainment setup. To make that happen naturally, you need to execute half a dozen distinct tasks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Turn on the television.&lt;/li&gt;
&lt;li&gt;Set the video input to HDMI 2.&lt;/li&gt;
&lt;li&gt;Turn on the surround sound receiver.&lt;/li&gt;
&lt;li&gt;Adjust the audio amplifier to optical audio.&lt;/li&gt;
&lt;li&gt;Dim the smart living room lights.&lt;/li&gt;
&lt;li&gt;Power on the media streaming console.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You could juggle four different remote controls and flip wall switches yourself, learning the exact quirks of each manufacturer. Alternatively, you could pick up a modern universal remote and press a single button: &lt;strong&gt;Watch Movie&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That universal remote is a facade. It does not replace the television, receiver, or lights; they still exist with all their specialized knobs and settings. Instead, the remote hides the tedious orchestration behind one single, convenient touchpoint.&lt;/p&gt;
&lt;h3 id=&quot;why-it-matters-in-daily-engineering&quot;&gt;Why It Matters in Daily Engineering&lt;/h3&gt;
&lt;p&gt;In everyday software development, projects tend to grow through accretion. A feature starts out simple, but soon requires five external dependencies: an authentication provider, a payment gateway, a cloud storage bucket, an email delivery service, and an internal logging pipeline.&lt;/p&gt;
&lt;p&gt;Without a facade, engineers frequently write &quot;glue code&quot; directly inside their HTTP request handlers (controllers). If five different routes need to charge a customer, each route handler winds up importing and configuring the payment library, checking inventory, calculating sales tax, and firing off analytics calls. This creates two immediate crises:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;High Coupling:&lt;/strong&gt; If the payment provider updates its software development kit (SDK) or changes its function signatures, you must find and update twenty different files across your project.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cognitive Overload:&lt;/strong&gt; Engineers reviewing or writing features are forced to read through hundreds of lines of low-level plumbing when they only care about whether an order succeeded.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By placing a facade between your core business logic and external subsystems, engineers prevent third-party updates from leaking across the entire codebase. When an underlying vendor changes, you update one facade file, leaving the rest of the application completely untouched.&lt;/p&gt;
&lt;h3 id=&quot;practical-implementation-in-node-js&quot;&gt;Practical Implementation in Node.js&lt;/h3&gt;
&lt;p&gt;Consider an e-commerce checkout flow in an Express and Node.js application. Completing an order touches an inventory service, a payment processor, and a notification service.&lt;/p&gt;
&lt;p&gt;Without a facade, your Express route handler would have to instantiate and coordinate every service manually. Here is how you encapsulate that complexity using a Facade:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Subsystem 1: Inventory Management
class InventoryService {
  checkStock(productId) {
    // Simulate querying database stock levels
    console.log(`Checking stock for item: ${productId}`);
    return true;
  }
  reserve(productId, quantity) {
    console.log(`Reserved ${quantity} unit(s) of ${productId}`);
  }
}

// Subsystem 2: Payment Processing
class PaymentProcessor {
  charge(amount, paymentToken) {
    console.log(`Charging $${amount} using token: ${paymentToken}`);
    return { status: 'success', transactionId: 'txn_987654' };
  }
}

// Subsystem 3: Customer Notifications
class NotificationService {
  sendReceipt(email, transactionId) {
    console.log(`Receipt sent to ${email} for transaction ${transactionId}`);
  }
}

// The Facade: OrderFacade
class OrderFacade {
  constructor() {
    this.inventory = new InventoryService();
    this.payment = new PaymentProcessor();
    this.notifications = new NotificationService();
  }

  processOrder({ productId, quantity, price, paymentToken, customerEmail }) {
    const isAvailable = this.inventory.checkStock(productId);
    if (!isAvailable) {
      throw new Error('Product is out of stock.');
    }

    this.inventory.reserve(productId, quantity);
    const paymentResult = this.payment.charge(price * quantity, paymentToken);
    this.notifications.sendReceipt(customerEmail, paymentResult.transactionId);

    return { success: true, transactionId: paymentResult.transactionId };
  }
}

// Express Route Usage
const express = require('express');
const app = express();
app.use(express.json());

const orderFacade = new OrderFacade();

app.post('/api/checkout', (req, res) =&amp;gt; {
  try {
    const orderConfirmation = orderFacade.processOrder(req.body);
    return res.status(200).json(orderConfirmation);
  } catch (error) {
    return res.status(400).json({ error: error.message });
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The route handler does not know or care how inventory reservation works or what parameters the payment gateway demands. It invokes &lt;code&gt;processOrder()&lt;/code&gt; and receives a clear outcome.&lt;/p&gt;
&lt;h3 id=&quot;key-takeaway&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;A facade is not an abstraction that conceals how a system works to make it private; it is an ergonomic shield that keeps your application's day-to-day feature code clean, readable, and insulated from volatile third-party tools.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/architectural-simplicity-why-facade.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32803/demystifying-the-facade-pattern-in-node-js-clean-apis-made-simple</guid>
<pubDate>Wed, 28 Oct 2026 10:10:50 +0000</pubDate>
</item>
<item>
<title>Understanding Database Replication: How to Keep Your Apps Fast and Reliable</title>
<link>https://coderlegion.com/32656/understanding-database-replication-how-to-keep-your-apps-fast-and-reliable</link>
<description>&lt;h3 id=&quot;what-is-database-replication-&quot;&gt;What is Database Replication?&lt;/h3&gt;
&lt;p&gt;Database replication is the automated process of keeping multiple exact copies of a database synchronized across different server instances. When data is added, updated, or removed in the primary database, those changes are copied to one or more replica databases in near real-time. This setup ensures that if one database fails or experiences high traffic, the overall application remains operational, responsive, and secure against data loss.&lt;/p&gt;
&lt;h3 id=&quot;a-real-world-analogy-the-restaurant-kitchen-whiteboard&quot;&gt;A Real-World Analogy: The Restaurant Kitchen Whiteboard&lt;/h3&gt;
&lt;p&gt;Imagine a high-volume restaurant kitchen with a single Head Chef and five Line Cooks. Every time a new order arrives or a special menu item changes, the Head Chef writes it down on a master whiteboard in the center of the kitchen.&lt;/p&gt;
&lt;p&gt;If all five Line Cooks had to walk up to that single whiteboard every few seconds to read the latest order, a massive bottleneck would form around the Head Chef. Cooking would grind to a halt because everyone is waiting in line just to read information.&lt;/p&gt;
&lt;p&gt;To solve this, the kitchen hires a runner whose sole job is to copy whatever the Head Chef writes on the master whiteboard onto five individual clipboards stationed at each Line Cook's workstation. The Head Chef remains the only person writing new orders (the Primary database), while the Line Cooks read from their dedicated station clipboards (the Read Replicas). Orders flow instantly, cooks stay productive, and the head chef is never overwhelmed.&lt;/p&gt;
&lt;h3 id=&quot;why-database-replication-matters-in-modern-engineering&quot;&gt;Why Database Replication Matters in Modern Engineering&lt;/h3&gt;
&lt;p&gt;In real-world web applications, user activity is overwhelmingly biased toward reading data rather than writing it. For example, millions of people scroll through social media posts or view e-commerce products for every one person who creates a new post or completes a purchase. If every read request hit the exact same database server handling write operations, that single database would quickly exhaust its memory and processing power.&lt;/p&gt;
&lt;p&gt;Engineers use database replication to solve several critical challenges:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;High Availability and Fault Tolerance&lt;/strong&gt;: A hardware failure or crash on the primary database server does not bring down the entire website. System administrators can quickly promote a replica to become the new primary writer with minimal downtime.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance Scaling&lt;/strong&gt;: Read traffic is distributed across multiple read-only replica servers. This practice, known as load balancing, prevents any single server from becoming a CPU bottleneck.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Heavy Reporting Offloading&lt;/strong&gt;: Running complex analytical queries (like generating monthly revenue reports) takes immense computing resources. Performing these queries on a replica ensures transactional write operations for live customers remain fast and unaffected.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;implementing-read-write-splitting-in-node-js&quot;&gt;Implementing Read/Write Splitting in Node.js&lt;/h3&gt;
&lt;p&gt;To take advantage of database replication, backend application code must route write commands (&lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;) to the primary database, while directing read queries (&lt;code&gt;SELECT&lt;/code&gt;) to replica nodes. A common database driver used in Node.js applications is &lt;code&gt;mysql2&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here is how you can set up dual connections in a Node.js module to execute read/write splitting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// db.js - Database connection module for Primary and Replica instances
const mysql = require('mysql2/promise');

// Primary connection reserved strictly for write operations
const primaryPool = mysql.createPool({
  host: process.env.DB_PRIMARY_HOST || 'primary.db.example.com',
  user: 'admin',
  password: 'securepassword',
  database: 'store_db',
  connectionLimit: 10
});

// Replica connection pool used for read-only queries
const replicaPool = mysql.createPool({
  host: process.env.DB_REPLICA_HOST || 'replica-1.db.example.com',
  user: 'read_only_user',
  password: 'readpassword',
  database: 'store_db',
  connectionLimit: 20
});

// Function to execute write operations on the primary DB
async function executeWrite(sqlQuery, params) {
  const [result] = await primaryPool.execute(sqlQuery, params);
  return result;
}

// Function to execute read operations on the replica DB
async function executeRead(sqlQuery, params) {
  const [rows] = await replicaPool.execute(sqlQuery, params);
  return rows;
}

module.exports = {
  executeWrite,
  executeRead
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, calling &lt;code&gt;executeWrite()&lt;/code&gt; executes the SQL query on the primary database node, ensuring that structural data modifications land safely on the source authority. Calling &lt;code&gt;executeRead()&lt;/code&gt; queries the replica node, taking performance stress off the writer server.&lt;/p&gt;
&lt;h3 id=&quot;key-takeaway-1&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;Database replication shifts application architecture from a fragile single point of failure into a redundant, horizontally scalable infrastructure. By separating write transactions from high-volume read requests, development teams guarantee that their software stays online and lightning-fast, even during massive user traffic surges.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/scaling-applications-with-database.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32656/understanding-database-replication-how-to-keep-your-apps-fast-and-reliable</guid>
<pubDate>Tue, 27 Oct 2026 09:21:52 +0000</pubDate>
</item>
<item>
<title>Stop Inventing Custom API Errors: A Guide to RFC 7807 Problem Details</title>
<link>https://coderlegion.com/32579/stop-inventing-custom-api-errors-a-guide-to-rfc-7807-problem-details</link>
<description>&lt;p&gt;RFC 7807 Problem Details is an official technical specification that defines a standard JSON format for describing errors returned by HTTP web APIs. Instead of having every software engineer create their own custom fields like &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;err_code&lt;/code&gt;, or &lt;code&gt;reason&lt;/code&gt;, this standard establishes predictable key names such as &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;detail&lt;/code&gt;, and &lt;code&gt;instance&lt;/code&gt;. This gives both computer programs and human developers a uniform way to understand exactly what went wrong during an API request.&lt;/p&gt;
&lt;h3 id=&quot;the-everyday-analogy-car-diagnostic-reports&quot;&gt;The Everyday Analogy: Car Diagnostic Reports&lt;/h3&gt;
&lt;p&gt;Imagine taking your car to a mechanic when the check engine light comes on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option A:&lt;/strong&gt; The mechanic hands you a piece of paper that simply says: &quot;Car is broken. Cost: $300.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option B:&lt;/strong&gt; The mechanic hands you a standardized vehicle diagnostic report that lists the exact sensor code (e.g., P0300), a short title (&quot;Random Cylinder Misfire&quot;), the severity level, specific details (&quot;Cylinder 3 failed 14 times in the past 10 miles&quot;), and a reference link to the manufacturer's repair guide.&lt;/p&gt;
&lt;p&gt;Option A leaves you guessing and forces you to ask ten follow-up questions. Option B gives you instant, structured clarity. RFC 7807 acts as that standardized diagnostic report for your web applications.&lt;/p&gt;
&lt;h3 id=&quot;why-rfc-7807-matters-for-engineers&quot;&gt;Why RFC 7807 Matters for Engineers&lt;/h3&gt;
&lt;p&gt;In modern software engineering, web applications are built by connecting multiple services together. Front-end user interfaces (like React apps) communicate with back-end servers running Express.js, which in turn communicate with databases or external payment systems.&lt;/p&gt;
&lt;p&gt;When something fails—such as an invalid credit card or an out-of-stock product—the back-end server must notify the front-end application. Without a standard like RFC 7807, team A might structure errors as &lt;code&gt;{ &quot;error&quot;: &quot;Invalid payload&quot; }&lt;/code&gt;, while team B uses &lt;code&gt;{ &quot;statusCode&quot;: 400, &quot;description&quot;: &quot;Bad input&quot; }&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This inconsistency causes real operational pain:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fragile UI Code&lt;/strong&gt;: Front-end developers must write endless conditional &lt;code&gt;if/else&lt;/code&gt; checks to parse different error formats from different endpoints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wasted Debugging Hours&lt;/strong&gt;: Software engineers spend extra time reading through server logs trying to figure out what a vague error message actually means.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Poor User Experience&lt;/strong&gt;: Applications fail silently or show generic &quot;An unknown error occurred&quot; popups because the front-end couldn't parse the unexpected error structure.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By enforcing RFC 7807, teams agree on a single contract. Front-end apps can use one central error-handling mechanism to extract the exact field that failed and display a meaningful message to the user.&lt;/p&gt;
&lt;h3 id=&quot;code-example-express-js-error-handler&quot;&gt;Code Example: Express.js Error Handler&lt;/h3&gt;
&lt;p&gt;Here is how you can implement an RFC 7807 compliant error handler in a Node.js and Express server. The custom HTTP content type for this standard is &lt;code&gt;application/problem+json&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require('express');
const app = express();

app.use(express.json());

// Route that triggers a validation error
app.post('/api/users', (req, res, next) =&amp;gt; {
  const { email } = req.body;
  if (!email) {
    const error = new Error('Email field is required.');
    error.status = 400;
    error.type = 'https://api.example.com/errors/missing-field';
    error.title = 'Invalid Request Payload';
    error.instance = '/api/users';
    return next(error);
  }
  res.status(201).json({ message: 'User created' });
});

// Central RFC 7807 Error Handling Middleware
app.use((err, req, res, next) =&amp;gt; {
  const status = err.status || 500;
  
  const problemDetails = {
    type: err.type || 'https://api.example.com/errors/internal-error',
    title: err.title || 'Internal Server Error',
    status: status,
    detail: err.message || 'An unexpected error occurred on the server.',
    instance: err.instance || req.originalUrl
  };

  // Set the standard RFC 7807 header
  res.setHeader('Content-Type', 'application/problem+json');
  res.status(status).json(problemDetails);
});

app.listen(3000, () =&amp;gt; console.log('Server running on port 3000'));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;key-takeaway-2&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;RFC 7807 transforms unpredictable API errors into predictable, structured data that both machines and humans can process seamlessly. Adopting this standard eliminates entire classes of error-handling bugs, simplifies frontend integration, and establishes clear debugging contracts across your entire software ecosystem.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/how-rfc-7807-problem-details-transforms.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32579/stop-inventing-custom-api-errors-a-guide-to-rfc-7807-problem-details</guid>
<pubDate>Mon, 26 Oct 2026 17:41:23 +0000</pubDate>
</item>
<item>
<title>Your Pagination Is Showing Users the Same Item Twice (and Skipping Others). Here's Why Offset Breaks</title>
<link>https://coderlegion.com/32501/your-pagination-showing-users-the-same-item-twice-and-skipping-others-heres-offset-breaks</link>
<description>&lt;p&gt;A user scrolls through your feed. Page 1 ends with &quot;Post 20&quot;. They tap &quot;Next&quot;, and page 2 starts with... &quot;Post 20&quot; again.&lt;/p&gt;
&lt;p&gt;Another user pages through an admin table and never sees order #4817. It exists. It is in the database. It just never showed up on any page.&lt;/p&gt;
&lt;p&gt;Nobody changed any code. The data changed underneath the pagination, and your &lt;code&gt;skip&lt;/code&gt; and &lt;code&gt;limit&lt;/code&gt; could not cope.&lt;/p&gt;
&lt;h3 id=&quot;the-code-everyone-writes&quot;&gt;The Code Everyone Writes&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;app.get('/api/posts', async (req, res) =&amp;gt; {
  const page = Number(req.query.page) || 1;
  const limit = 20;

  const posts = await Post.find()
    .sort({ createdAt: -1 })
    .skip((page - 1) * limit)
    .limit(limit);

  res.json(posts);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is simple, it works in development, and &lt;code&gt;?page=2&lt;/code&gt; is easy to understand. That is why it is everywhere.&lt;/p&gt;
&lt;h3 id=&quot;why-it-breaks&quot;&gt;Why It Breaks&lt;/h3&gt;
&lt;p&gt;Offset pagination says &quot;skip the first N rows, then give me the next 20&quot;. It counts positions, and positions move when the data does.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Duplicates.&lt;/strong&gt; Someone publishes a new post while a reader is on page 1. Every existing post shifts down one position. When the reader requests page 2, the last item of page 1 is now the first item of page 2.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Skips.&lt;/strong&gt; The reverse: a post is deleted while the reader is on page 1. Everything shifts up one, and the first item of what would have been page 2 slides into page 1's range. The reader never sees it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Before:  [1 2 3 4 5 | 6 7 8 9 10]    page 1 = 1-5, page 2 = 6-10
New item arrives at the top:
After:   [0 1 2 3 4 | 5 6 7 8 9]     page 2 now starts with 5, which they already saw
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-second-problem-it-gets-slower-on-deep-pages&quot;&gt;The Second Problem: It Gets Slower on Deep Pages&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;skip(100000)&lt;/code&gt; does not jump to row 100,000. The database has to walk past 100,000 documents and throw them away before returning your 20. Page 1 is fast. Page 5,000 is slow, and the cost grows with the page number.&lt;/p&gt;
&lt;p&gt;This is why a feed that feels fine with 1,000 rows slows down quietly at a million.&lt;/p&gt;
&lt;h3 id=&quot;the-fix-cursor-pagination&quot;&gt;The Fix: Cursor Pagination&lt;/h3&gt;
&lt;p&gt;Instead of &quot;skip N&quot;, say &quot;give me the 20 items &lt;strong&gt;after this one&lt;/strong&gt;&quot;. The anchor is a &lt;strong&gt;cursor&lt;/strong&gt;: a value that identifies where the last page ended.&lt;/p&gt;
&lt;p&gt;A new item arriving at the top cannot shift your place, because you are not counting positions. You are standing on a specific item.&lt;/p&gt;
&lt;h4 id=&quot;step-1-sort-by-something-stable-with-a-tie-breaker&quot;&gt;Step 1: Sort by something stable, with a tie-breaker&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;createdAt&lt;/code&gt; alone is not enough, because two documents can share the same timestamp. Add &lt;code&gt;_id&lt;/code&gt; as a tie-breaker so the order is always exact:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;.sort({ createdAt: -1, _id: -1 })
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-2-support-the-query-with-an-index&quot;&gt;Step 2: Support the query with an index&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;PostSchema.index({ createdAt: -1, _id: -1 });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this index, the database jumps straight to the cursor and reads 20 documents, whether you are on page 1 or page 5,000.&lt;/p&gt;
&lt;h4 id=&quot;step-3-query-after-the-cursor-&quot;&gt;Step 3: Query &quot;after the cursor&quot;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;app.get('/api/posts', async (req, res) =&amp;gt; {
  const limit = 20;
  const cursor = req.query.cursor ? decodeCursor(req.query.cursor) : null;

  const filter = cursor
    ? {
        $or: [
          { createdAt: { $lt: cursor.createdAt } },
          { createdAt: cursor.createdAt, _id: { $lt: cursor._id } },
        ],
      }
    : {};

  const rows = await Post.find(filter)
    .sort({ createdAt: -1, _id: -1 })
    .limit(limit + 1); // fetch one extra to know if there is a next page

  const hasMore = rows.length &amp;gt; limit;
  const items = hasMore ? rows.slice(0, limit) : rows;
  const last = items[items.length - 1];

  res.json({
    items,
    nextCursor: hasMore ? encodeCursor(last) : null,
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;$or&lt;/code&gt; says: &quot;older than the cursor, or the same timestamp but a smaller &lt;code&gt;_id&lt;/code&gt;&quot;. That second branch is what stops items with identical timestamps from being skipped or repeated.&lt;/p&gt;
&lt;h4 id=&quot;step-4-encode-the-cursor-so-clients-do-not-depend-on-it&quot;&gt;Step 4: Encode the cursor so clients do not depend on it&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const encodeCursor = (doc) =&amp;gt;
  Buffer.from(JSON.stringify({ createdAt: doc.createdAt, _id: doc._id })).toString('base64url');

const decodeCursor = (str) =&amp;gt; {
  const { createdAt, _id } = JSON.parse(Buffer.from(str, 'base64url').toString());
  return { createdAt: new Date(createdAt), _id: new mongoose.Types.ObjectId(_id) };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clients treat it as an opaque string and send it back as it is. You can change what is inside later without breaking anyone. In production, validate the decoded values and handle a malformed cursor with a &lt;code&gt;400&lt;/code&gt;, not a crash.&lt;/p&gt;
&lt;h3 id=&quot;what-you-give-up&quot;&gt;What You Give Up&lt;/h3&gt;
&lt;p&gt;Cursor pagination has a trade-off, and you should know it before switching:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No &quot;jump to page 47&quot;.&lt;/strong&gt; You can only go forward (or back, with a second cursor). For feeds and infinite scroll, that is perfect. For a numbered page bar, it is not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No cheap total count.&lt;/strong&gt; &lt;code&gt;countDocuments&lt;/code&gt; on a large collection is not free either way, so many apps drop the &quot;page 3 of 500&quot; display.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;when-offset-is-still-fine&quot;&gt;When Offset Is Still Fine&lt;/h3&gt;
&lt;p&gt;Do not rewrite everything. Offset pagination is a reasonable choice for small, slow-changing tables, like an admin list with a few hundred rows, where users really do want page numbers.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Situation &lt;/th&gt;&lt;th&gt;Use&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Feed, timeline, infinite scroll &lt;/td&gt;&lt;td&gt;Cursor&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Large collection, deep pages &lt;/td&gt;&lt;td&gt;Cursor&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Data changes while people browse &lt;/td&gt;&lt;td&gt;Cursor&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Small admin table with page numbers &lt;/td&gt;&lt;td&gt;Offset is fine&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;the-rule&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Offset pagination counts positions, and positions change. Cursor pagination anchors to a specific item, so it stays correct when data is added or removed.&lt;/strong&gt; Always sort by a unique, indexed combination, and fetch one extra row to know whether there is a next page.&lt;/p&gt;
&lt;p&gt;Quick audit: search your project for &lt;code&gt;.skip(&lt;/code&gt;. For each hit, ask whether the collection can grow past a few thousand documents, or change while someone is paging through it.&lt;/p&gt;
&lt;p&gt;If you work with MongoDB, I collect more patterns like this, around indexes and queries, in my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com/blog/mongodb-mongoose-patterns-2026&quot;&gt;MongoDB and Mongoose patterns guide&lt;/a&gt;, and you can browse my Next.js &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;templates on pixelanas.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Have you ever been bitten by duplicate or missing items in a paginated list? Tell me how you noticed in the comments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32501/your-pagination-showing-users-the-same-item-twice-and-skipping-others-heres-offset-breaks</guid>
<pubDate>Mon, 26 Oct 2026 11:11:24 +0000</pubDate>
</item>
<item>
<title>Your 'Pay Now' Button Can Charge Twice. Idempotency Keys Are the Fix Nobody Adds Until It Happens</title>
<link>https://coderlegion.com/32382/your-pay-now-button-can-charge-twice-idempotency-keys-are-the-fix-nobody-adds-until-happens</link>
<description>&lt;p&gt;A customer clicks &quot;Pay now&quot;. Nothing happens for three seconds, so they click again. Or their phone loses signal for a moment and the app quietly retries the request. Or your load balancer times out and sends the request again.&lt;/p&gt;
&lt;p&gt;Your server received the same order twice, and it did exactly what you told it to do: it created two orders.&lt;/p&gt;
&lt;p&gt;This is not a rare edge case. It is the default behavior of every &lt;code&gt;POST&lt;/code&gt; endpoint you have ever written.&lt;/p&gt;
&lt;h3 id=&quot;the-endpoint-that-looks-fine&quot;&gt;The Endpoint That Looks Fine&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;app.post('/api/orders', requireAuth, async (req, res) =&amp;gt; {
  const order = await createOrder(req.user.id, req.body);
  await chargeCard(order);
  res.status(201).json({ orderId: order.id });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing is wrong with this code in isolation. The problem is what it cannot know: whether &lt;em&gt;this&lt;/em&gt; request is new, or a repeat of one that already succeeded.&lt;/p&gt;
&lt;h3 id=&quot;why-this-is-hard-to-catch&quot;&gt;Why This Is Hard to Catch&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You test with one request at a time.&lt;/strong&gt; The bug needs two requests with the same intent, close together.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timeouts are ambiguous.&lt;/strong&gt; When a client times out, it does not know if the server never got the request, or did the work and the response got lost. Retrying is the only safe-looking choice, and it is the one that duplicates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disabling the button is not enough.&lt;/strong&gt; It helps with double clicks but not with network retries, mobile apps, or another service calling your API.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-idea-same-key-same-result&quot;&gt;The Idea: Same Key, Same Result&lt;/h3&gt;
&lt;p&gt;An &lt;strong&gt;idempotent&lt;/strong&gt; operation is one where doing it twice has the same effect as doing it once. &lt;code&gt;POST&lt;/code&gt; is not idempotent by default, but you can make it behave that way:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The client generates a unique &lt;strong&gt;idempotency key&lt;/strong&gt; for each &lt;em&gt;intent&lt;/em&gt; and sends it in a header.&lt;/li&gt;
&lt;li&gt;The server remembers the key together with the result.&lt;/li&gt;
&lt;li&gt;If the same key arrives again, the server returns the saved result and does not do the work again.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Payment providers such as Stripe use exactly this pattern with an &lt;code&gt;Idempotency-Key&lt;/code&gt; header.&lt;/p&gt;
&lt;h3 id=&quot;a-working-version-with-express-and-mongodb&quot;&gt;A Working Version With Express and MongoDB&lt;/h3&gt;
&lt;p&gt;First, a unique index, plus an expiry so old keys clean themselves up:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;await keys.createIndex({ userId: 1, key: 1 }, { unique: true });
await keys.createIndex({ createdAt: 1 }, { expireAfterSeconds: 86400 });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;app.post('/api/orders', requireAuth, async (req, res) =&amp;gt; {
  const key = req.get('Idempotency-Key');
  if (!key) return res.status(400).json({ error: 'Idempotency-Key header required' });

  const userId = req.user.id;
  const bodyHash = hash(stableStringify(req.body));

  // 1. Try to claim the key. Only one request can win this insert.
  try {
    await keys.insertOne({ userId, key, bodyHash, status: 'processing', createdAt: new Date() });
  } catch (err) {
    if (err.code !== 11000) throw err; // 11000 = duplicate key

    const saved = await keys.findOne({ userId, key });
    if (saved.bodyHash !== bodyHash) {
      return res.status(422).json({ error: 'Key was used with a different request' });
    }
    if (saved.status === 'processing') {
      return res.status(409).json({ error: 'Request is still being processed' });
    }
    return res.status(saved.statusCode).json(saved.response); // replay
  }

  // 2. We own the key. Do the work once.
  try {
    const order = await createOrder(userId, req.body);
    await chargeCard(order);

    const response = { orderId: order.id };
    await keys.updateOne(
      { userId, key },
      { $set: { status: 'done', statusCode: 201, response } }
    );
    return res.status(201).json(response);
  } catch (err) {
    await keys.deleteOne({ userId, key }); // let the client retry
    throw err;
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-race-condition-most-examples-miss&quot;&gt;The Race Condition Most Examples Miss&lt;/h3&gt;
&lt;p&gt;A common first attempt is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const existing = await keys.findOne({ userId, key });
if (existing) return res.json(existing.response);
// ... do the work ...
await keys.insertOne({ userId, key, response });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two simultaneous requests both run &lt;code&gt;findOne&lt;/code&gt;, both see nothing, and both do the work. You added a check and still got a duplicate.&lt;/p&gt;
&lt;p&gt;The fix is in the version above: &lt;strong&gt;claim the key first with an insert that can only succeed once&lt;/strong&gt;, and let the unique index decide the winner. The database settles the race, not your code.&lt;/p&gt;
&lt;h3 id=&quot;four-details-that-matter&quot;&gt;Four Details That Matter&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Scope keys per user.&lt;/strong&gt; Two users who happen to generate the same key must not see each other's responses. That is why the index is on &lt;code&gt;{ userId, key }&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Compare the request body.&lt;/strong&gt; If the same key arrives with a different body, that is a bug or an attack, not a retry. Return an error instead of replaying a response that does not match the request. Use a stable stringify (sorted keys) for the hash, or two identical bodies with different property order will hash differently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Generate the key per intent, not per attempt.&lt;/strong&gt; On the client, create the key when the user starts the action and &lt;strong&gt;reuse it for every retry&lt;/strong&gt; of that action. A new key per retry brings the duplicate right back.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const key = crypto.randomUUID(); // once, when the checkout starts
await fetch('/api/orders', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'Idempotency-Key': key },
  body: JSON.stringify(cart),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. Pass the key on to your payment provider too.&lt;/strong&gt; If your server crashes after charging the card but before saving the result, your own table cannot help. A key sent to the provider protects that gap, and the expiry index clears any stuck &lt;code&gt;processing&lt;/code&gt; record.&lt;/p&gt;
&lt;h3 id=&quot;the-rule-1&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Any request that creates something or moves money must be safe to send twice.&lt;/strong&gt; Do not rely on disabled buttons or &quot;the network is reliable&quot;. Give the client a key, claim it atomically on the server, and replay the saved result on a repeat.&lt;/p&gt;
&lt;p&gt;Quick audit: list your &lt;code&gt;POST&lt;/code&gt; endpoints that create orders, payments, invoices, emails or bookings. For each, ask what happens if the request arrives twice, a second apart. If the answer is &quot;two of them&quot;, you have found your next ticket.&lt;/p&gt;
&lt;p&gt;If you work with MongoDB, my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com/blog/mongodb-mongoose-patterns-2026&quot;&gt;MongoDB and Mongoose patterns guide&lt;/a&gt; has more on using the database to enforce rules, and you can browse my Next.js &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;templates on pixelanas.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Have you ever shipped a double-charge or a duplicate-order bug? Tell me how you found out, in the comments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32382/your-pay-now-button-can-charge-twice-idempotency-keys-are-the-fix-nobody-adds-until-happens</guid>
<pubDate>Sun, 25 Oct 2026 11:09:21 +0000</pubDate>
</item>
<item>
<title>Building the MyZubster Metaverse: From Prototype to Secure Realtime Infrastructure</title>
<link>https://coderlegion.com/32147/building-the-myzubster-metaverse-from-prototype-to-secure-realtime-infrastructure</link>
<description>&lt;p&gt;Building the MyZubster Metaverse: From Prototype to Secure Realtime Infrastructure&lt;br&gt;
The MyZubster Metaverse is evolving from a simple interactive experience into a distributed platform designed around secure identity, realtime communication, reliability, privacy, and verifiable collaboration.&lt;br&gt;
During the latest development phase, we focused on the infrastructure required to operate the metaverse safely under real production conditions.&lt;br&gt;
This article explains what we implemented, how the architecture works, what we verified, and what remains before the system can be considered production-ready.&lt;br&gt;
The Current Architecture&lt;br&gt;
The metaverse now follows a layered architecture:&lt;br&gt;
Authenticated user&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MyZubster session&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Short-lived realtime token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Authorized realtime channel&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Distributed gateway&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis coordination layer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Presence, rate limits and observability&lt;br&gt;
The objective is to prevent clients from connecting directly to unrestricted realtime resources.&lt;br&gt;
Every connection must originate from an authenticated MyZubster session and receive only the permissions required for its specific room or operation.&lt;br&gt;
Authenticated Realtime Sessions&lt;br&gt;
We introduced a dedicated token exchange for realtime connections.&lt;br&gt;
The server:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;verifies the current MyZubster session;&lt;/li&gt;
&lt;li&gt;checks whether the user may access the requested resource;&lt;/li&gt;
&lt;li&gt;generates a short-lived realtime token;&lt;/li&gt;
&lt;li&gt;restricts that token to authorized channels;&lt;/li&gt;
&lt;li&gt;rejects anonymous or malformed requests.&lt;br&gt;
This limits the damage caused by leaked tokens and prevents clients from subscribing to arbitrary rooms.&lt;br&gt;
A client cannot simply invent a room identifier and connect to it. Authorization is evaluated server-side before realtime access is granted.&lt;br&gt;
Channel Authorization&lt;br&gt;
Realtime channels are treated as protected resources.&lt;br&gt;
The authorization layer validates:&lt;/li&gt;
&lt;li&gt;user identity;&lt;/li&gt;
&lt;li&gt;session validity;&lt;/li&gt;
&lt;li&gt;resource ownership;&lt;/li&gt;
&lt;li&gt;room membership;&lt;/li&gt;
&lt;li&gt;token expiration;&lt;/li&gt;
&lt;li&gt;permitted operations.&lt;br&gt;
This establishes a clear trust boundary between the browser and the realtime gateway.&lt;br&gt;
The client requests access, but the server remains responsible for deciding whether that access is legitimate.&lt;br&gt;
Distributed Presence with Redis&lt;br&gt;
In-memory presence works only while a single application instance is running. It becomes unreliable when multiple instances or serverless functions are involved.&lt;br&gt;
We therefore introduced Redis-backed presence coordination.&lt;br&gt;
Redis is used to track:&lt;/li&gt;
&lt;li&gt;active participants;&lt;/li&gt;
&lt;li&gt;room membership;&lt;/li&gt;
&lt;li&gt;temporary presence state;&lt;/li&gt;
&lt;li&gt;expiration of abandoned sessions;&lt;/li&gt;
&lt;li&gt;coordination between separate gateway instances.&lt;br&gt;
Presence entries have a limited lifetime. If a client disappears without completing a normal disconnection, the corresponding state can expire automatically instead of remaining indefinitely.&lt;br&gt;
Graceful Redis Failure Handling&lt;br&gt;
Redis is now considered an explicit infrastructure dependency.&lt;br&gt;
The system distinguishes between:&lt;/li&gt;
&lt;li&gt;the application process being alive;&lt;/li&gt;
&lt;li&gt;the realtime gateway being operational;&lt;/li&gt;
&lt;li&gt;Redis being reachable;&lt;/li&gt;
&lt;li&gt;the complete service being ready to accept traffic.&lt;br&gt;
Failures are handled deliberately rather than silently ignored. This is important because returning a successful health response while the coordination layer is unavailable would provide misleading information to deployment systems and operators.&lt;br&gt;
Distributed Rate Limiting&lt;br&gt;
Local counters cannot reliably enforce limits across multiple runtime instances.&lt;br&gt;
We implemented a distributed rate-limiting foundation backed by Redis so that requests are evaluated against shared state.&lt;br&gt;
The rate-limiting system is designed to protect:&lt;/li&gt;
&lt;li&gt;realtime token generation;&lt;/li&gt;
&lt;li&gt;connection attempts;&lt;/li&gt;
&lt;li&gt;room joins;&lt;/li&gt;
&lt;li&gt;message publication;&lt;/li&gt;
&lt;li&gt;presence updates;&lt;/li&gt;
&lt;li&gt;abuse-sensitive API routes.&lt;br&gt;
This provides a foundation for defending the metaverse against spam, accidental request loops and automated abuse.&lt;br&gt;
Future work will refine quotas by route, identity, IP risk and operation type.&lt;br&gt;
Privacy-Safe Observability&lt;br&gt;
Reliability requires visibility, but observability must not expose private user information.&lt;br&gt;
The realtime infrastructure now supports structured operational signals for:&lt;/li&gt;
&lt;li&gt;authorization failures;&lt;/li&gt;
&lt;li&gt;connection lifecycle events;&lt;/li&gt;
&lt;li&gt;Redis availability;&lt;/li&gt;
&lt;li&gt;rate-limit decisions;&lt;/li&gt;
&lt;li&gt;degraded states;&lt;/li&gt;
&lt;li&gt;health and readiness checks.&lt;br&gt;
The goal is to make infrastructure problems diagnosable without logging raw credentials, session tokens or unnecessary personal information.&lt;br&gt;
Health and Readiness Endpoints&lt;br&gt;
A process being alive does not mean it is ready to serve realtime traffic.&lt;br&gt;
We extended the health model to include Redis readiness. The service can now report a degraded state when Redis is unavailable instead of returning an unconditional success response.&lt;br&gt;
Conceptually:&lt;br&gt;
Process alive + Redis unavailable&lt;pre&gt;&lt;code&gt;       ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Service is not fully ready&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Health response reports degradation&lt;br&gt;
This distinction is essential for:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;deployment validation;&lt;/li&gt;
&lt;li&gt;load balancers;&lt;/li&gt;
&lt;li&gt;automated monitoring;&lt;/li&gt;
&lt;li&gt;incident diagnosis;&lt;/li&gt;
&lt;li&gt;rolling releases;&lt;/li&gt;
&lt;li&gt;recovery procedures.&lt;br&gt;
Shared Redis Client&lt;br&gt;
A reusable Redis client layer was introduced to avoid creating duplicated connections across services and requests.&lt;br&gt;
The shared client centralizes:&lt;/li&gt;
&lt;li&gt;configuration;&lt;/li&gt;
&lt;li&gt;connection reuse;&lt;/li&gt;
&lt;li&gt;error handling;&lt;/li&gt;
&lt;li&gt;readiness checks;&lt;/li&gt;
&lt;li&gt;shutdown behavior;&lt;/li&gt;
&lt;li&gt;instrumentation.&lt;br&gt;
Centralizing this behavior reduces connection pressure and gives every Redis-dependent feature a consistent operational model.&lt;br&gt;
Preview Infrastructure&lt;br&gt;
The new realtime changes were deployed to an isolated Vercel Preview environment.&lt;br&gt;
This allows us to validate infrastructure modifications before they affect the public production deployment.&lt;br&gt;
The preview environment provides a safe place to test:&lt;/li&gt;
&lt;li&gt;application builds;&lt;/li&gt;
&lt;li&gt;serverless deployment behavior;&lt;/li&gt;
&lt;li&gt;environment configuration;&lt;/li&gt;
&lt;li&gt;readiness responses;&lt;/li&gt;
&lt;li&gt;Redis connectivity;&lt;/li&gt;
&lt;li&gt;authentication flows;&lt;/li&gt;
&lt;li&gt;realtime regressions.&lt;br&gt;
Production remains unchanged while the current pull request is still under review.&lt;br&gt;
Testing and Continuous Integration&lt;br&gt;
The implementation is covered by automated backend tests, including readiness behavior and Redis degradation scenarios.&lt;br&gt;
The principal validation pipeline successfully completed:&lt;/li&gt;
&lt;li&gt;backend tests;&lt;/li&gt;
&lt;li&gt;frontend tests;&lt;/li&gt;
&lt;li&gt;linting;&lt;/li&gt;
&lt;li&gt;application builds;&lt;/li&gt;
&lt;li&gt;Seller Free policy verification;&lt;/li&gt;
&lt;li&gt;continuous evidence generation;&lt;/li&gt;
&lt;li&gt;preview deployment.&lt;br&gt;
This provides strong evidence that the new infrastructure integrates correctly with the existing application.&lt;br&gt;
However, passing tests alone do not make the service production-ready. Runtime drills, dependency remediation and real multi-instance load testing are still required.&lt;br&gt;
Current Development Status&lt;br&gt;
The realtime readiness work is currently available in a draft pull request:&lt;br&gt;
MyZubster realtime readiness PR #40&lt;br&gt;
The implementation includes four focused commits:&lt;br&gt;
feat(realtime): expose Redis readiness&lt;br&gt;
test(realtime): cover Redis readiness&lt;br&gt;
feat(realtime): gate health on Redis&lt;br&gt;
test(realtime): cover Redis health degradation&lt;br&gt;
Keeping the commits focused makes review and rollback easier.&lt;br&gt;
Current Security Blocker&lt;br&gt;
The security audit currently reports vulnerabilities in the transitive ip-address dependency for versions up to 10.7.3.&lt;br&gt;
The available patched version is 10.7.4.&lt;br&gt;
Before merging the realtime work, we need to:&lt;/li&gt;
&lt;li&gt;identify which direct dependency introduces ip-address;&lt;/li&gt;
&lt;li&gt;update the dependency chain safely;&lt;/li&gt;
&lt;li&gt;regenerate the lockfile;&lt;/li&gt;
&lt;li&gt;run the complete test suite;&lt;/li&gt;
&lt;li&gt;repeat the security audit;&lt;/li&gt;
&lt;li&gt;confirm that no high or critical vulnerability remains.&lt;br&gt;
The pull request should remain in draft until this has been addressed and the runtime verification has been completed.&lt;br&gt;
What Still Needs to Be Done&lt;/li&gt;
&lt;li&gt;Dependency remediation&lt;br&gt;
Upgrade the affected dependency chain and obtain a clean security audit.&lt;/li&gt;
&lt;li&gt;Direct runtime verification&lt;br&gt;
Verify the deployed preview response from:&lt;br&gt;
/api/realtime/health&lt;br&gt;
The response should explicitly show whether Redis is ready and whether the service is operating normally or in a degraded state.&lt;/li&gt;
&lt;li&gt;Redis failure drill&lt;br&gt;
Temporarily simulate Redis unavailability in an isolated environment and verify that:&lt;/li&gt;
&lt;li&gt;readiness fails correctly;&lt;/li&gt;
&lt;li&gt;the service does not claim to be healthy;&lt;/li&gt;
&lt;li&gt;errors remain controlled;&lt;/li&gt;
&lt;li&gt;recovery occurs after Redis reconnects.&lt;/li&gt;
&lt;li&gt;Multi-instance load testing&lt;br&gt;
Run concurrent tests against multiple gateway instances and verify:&lt;/li&gt;
&lt;li&gt;shared rate limits;&lt;/li&gt;
&lt;li&gt;consistent presence;&lt;/li&gt;
&lt;li&gt;room synchronization;&lt;/li&gt;
&lt;li&gt;connection cleanup;&lt;/li&gt;
&lt;li&gt;acceptable latency;&lt;/li&gt;
&lt;li&gt;stable Redis connection usage.&lt;/li&gt;
&lt;li&gt;Restart and recovery testing&lt;br&gt;
Restart application and Redis instances while clients are connected.&lt;br&gt;
We need evidence that the platform can recover without leaving permanent ghost sessions or inconsistent presence records.&lt;/li&gt;
&lt;li&gt;End-to-end acceptance testing&lt;br&gt;
The complete flow must be tested:&lt;br&gt;
Login&lt;br&gt;
→ request realtime token&lt;br&gt;
→ authorize channel&lt;br&gt;
→ connect&lt;br&gt;
→ join room&lt;br&gt;
→ publish event&lt;br&gt;
→ update presence&lt;br&gt;
→ disconnect&lt;br&gt;
→ clean up state&lt;br&gt;
Unauthorized variations must also be tested.&lt;/li&gt;
&lt;li&gt;Production rollout strategy&lt;br&gt;
The production release should be gradual:&lt;/li&gt;
&lt;li&gt;deploy to Preview;&lt;/li&gt;
&lt;li&gt;complete runtime verification;&lt;/li&gt;
&lt;li&gt;run load and failure drills;&lt;/li&gt;
&lt;li&gt;review monitoring signals;&lt;/li&gt;
&lt;li&gt;merge after approval;&lt;/li&gt;
&lt;li&gt;deploy production;&lt;/li&gt;
&lt;li&gt;observe error rates and Redis health;&lt;/li&gt;
&lt;li&gt;retain a documented rollback procedure.&lt;br&gt;
Longer-Term Metaverse Roadmap&lt;br&gt;
Once reliability and security are validated, development can move toward the experience layer.&lt;br&gt;
Planned areas include:&lt;/li&gt;
&lt;li&gt;WebXR support;&lt;/li&gt;
&lt;li&gt;immersive rooms;&lt;/li&gt;
&lt;li&gt;interactive avatars;&lt;/li&gt;
&lt;li&gt;spatial collaboration;&lt;/li&gt;
&lt;li&gt;verified professional profiles;&lt;/li&gt;
&lt;li&gt;Zorgax integration;&lt;/li&gt;
&lt;li&gt;verified knowledge inside virtual environments;&lt;/li&gt;
&lt;li&gt;community-managed spaces;&lt;/li&gt;
&lt;li&gt;portable identity;&lt;/li&gt;
&lt;li&gt;privacy-safe analytics;&lt;/li&gt;
&lt;li&gt;independent interoperable nodes.&lt;br&gt;
The long-term objective is not merely to build another virtual world.&lt;br&gt;
MyZubster aims to combine identity, knowledge, collaboration and transparent open-source development inside an interoperable environment.&lt;br&gt;
Lessons Learned&lt;br&gt;
The most important lesson from this phase is that realtime development is not simply about sending messages quickly.&lt;br&gt;
A reliable realtime platform must answer several harder questions:&lt;/li&gt;
&lt;li&gt;Who is allowed to connect?&lt;/li&gt;
&lt;li&gt;Which channels may they access?&lt;/li&gt;
&lt;li&gt;How is state shared across instances?&lt;/li&gt;
&lt;li&gt;What happens when Redis fails?&lt;/li&gt;
&lt;li&gt;How is abuse limited globally?&lt;/li&gt;
&lt;li&gt;Can operators distinguish “alive” from “ready”?&lt;/li&gt;
&lt;li&gt;Can the system recover cleanly after a restart?&lt;/li&gt;
&lt;li&gt;Can failures be investigated without exposing private data?&lt;br&gt;
These concerns are now becoming first-class parts of the MyZubster architecture.&lt;br&gt;
Conclusion&lt;br&gt;
The MyZubster Metaverse has progressed beyond an isolated prototype.&lt;br&gt;
We now have the foundation for:&lt;/li&gt;
&lt;li&gt;authenticated realtime access;&lt;/li&gt;
&lt;li&gt;server-controlled channel authorization;&lt;/li&gt;
&lt;li&gt;Redis-backed presence;&lt;/li&gt;
&lt;li&gt;distributed rate limiting;&lt;/li&gt;
&lt;li&gt;privacy-aware observability;&lt;/li&gt;
&lt;li&gt;dependency-aware health checks;&lt;/li&gt;
&lt;li&gt;isolated preview deployments;&lt;/li&gt;
&lt;li&gt;automated tests and evidence generation.&lt;br&gt;
The next milestone is to prove that this architecture remains reliable under dependency failures, restarts, concurrency and realistic traffic.&lt;br&gt;
Only after those checks—and after resolving the current security advisory—should the realtime infrastructure be promoted to production.&lt;br&gt;
MyZubster remains an open-source project, and technical review, testing and constructive contributions are welcome.&lt;br&gt;
Project Links&lt;/li&gt;
&lt;li&gt;MyZubster repository&lt;/li&gt;
&lt;li&gt;MyZubster Metaverse&lt;/li&gt;
&lt;li&gt;Realtime readiness pull request&lt;br&gt;
 Repository GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/danieldirimini-myzubster/myzubster&quot;&gt;https://github.com/danieldirimini-myzubster/myzubster&lt;/a&gt;&lt;br&gt;
Metaverso MyZubster: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/metaverse&quot;&gt;https://www.myzubster.com/metaverse&lt;/a&gt;&lt;br&gt;
Pull request Realtime Readiness #40: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/danieldirimini-myzubster/myzubster/pull/40&quot;&gt;https://github.com/danieldirimini-myzubster/myzubster/pull/40&lt;/a&gt;&lt;br&gt;
Roadmap Linear MYZ-83: &lt;a rel=&quot;nofollow&quot; href=&quot;https://linear.app/myzubster/issue/MYZ-83/b6-realtime-observability-reliability-and-load-tests&quot;&gt;https://linear.app/myzubster/issue/MYZ-83/b6-realtime-observability-reliability-and-load-tests&lt;/a&gt;&lt;br&gt;
Organizzazione GitHub MyZubster Ecosystem: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem&quot;&gt;https://github.com/MyZubster-Ecosystem&lt;/a&gt;&lt;br&gt;
Profilo GitHub MyZubster: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/myzubster&quot;&gt;https://github.com/myzubster&lt;/a&gt;&lt;br&gt;
Sito principale: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com&quot;&gt;https://www.myzubster.com&lt;/a&gt;&lt;br&gt;
Knowledge Explorer: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/knowledge&quot;&gt;https://www.myzubster.com/knowledge&lt;/a&gt;&lt;br&gt;
Profilo Zorgax: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/zorgax-profile-onboarding&quot;&gt;https://www.myzubster.com/zorgax-profile-onboarding&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32147/building-the-myzubster-metaverse-from-prototype-to-secure-realtime-infrastructure</guid>
<pubDate>Fri, 23 Oct 2026 05:22:14 +0000</pubDate>
</item>
<item>
<title>How to Decide If a Node.js App Belongs Behind SafeLine</title>
<link>https://coderlegion.com/32135/how-to-decide-if-a-node-js-app-belongs-behind-safeline</link>
<description>&lt;p&gt;A Node.js app is not protected by installing a library that these SafeLine documents never mention. The supplied pages describe a self-hosted web application firewall that filters and monitors HTTP traffic between a web application and the Internet. They do not document a Node module, an npm package, or framework middleware. The supported decision is whether the process can sit behind the documented reverse proxy. I will not invent a runtime agent to fill that gap.&lt;/p&gt;
&lt;p&gt;That boundary is the whole plan. Validation, sessions, and secrets stay in the application. SafeLine sits on the network path in front of it. Clients reach the WAF first. Only traffic it accepts should be forwarded to the Node process that still serves the original app.&lt;/p&gt;
&lt;h3 id=&quot;what-the-pages-actually-specify&quot;&gt;What the pages actually specify&lt;/h3&gt;
&lt;p&gt;The introduction says deploying a WAF in front of a web application places a shield between that application and the Internet. A forward proxy hides a client. SafeLine is the other shape: a reverse proxy that reduces direct exposure of the server, because clients pass through it before they reach the origin. It filters, monitors, and blocks malicious HTTP and HTTPS traffic, and it is also described as preventing unauthorized data from leaving the app by applying policies that separate malicious traffic from safe traffic. Listed core capabilities are defenses for web attacks, proactive bot abuse defense, HTML and JS code encryption, IP-based rate limiting, and a web access control list. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The application guide is the operational half of that statement. SafeLine is developed based on nginx. It receives traffic for the original website, cleans malicious attack traffic, and forwards safe and reliable traffic to the original application. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Nothing on those pages ties the origin to one language. A Node HTTP server qualifies only as an upstream address. Express, Fastify, or the built-in http module do not receive a special connector in this material. The checklist is how I decide the proxy is appropriate, then how I fill only the fields the console documents.&lt;/p&gt;
&lt;h3 id=&quot;five-checks-before-dns-changes&quot;&gt;Five checks before DNS changes&lt;/h3&gt;
&lt;p&gt;Host fit is first. The deploy guide requires Linux, x86_64 or arm64, Docker 20.10.14 or above, and Docker Compose 2.0.0 or above. The minimum environment is 1 core CPU, 1 GB memory, and 5 GB disk. The CPU must support ssse3. The same page lists uname -m, docker version, docker compose version, free -h, df -h, and an lscpu check for ssse3 so the baseline can be confirmed before install. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Edition fit is second, and it matters on small Node hosts. SafeLine installed on ARM requires a Pro license to work properly. The Personal Edition is not supported on ARM. An arm64 image is therefore a license decision, not a footnote after the container starts. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Upstream design is third. Log into the web admin console, open Applications, and choose Add Application. Domain may be a domain name, a hostname, or an IP address. Port is what SafeLine listens on, such as 80 or 443. HTTPS applications need the SSL option checked. Upstream is the real address of the original app, and that is where cleaned traffic is forwarded. After the entry is saved, resolve the entered name to the IP of the server running SafeLine. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I do not publish the Node listen port as the client entry. If the upstream name resolves back to SafeLine, the forward path loops. The docs do not ship a Node bind tutorial, so the rule stays the documented one: upstream must be the real application address, reachable from the WAF.&lt;/p&gt;
&lt;p&gt;Proof is fourth. The application guide gives sample cases for SQL injection, XSS, path traversal, code injection, and XXE. Replace the example host with the protected domain. A blocking page means that case was defended. The web console attack list, then the detail view, shows whether the event was recorded. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Release track is fifth. The LTS version will no longer be maintained starting from 9.1.0-LTS (14 August 2025). LTS users are strongly recommended to upgrade to the latest version. If there are concerns, the deploy guide recommends waiting one month after a new release before upgrading. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;controls-that-match-a-public-node-service&quot;&gt;Controls that match a public Node service&lt;/h3&gt;
&lt;p&gt;Attack blocking is the wide net. The feature list names SQL injection, XSS, code injection, OS command injection, CRLF injection, XXE, SSRF, and path traversal. The introduction also names LDAP injection, XPath injection, RCE, backdoor, bruteforce, HTTP flood, and bot abuse. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Rate limiting is documented against DoS attacks, bruteforce attempts, traffic surges, and other abuse, by throttling traffic that exceeds defined limits. IP-based rate limiting is part of the core list. A captcha challenge is documented so human users can continue while crawlers and bots are blocked. When the authentication challenge is on, visitors must enter the password or they are blocked. When dynamic protection is on, HTML and JS from the web server are dynamically encrypted on each visit. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I do not apply every control as if every Node app were a browser site. Dynamic HTML and JS encryption matches a page that returns markup. A JSON API should not be described as if that encryption were its main defense. For an API, the documented controls I plan around are attack filtering, rate limits, the bot challenge, and the access control list. The authentication challenge is a visitor password gate. It is not application login, and these pages do not say it replaces tokens or sessions inside Node.&lt;/p&gt;
&lt;p&gt;The home page also publishes an effect evaluation with 33669 samples in each column. SafeLine Balance is listed at 71.65 percent detection, 0.07 percent false positive, and 99.45 percent accuracy. SafeLine Strict is listed at 76.17 percent detection, 0.22 percent false positive, and 99.38 percent accuracy. The same table includes ModSecurity Level 1 and CloudFlare Free. I treat those cells as published comparison figures, not as a measurement I ran against a Node route. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;scale-and-commercial-notes-kept-apart-from-setup&quot;&gt;Scale and commercial notes, kept apart from setup&lt;/h3&gt;
&lt;p&gt;The introduction says SafeLine is production-ready. It reports over 180,000 installations worldwide, protection for over 1,000,000 websites, and more than 30,000,000,000 HTTP requests handled daily. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As of September 30, 2026, SafeLine surpassed 700,000 installations worldwide. SafeLine Pro annual promotional pricing is $650-$880 versus the $1,000 regular price. I keep those planning figures next to the older published installation count, and I do not let either number skip the proxy checks.&lt;/p&gt;
&lt;p&gt;Install steps stay inside the deploy guide. Automatic deploy runs the official manager script, then uses the web UI. Manual deploy creates a directory with at least 5 GB free, downloads compose.yaml, writes the .env file, generates the documented secrets, restricts permissions, and starts the stack with docker compose up -d. The initial admin account is printed by docker exec safeline-mgt resetadmin. The console is opened at &lt;a rel=&quot;nofollow&quot; href=&quot;https://&amp;lt;safeline-ip&amp;gt;:9443/&quot;&gt;https://&amp;lt;safeline-ip&amp;gt;:9443/&lt;/a&gt;. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I do not extend that into an unsupported Node story. These pages do not document WebSocket handling, gRPC mode, sticky sessions, or a process manager integration. If the app is ordinary HTTP or HTTPS, add the application, set domain, port, SSL, and upstream, and point DNS at SafeLine. If the traffic is not that, leave the claim unsupported instead of inventing a connector.&lt;/p&gt;
&lt;h4 id=&quot;does-safeline-document-a-node-js-plugin-&quot;&gt;Does SafeLine document a Node.js plugin?&lt;/h4&gt;
&lt;p&gt;No. The supplied home, deploy, and application pages describe a reverse proxy and console fields for domain, port, SSL, and upstream. They do not document an npm package or a Node agent. Protect the app by placing that proxy in front of the process.&lt;/p&gt;
&lt;h4 id=&quot;which-address-should-clients-use-&quot;&gt;Which address should clients use?&lt;/h4&gt;
&lt;p&gt;Clients should use the domain that resolves to the SafeLine server and the listen port on the application entry, such as 80 or 443. The Node address belongs in upstream. Leaving the Node port open on the public interface bypasses the shield described in the introduction.&lt;/p&gt;
&lt;h4 id=&quot;how-can-i-verify-a-block-without-guessing-&quot;&gt;How can I verify a block without guessing?&lt;/h4&gt;
&lt;p&gt;Use the documented SQL injection, XSS, path traversal, code injection, and XXE cases against your own domain. A blocking page is the expected sign that the case was stopped. Then open the console attack list and check detail for that event. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;when-does-architecture-force-a-pro-license-&quot;&gt;When does architecture force a Pro license?&lt;/h4&gt;
&lt;p&gt;When the install is ARM. The deploy guide says ARM requires a Pro license to work properly, and that the Personal Edition is not supported on ARM. Confirm uname -m before you treat a cheap arm64 host as ready. &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;what-should-stay-inside-the-node-process-&quot;&gt;What should stay inside the Node process?&lt;/h4&gt;
&lt;p&gt;Route validation, account authentication, and secret handling. SafeLine is documented to clean traffic and forward the safe portion to the original application. It does not take over application logic. Keep those checks even after the sample attacks show a blocking page.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;SafeLine has surpassed 700,000 installations worldwide as of September 30, 2026. SafeLine Pro annual promotional pricing is $650-$880, compared with the regular $1,000 per year.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SafeLine website: &lt;a rel=&quot;nofollow&quot; href=&quot;https://ly.cyberserval.tech/MqhYdmI&quot;&gt;https://ly.cyberserval.tech/MqhYdmI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://demo.waf.chaitin.com:9443/statistics&quot;&gt;https://demo.waf.chaitin.com:9443/statistics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Official docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chaitin/SafeLine&quot;&gt;https://github.com/chaitin/SafeLine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Pro offer: &lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;https://cyberserval.tech/landing/lto&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32135/how-to-decide-if-a-node-js-app-belongs-behind-safeline</guid>
<pubDate>Fri, 23 Oct 2026 03:05:28 +0000</pubDate>
</item>
<item>
<title>How to Protect a Node.js App with a SafeLine Fit Checklist</title>
<link>https://coderlegion.com/32134/how-to-protect-a-node-js-app-with-a-safeline-fit-checklist</link>
<description>&lt;p&gt;Protecting a Node.js app with SafeLine starts as a fit decision, not as a search for an in-process library. The product documents describe SafeLine as a self-hosted web application firewall that filters and monitors HTTP traffic between a web application and the Internet. Clients pass through it before they reach the server. It is a reverse proxy, not a module loaded by your runtime (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). The application guide says the same thing in operational language: SafeLine receives traffic for the original website, cleans malicious attack traffic, and forwards the safe remainder to the original application (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). Those pages do not document a Node.js SDK, Express middleware, Fastify plugin, or runtime agent. If that is what the keyword suggested, do not invent the integration. Keep Node as the upstream and decide whether the documented front door fits.&lt;/p&gt;
&lt;p&gt;The console model is small enough to checklist. You add an application with a domain, hostname, or IP, a listen port such as 80 or 443, the SSL option for an HTTPS app, and an upstream address. Upstream is the real address of the original app. For this keyword, that is the host and port the Node process already serves. After the application is saved, you resolve the domain to the SafeLine server so visitors reach the proxy first (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). A Node port that is still open on the public internet is a bypass. The checklist is not done until that bypass is closed.&lt;/p&gt;
&lt;p&gt;Confirm the protocol before you argue about frameworks. SafeLine is documented for HTTP and HTTPS web traffic. A Node process that only speaks some other protocol is outside this guide. Do not treat a reverse-proxy WAF as a generic socket filter just because the service happens to be written in JavaScript.&lt;/p&gt;
&lt;p&gt;Map risk to the published attack list. The home page names SQL injection, XSS, code injection, OS command injection, CRLF injection, LDAP injection, XPath injection, RCE, XXE, SSRF, path traversal, backdoor, brute force, HTTP flood, and bot abused traffic (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). Those classes matter when a Node route builds SQL, reflects input into HTML, shells out, parses XML, fetches a user-supplied URL, or reads a file path from a request. They do not retire bugs that never become a policy decision at the proxy, such as broken authorization inside a route or a secret committed to the repo. Say that limit out loud before the DNS cutover.&lt;/p&gt;
&lt;p&gt;Then pick only controls the docs name. Core capabilities are defenses for web attacks, proactive bot abused defense, HTML and JS code encryption, IP-based rate limiting, and a web access control list (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). Rate limiting is described as throttling traffic that exceeds defined limits, aimed at DoS, brute force, traffic surges, and other abuse. CAPTCHA is described as allowing human users while blocking crawlers and bots. The authentication challenge blocks visitors who do not enter the password. Dynamic protection encrypts HTML and JS on each visit (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). A server-rendered Node site can use that HTML and JS control as documented. A JSON API should not be described as encrypted by it. For an API, the documented primary controls are attack blocking, rate limiting, and the access control list, with CAPTCHA or the password challenge only if you accept the visitor friction.&lt;/p&gt;
&lt;p&gt;Check the host against the deploy guide, not against your Node version. Requirements are Linux, x86_64 or arm64, Docker 20.10.14 or above, Docker Compose 2.0.0 or above, at least 1 CPU core, 1 GB of memory, and 5 GB of disk, plus a CPU with ssse3 (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;). SafeLine installed on ARM requires a Pro license to work properly. The Personal Edition is not supported on ARM. The LTS version is no longer maintained starting from 9.1.0-LTS on 14 August 2025, and the guide strongly recommends the latest version. None of that is an npm constraint. Meeting it does not mean SafeLine inspects your package manifest.&lt;/p&gt;
&lt;p&gt;Prove blocking with the published cases, then read the console. The application guide lists SQL injection, XSS, path traversal, code injection, and XXE samples and tells you to replace the example domain with yours. A blocking page means the defense fired. The attack list and its detail view are how you confirm what was stopped (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). Replay legitimate Node routes in the same pass. Encoded queries, file uploads, and webhook bodies can look hostile without being attacks against your app.&lt;/p&gt;
&lt;p&gt;Use the published evaluation as a comparison, not as your error budget. On 33,669 samples, the home page lists SafeLine Balance at 71.65% detection, 0.07% false positives, and 99.45% accuracy, and SafeLine Strict at 76.17% detection, 0.22% false positives, and 99.38% accuracy. The same table lists ModSecurity Level 1 at 69.74% detection, 17.58% false positives, and 82.20% accuracy, and CloudFlare Free at 10.70% detection, 0.07% false positives, and 98.40% accuracy (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). Stage your own traffic anyway. Those rows do not mention your routes.&lt;/p&gt;
&lt;p&gt;As of September 30, 2026, SafeLine surpassed 700,000 installations worldwide. SafeLine Pro annual promotional pricing is $650-$880 versus the $1,000 regular price. Treat that range as a licensing input when you need Pro, including the ARM case in the deploy guide. Do not treat it as proof that every Node.js app needs that edition. The home page also calls SafeLine production-ready, protecting over 1,000,000 websites and handling over 30,000,000,000 HTTP requests daily (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). Scale still does not replace an upstream health check. If the proxy cannot be reached, Node never sees the client. If Node is down, the proxy cannot write your application response for you.&lt;/p&gt;
&lt;p&gt;The console is &lt;a rel=&quot;nofollow&quot; href=&quot;https://&amp;lt;safeline-ip&amp;gt;:9443/&quot;&gt;https://&amp;lt;safeline-ip&amp;gt;:9443/&lt;/a&gt; unless the documented management port is changed, and the initial administrator account comes from docker exec safeline-mgt resetadmin (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;). If you enable CAPTCHA or the password challenge, test them in a browser, because both change what happens before Node receives the request. The fit decision is complete only when clients use SafeLine, the Node upstream is private to that proxy path, attack probes show a blocking page and a console detail, and normal traffic still arrives.&lt;/p&gt;
&lt;h4 id=&quot;does-safeline-provide-a-node-js-integration-package-&quot;&gt;Does SafeLine provide a Node.js integration package?&lt;/h4&gt;
&lt;p&gt;The install guide and the application guide do not document an SDK, middleware, or runtime agent. The supported pattern is to add the Node process as the upstream and send clients to SafeLine first (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;).&lt;/p&gt;
&lt;h4 id=&quot;which-fields-define-the-node-upstream-&quot;&gt;Which fields define the Node upstream?&lt;/h4&gt;
&lt;p&gt;The documented fields are domain, port, the SSL option for HTTPS, and upstream. Upstream is the real address of the original app, meaning the host and port your Node process already serves (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;).&lt;/p&gt;
&lt;h4 id=&quot;will-dynamic-protection-encrypt-json-api-responses-&quot;&gt;Will dynamic protection encrypt JSON API responses?&lt;/h4&gt;
&lt;p&gt;Dynamic protection is documented as encrypting HTML and JS on each visit (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). Judge a JSON API on attack blocking, IP-based rate limiting, the web access control list, and, if you accept the extra step, CAPTCHA or the authentication challenge.&lt;/p&gt;
&lt;h4 id=&quot;how-do-i-confirm-a-probe-was-stopped-by-safeline-&quot;&gt;How do I confirm a probe was stopped by SafeLine?&lt;/h4&gt;
&lt;p&gt;Run the published SQL injection, XSS, path traversal, code injection, and XXE cases against your domain. A blocking page plus an attack-list detail in the console is the documented confirmation (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;).&lt;/p&gt;
&lt;h4 id=&quot;when-does-pro-pricing-matter-for-this-setup-&quot;&gt;When does Pro pricing matter for this setup?&lt;/h4&gt;
&lt;p&gt;ARM installs require a Pro license, and the Personal Edition is not supported on ARM (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;). As of September 30, 2026, SafeLine Pro annual promotional pricing is $650-$880 versus the $1,000 regular price. That is a license input, not a claim that every x86_64 Node deployment must be Pro.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;SafeLine has surpassed 700,000 installations worldwide as of September 30, 2026. SafeLine Pro annual promotional pricing is $650-$880, compared with the regular $1,000 per year.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SafeLine website: &lt;a rel=&quot;nofollow&quot; href=&quot;https://ly.cyberserval.tech/MqhYdmI&quot;&gt;https://ly.cyberserval.tech/MqhYdmI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://demo.waf.chaitin.com:9443/statistics&quot;&gt;https://demo.waf.chaitin.com:9443/statistics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Official docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chaitin/SafeLine&quot;&gt;https://github.com/chaitin/SafeLine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Pro offer: &lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;https://cyberserval.tech/landing/lto&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32134/how-to-protect-a-node-js-app-with-a-safeline-fit-checklist</guid>
<pubDate>Fri, 23 Oct 2026 03:04:56 +0000</pubDate>
</item>
<item>
<title># Node.js Brotli RCE (CVE-2026-111497): What Happened and How to Patch</title>
<link>https://coderlegion.com/32089/node-js-brotli-rce-cve-2026-111497-what-happened-and-how-to-patch</link>
<description>&lt;h2 id=&quot;node-js-brotli-rce-cve-2026-111497-what-happened-and-how-to-patch&quot;&gt;Node.js Brotli RCE (CVE-2026-111497): What Happened and How to Patch&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Node.js · Security · npm&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;On &lt;strong&gt;October 21, 2026&lt;/strong&gt; the Node.js team released security fixes for the 22.x, 24.x and 26.x lines, covering &lt;strong&gt;10 CVEs&lt;/strong&gt;. The most serious, &lt;strong&gt;CVE-2026-111497&lt;/strong&gt;, is an out-of-bounds write in the Brotli decoder that the advisory says &lt;strong&gt;permits remote code execution&lt;/strong&gt;. The advisory lists &lt;strong&gt;no workaround&lt;/strong&gt;, so the only fix is to update Node.&lt;/p&gt;
&lt;p&gt;This post covers what was fixed, how to check if you're exposed, and how to patch your projects and keep an unpatched Node from coming back.&lt;/p&gt;
&lt;p&gt;Read more on &lt;a rel=&quot;nofollow&quot; href=&quot;https://rusu-ionut.ro/blog/node-js-brotli-rce-cve-2026-111497-what-happened-and-how-to-patch&quot;&gt;https://rusu-ionut.ro/blog/node-js-brotli-rce-cve-2026-111497-what-happened-and-how-to-patch&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;tl-dr&quot;&gt;TL;DR&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Update to &lt;strong&gt;Node 22.23.4&lt;/strong&gt;, &lt;strong&gt;24.22.1&lt;/strong&gt; or &lt;strong&gt;26.12.1&lt;/strong&gt; (or newer).&lt;/li&gt;
&lt;li&gt;There is no workaround for the Brotli bug.&lt;/li&gt;
&lt;li&gt;The advisory doesn't mention Node 20, but it says end-of-life versions are always affected. If you're on one, plan a move to a supported line.&lt;/li&gt;
&lt;li&gt;Add an &lt;code&gt;engines&lt;/code&gt; range to &lt;code&gt;package.json&lt;/code&gt; so an unpatched Node gets flagged.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-was-fixed&quot;&gt;What was fixed&lt;/h3&gt;
&lt;p&gt;Four High, three Medium and three Low severity issues:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;CVE &lt;/th&gt;&lt;th&gt;Severity &lt;/th&gt;&lt;th&gt;What it is &lt;/th&gt;&lt;th&gt;Lines&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;CVE-2026-111497 &lt;/td&gt;&lt;td&gt;High &lt;/td&gt;&lt;td&gt;Brotli decoder out-of-bounds write permits remote code execution &lt;/td&gt;&lt;td&gt;22, 24, 26&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CVE-2026-105482 &lt;/td&gt;&lt;td&gt;High &lt;/td&gt;&lt;td&gt;&lt;code&gt;dns.resolve&lt;/code&gt; truncates names containing a NUL character &lt;/td&gt;&lt;td&gt;22, 24, 26&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CVE-2026-105483 &lt;/td&gt;&lt;td&gt;High &lt;/td&gt;&lt;td&gt;TLS hostname bypass when an IP is written with fullwidth digits &lt;/td&gt;&lt;td&gt;24, 26&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CVE-2026-106069 &lt;/td&gt;&lt;td&gt;High &lt;/td&gt;&lt;td&gt;TLS hostname bypass when a host loosely resolves to an IP &lt;/td&gt;&lt;td&gt;22, 24, 26&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CVE-2026-105481 &lt;/td&gt;&lt;td&gt;Medium &lt;/td&gt;&lt;td&gt;Keep-alive socket processes a response written before the request was sent &lt;/td&gt;&lt;td&gt;22, 24, 26&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CVE-2026-106072 &lt;/td&gt;&lt;td&gt;Medium &lt;/td&gt;&lt;td&gt;Non-canonical IPv4 hostnames let DNS SANs authenticate IP endpoints &lt;/td&gt;&lt;td&gt;22, 24, 26&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CVE-2026-106074 &lt;/td&gt;&lt;td&gt;Medium &lt;/td&gt;&lt;td&gt;&lt;code&gt;crypto.randomUUID()&lt;/code&gt; / &lt;code&gt;randomInt()&lt;/code&gt; entropy caches saved into startup snapshots repeat the same values &lt;/td&gt;&lt;td&gt;22, 24, 26&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CVE-2026-106070 &lt;/td&gt;&lt;td&gt;Low &lt;/td&gt;&lt;td&gt;&lt;code&gt;ZipFile&lt;/code&gt; returns attacker-controlled prefix bytes after failed header validation &lt;/td&gt;&lt;td&gt;26&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CVE-2026-106071 &lt;/td&gt;&lt;td&gt;Low &lt;/td&gt;&lt;td&gt;Permission Model filesystem over-grant via wildcard radix lookup &lt;/td&gt;&lt;td&gt;26&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CVE-2026-106073 &lt;/td&gt;&lt;td&gt;Low &lt;/td&gt;&lt;td&gt;Permission Model bypass with &lt;code&gt;fs.realpathSync&lt;/code&gt; &lt;/td&gt;&lt;td&gt;22, 24, 26&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The same releases also update bundled dependencies (c-ares, OpenSSL, Undici and others).&lt;/p&gt;
&lt;h3 id=&quot;why-the-brotli-bug-is-the-one-to-care-about&quot;&gt;Why the Brotli bug is the one to care about&lt;/h3&gt;
&lt;p&gt;Brotli is a compression format. Node decodes it in several places you may not think about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fetch()&lt;/code&gt; decodes responses sent with &lt;code&gt;Content-Encoding: br&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Express 5's body parser (&lt;code&gt;body-parser&lt;/code&gt; 2.x) decodes Brotli &lt;strong&gt;request bodies&lt;/strong&gt; by default.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;axios&lt;/code&gt; asks for Brotli by default.&lt;/li&gt;
&lt;li&gt;Any direct &lt;code&gt;zlib&lt;/code&gt; Brotli decompression call.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Browser JavaScript uses the browser's own decoder, and code that only &lt;em&gt;compresses&lt;/em&gt; with Brotli isn't the entry point. The trigger is described as a malformed Brotli stream. Reports on public proof-of-concepts disagree about whether it works against a standard build, and one tracker says no exploitation in the wild had been reported as of October 22. I wouldn't wait to find out: update.&lt;/p&gt;
&lt;h3 id=&quot;step-1-check-what-you-re-running&quot;&gt;Step 1: check what you're running&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;node -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You want &lt;strong&gt;22.23.4+&lt;/strong&gt;, &lt;strong&gt;24.22.1+&lt;/strong&gt; or &lt;strong&gt;26.12.1+&lt;/strong&gt;. There's also a direct check. The fix adds a decoder error constant, so this prints &lt;code&gt;true&lt;/code&gt; only on a patched Node (tested on 24.21.0 → &lt;code&gt;false&lt;/code&gt; and 24.22.1 → &lt;code&gt;true&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;node -p &quot;'BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH' in require('zlib').constants&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check every place Node runs: your laptop, CI, Docker base images, and your hosting runtime.&lt;/p&gt;
&lt;h3 id=&quot;step-2-update&quot;&gt;Step 2: update&lt;/h3&gt;
&lt;p&gt;With nvm:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nvm install 24          # installs the newest 24.x (24.22.1 or later)
nvm alias default 24
nvm use 24
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two traps to watch for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;An already-open terminal keeps the old Node&lt;/strong&gt; until you run &lt;code&gt;nvm use 24&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker and CI&lt;/strong&gt; need a rebuild on a patched base image (&lt;code&gt;docker build --pull --no-cache .&lt;/code&gt;). &lt;strong&gt;Managed runtimes&lt;/strong&gt; (Firebase, Vercel and similar) choose the patch version for you, so check your provider rather than assuming.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;step-3-stop-an-unpatched-node-from-sneaking-back-in&quot;&gt;Step 3: stop an unpatched Node from sneaking back in&lt;/h3&gt;
&lt;p&gt;Updating your machine fixes your machine. To protect the project, declare which Node versions it supports with an &lt;code&gt;engines&lt;/code&gt; field in every &lt;code&gt;package.json&lt;/code&gt; (root, server, workspaces):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;engines&quot;: {
    &quot;node&quot;: &quot;&amp;gt;=22.23.4 &amp;lt;23.0.0 || &amp;gt;=24.22.1 &amp;lt;25.0.0&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;npm also mirrors it into the lockfile's root entry (&lt;code&gt;packages[&quot;&quot;].engines&lt;/code&gt;). Here's what that range does:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Node version &lt;/th&gt;&lt;th&gt;Result&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;22.23.3 &lt;/td&gt;&lt;td&gt;flagged&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;22.23.4 &lt;/td&gt;&lt;td&gt;allowed&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;24.21.0 &lt;/td&gt;&lt;td&gt;flagged&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;24.22.1 &lt;/td&gt;&lt;td&gt;allowed&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;26.12.1 &lt;/td&gt;&lt;td&gt;flagged, because it isn't in this range&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you run the 26 line, add &lt;code&gt;|| &amp;gt;=26.12.1 &amp;lt;27.0.0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;By default npm only &lt;strong&gt;warns&lt;/strong&gt; (&lt;code&gt;EBADENGINE&lt;/code&gt;) and keeps installing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;npm warn EBADENGINE Unsupported engine {
npm warn EBADENGINE   required: { node: '&amp;gt;=22.23.4 &amp;lt;23.0.0 || &amp;gt;=24.22.1 &amp;lt;25.0.0' },
npm warn EBADENGINE   current: { node: 'v24.21.0', npm: '12.2.0' }
npm warn EBADENGINE }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To make it a hard failure, add this to &lt;code&gt;.npmrc&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;engine-strict=true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;step-4-make-sure-your-dependencies-accept-the-patched-node&quot;&gt;Step 4: make sure your dependencies accept the patched Node&lt;/h3&gt;
&lt;p&gt;Before tightening the range, check that no installed package refuses the patched version. Save this as &lt;code&gt;scan-engines.cjs&lt;/code&gt; in your project root (it needs &lt;code&gt;semver&lt;/code&gt;, which most projects already have in &lt;code&gt;node_modules&lt;/code&gt;; otherwise &lt;code&gt;npm i -D semver&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// node scan-engines.cjs 24.22.1
const fs = require(&quot;fs&quot;);
const path = require(&quot;path&quot;);
const semver = require(&quot;semver&quot;);

const target = process.argv[2];
const rejected = [];
let checked = 0;

function scan(dir) {
  if (!fs.existsSync(dir)) return;
  for (const name of fs.readdirSync(dir)) {
    const full = path.join(dir, name);
    if (name.startsWith(&quot;@&quot;)) { scan(full); continue; }
    const manifest = path.join(full, &quot;package.json&quot;);
    if (!fs.existsSync(manifest)) continue;
    const { name: pkg, version, engines } = JSON.parse(fs.readFileSync(manifest, &quot;utf8&quot;));
    if (!engines?.node) continue;
    checked++;
    if (!semver.satisfies(target, engines.node)) rejected.push(`${pkg}@${version} needs ${engines.node}`);
  }
}

scan(path.resolve(&quot;node_modules&quot;));
console.log(`${checked} packages declare engines.node`);
console.log(rejected.length ? rejected.join(&quot;\n&quot;) : `none reject Node ${target}`);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If it lists any packages, check whether a newer version of each supports the patched Node before you tighten the range.&lt;/p&gt;
&lt;h3 id=&quot;checklist&quot;&gt;Checklist&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;node -v&lt;/code&gt; shows a patched version on every machine, CI runner and container&lt;/li&gt;
&lt;li&gt;[ ] Docker images rebuilt with &lt;code&gt;--pull --no-cache&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] Hosting runtime confirmed with your provider&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;engines.node&lt;/code&gt; set in every &lt;code&gt;package.json&lt;/code&gt; (and mirrored in the lockfile)&lt;/li&gt;
&lt;li&gt;[ ] Optional: &lt;code&gt;engine-strict=true&lt;/code&gt; in &lt;code&gt;.npmrc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] Dependencies checked against the patched Node&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;sources&quot;&gt;Sources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nodejs.org/en/blog/vulnerability/october-2026-security-releases&quot;&gt;Node.js Security Releases: October 21, 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://hol.org/guard/security/campaigns/cve-2026-111497-nodejs-brotli-decoder-out-of-bounds-write&quot;&gt;CVE-2026-111497 write-up on HOL Guard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nodejs/release#release-schedule&quot;&gt;Node.js release schedule&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32089/node-js-brotli-rce-cve-2026-111497-what-happened-and-how-to-patch</guid>
<pubDate>Thu, 22 Oct 2026 15:32:04 +0000</pubDate>
</item>
<item>
<title>How to Scrape Threads with Node.js: Profiles, Posts, Replies, and an Engagement Tracker (2026)</title>
<link>https://coderlegion.com/31945/how-to-scrape-threads-with-node-js-profiles-posts-replies-and-an-engagement-tracker-2026</link>
<description>&lt;p&gt;Threads is an awkward target. The web app is a React client from Meta, most of the interesting data loads through internal GraphQL calls, and a logged-out visitor sees only a slice of what a logged-in one does. Still, a lot of public data ships inside the first HTML response, and for a modest job like &quot;track likes and replies on one account's recent posts&quot; that slice is enough.&lt;/p&gt;
&lt;p&gt;This tutorial builds a small threads scraper in Node.js 18+ with nothing but native &lt;code&gt;fetch&lt;/code&gt;. It reads a public profile, pulls its recent posts with like, reply, repost, and quote counts, and appends a timestamped snapshot to a JSON Lines file every time it runs. At the end there's a short report script that shows which posts gained the most engagement between runs.&lt;/p&gt;
&lt;p&gt;I'll also be upfront about where this breaks, because on Threads it breaks often.&lt;/p&gt;
&lt;h3 id=&quot;what-a-logged-out-visitor-can-reach&quot;&gt;What a logged-out visitor can reach&lt;/h3&gt;
&lt;p&gt;Short version, as of writing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Profile pages&lt;/strong&gt; (&lt;code&gt;https://www.threads.com/@handle&lt;/code&gt;): name, bio, follower count, verification, and the first batch of recent posts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single post pages&lt;/strong&gt; (&lt;code&gt;https://www.threads.com/@handle/post/&amp;lt;code&amp;gt;&lt;/code&gt;): the post itself and usually a first batch of replies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Search, feeds, follower lists, &quot;load more&quot;&lt;/strong&gt;: these either show a login prompt or depend on GraphQL calls that need session tokens. Treat them as off-limits for a no-login scraper.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Threads moved its main domain from &lt;code&gt;threads.net&lt;/code&gt; to &lt;code&gt;threads.com&lt;/code&gt; in 2025. Old &lt;code&gt;threads.net&lt;/code&gt; links redirect, so let &lt;code&gt;fetch&lt;/code&gt; follow redirects and normalize URLs to &lt;code&gt;threads.com&lt;/code&gt; in your own storage so the same post doesn't get two keys.&lt;/p&gt;
&lt;h3 id=&quot;where-the-data-actually-lives&quot;&gt;Where the data actually lives&lt;/h3&gt;
&lt;p&gt;If you open the page source of a profile, you won't find the posts as readable HTML. What you will find are many &lt;code&gt;&amp;lt;script type=&quot;application/json&quot;&amp;gt;&lt;/code&gt; tags, several marked with a &lt;code&gt;data-sjs&lt;/code&gt; attribute. These are server-side preloaded data for Meta's Relay client, and some of them contain the same post objects the app renders: &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;taken_at&lt;/code&gt;, &lt;code&gt;like_count&lt;/code&gt;, &lt;code&gt;caption.text&lt;/code&gt;, and a &lt;code&gt;text_post_app_info&lt;/code&gt; object with &lt;code&gt;direct_reply_count&lt;/code&gt;, &lt;code&gt;repost_count&lt;/code&gt;, and &lt;code&gt;quote_count&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The structure is deeply nested and changes without notice, so don't hard-code a path like &lt;code&gt;result.data.mediaData.edges[0]...&lt;/code&gt;. A sturdier approach is to parse every JSON script and walk the whole tree looking for anything that &lt;em&gt;looks&lt;/em&gt; like a post. That costs a few milliseconds and survives most reshuffles.&lt;/p&gt;
&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir threads-scraper &amp;amp;&amp;amp; cd threads-scraper
npm init -y
npm pkg set type=module
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No dependencies. Node 18 or newer gives you global &lt;code&gt;fetch&lt;/code&gt;, and everything else comes from the standard library.&lt;/p&gt;
&lt;h3 id=&quot;step-1-fetch-a-page-politely&quot;&gt;Step 1: Fetch a page politely&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// fetchPage.js
const UA =
  &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 &quot; +
  &quot;(KHTML, like Gecko) Chrome/129.0 Safari/537.36&quot;;

const sleep = (ms) =&amp;gt; new Promise((r) =&amp;gt; setTimeout(r, ms));

export async function fetchPage(url, { retries = 3 } = {}) {
  for (let attempt = 0; attempt &amp;lt;= retries; attempt++) {
    const res = await fetch(url, {
      headers: {
        &quot;user-agent&quot;: UA,
        &quot;accept&quot;: &quot;text/html,application/xhtml+xml&quot;,
        &quot;accept-language&quot;: &quot;en-US,en;q=0.9&quot;,
      },
      redirect: &quot;follow&quot;,
    });

    if (res.ok) {
      const html = await res.text();
      // A login redirect sometimes comes back as 200 with a tiny shell page.
      if (res.url.includes(&quot;/login&quot;) || html.length &amp;lt; 20_000) {
        throw new Error(`Looks like a login wall for ${url} (final URL ${res.url})`);
      }
      return html;
    }

    if (res.status === 429 || res.status &amp;gt;= 500) {
      const wait = 2 ** attempt * 5_000 + Math.random() * 2_000;
      console.warn(`HTTP ${res.status} on ${url}, retrying in ${Math.round(wait / 1000)}s`);
      await sleep(wait);
      continue;
    }
    throw new Error(`HTTP ${res.status} for ${url}`);
  }
  throw new Error(`Gave up on ${url} after ${retries + 1} attempts`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The 20,000-character threshold is a heuristic, not a documented rule. A real profile page is far larger than that, while the shell you get when Meta wants you to sign in is much smaller. Tune it if you see false positives.&lt;/p&gt;
&lt;h3 id=&quot;step-2-pull-the-embedded-json-out-of-the-html&quot;&gt;Step 2: Pull the embedded JSON out of the HTML&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// extract.js
const SCRIPT_RE = /&amp;lt;script type=&quot;application\/json&quot;[^&amp;gt;]*&amp;gt;([\s\S]*?)&amp;lt;\/script&amp;gt;/g;

export function jsonBlobs(html) {
  const blobs = [];
  for (const m of html.matchAll(SCRIPT_RE)) {
    try {
      blobs.push(JSON.parse(m[1]));
    } catch {
      // Some blobs are not valid standalone JSON; skip them.
    }
  }
  return blobs;
}

// Walk any JSON tree and collect objects that look like Threads posts.
export function findPosts(root) {
  const found = new Map();
  const stack = [root];
  while (stack.length) {
    const node = stack.pop();
    if (!node || typeof node !== &quot;object&quot;) continue;

    if (
      typeof node.code === &quot;string&quot; &amp;amp;&amp;amp;
      typeof node.taken_at === &quot;number&quot; &amp;amp;&amp;amp;
      &quot;like_count&quot; in node &amp;amp;&amp;amp;
      node.user?.username
    ) {
      found.set(node.code, node);
    }
    for (const v of Array.isArray(node) ? node : Object.values(node)) {
      if (v &amp;amp;&amp;amp; typeof v === &quot;object&quot;) stack.push(v);
    }
  }
  return [...found.values()];
}

// Same idea for the profile header.
export function findProfile(root, handle) {
  const stack = [root];
  while (stack.length) {
    const node = stack.pop();
    if (!node || typeof node !== &quot;object&quot;) continue;
    if (node.username === handle &amp;amp;&amp;amp; &quot;follower_count&quot; in node) return node;
    for (const v of Array.isArray(node) ? node : Object.values(node)) {
      if (v &amp;amp;&amp;amp; typeof v === &quot;object&quot;) stack.push(v);
    }
  }
  return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Map&lt;/code&gt; keyed by &lt;code&gt;code&lt;/code&gt; matters: the same post often appears in more than one blob, and you don't want to count it twice. Using an explicit stack instead of recursion avoids blowing the call stack on Meta's very deep payloads.&lt;/p&gt;
&lt;h3 id=&quot;step-3-normalize-a-post&quot;&gt;Step 3: Normalize a post&lt;/h3&gt;
&lt;p&gt;Raw post objects carry dozens of fields you don't need. Flatten them into something you'd actually store:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// normalize.js
export function normalizePost(p) {
  const info = p.text_post_app_info ?? {};
  return {
    code: p.code,
    url: `https://www.threads.com/@${p.user.username}/post/${p.code}`,
    author: p.user.username,
    postedAt: new Date(p.taken_at * 1000).toISOString(),
    text: p.caption?.text ?? &quot;&quot;,
    likes: p.like_count ?? null,
    replies: info.direct_reply_count ?? null,
    reposts: info.repost_count ?? null,
    quotes: info.quote_count ?? null,
    isReply: Boolean(info.is_reply),
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two caveats. First, authors can hide like counts, so &lt;code&gt;like_count&lt;/code&gt; may be missing or zero even on a popular post; store &lt;code&gt;null&lt;/code&gt; rather than guessing. Second, the profile page mixes in the author's own continuation posts (a multi-part thread shows up as a post plus replies to itself). &lt;code&gt;isReply&lt;/code&gt; lets you filter those out if you only want top-level posts.&lt;/p&gt;
&lt;h3 id=&quot;step-4-the-threads-scraper-core-a-profile-&quot;&gt;Step 4: The threads scraper core (a profile)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// profile.js
import { fetchPage } from &quot;./fetchPage.js&quot;;
import { jsonBlobs, findPosts, findProfile } from &quot;./extract.js&quot;;
import { normalizePost } from &quot;./normalize.js&quot;;

export async function scrapeProfile(handle) {
  handle = handle.replace(/^@/, &quot;&quot;);
  const html = await fetchPage(`https://www.threads.com/@${handle}`);
  const blobs = jsonBlobs(html);

  let profile = null;
  const posts = new Map();
  for (const blob of blobs) {
    profile ??= findProfile(blob, handle);
    for (const p of findPosts(blob)) {
      if (p.user.username === handle) posts.set(p.code, normalizePost(p));
    }
  }

  return {
    handle,
    followers: profile?.follower_count ?? null,
    verified: profile?.is_verified ?? null,
    bio: profile?.biography ?? null,
    posts: [...posts.values()].sort((a, b) =&amp;gt; b.postedAt.localeCompare(a.postedAt)),
  };
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const data = await scrapeProfile(process.argv[2] ?? &quot;zuck&quot;);
  console.log(data.followers, data.posts.length);
  console.table(data.posts.slice(0, 5).map(({ code, likes, replies, reposts }) =&amp;gt; ({ code, likes, replies, reposts })));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it with &lt;code&gt;node profile.js someuser&lt;/code&gt;. Expect a first page of posts, roughly what you'd see before scrolling. If you get zero posts but a valid profile, Meta has probably changed where the posts are preloaded, or this request was served a stripped page. Save the HTML to disk and search it for a post code you know exists; that tells you quickly which case you're in.&lt;/p&gt;
&lt;h3 id=&quot;step-5-replies-on-a-single-post&quot;&gt;Step 5: Replies on a single post&lt;/h3&gt;
&lt;p&gt;The post permalink page works the same way. The main post plus the first batch of replies are usually in the preloaded JSON:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// post.js
import { fetchPage } from &quot;./fetchPage.js&quot;;
import { jsonBlobs, findPosts } from &quot;./extract.js&quot;;
import { normalizePost } from &quot;./normalize.js&quot;;

export async function scrapePost(url) {
  const clean = url.replace(&quot;threads.net&quot;, &quot;threads.com&quot;).split(/[?#]/)[0];
  const code = clean.match(/\/post\/([^/]+)/)?.[1];
  if (!code) throw new Error(`Not a Threads post URL: ${url}`);

  const all = new Map();
  for (const blob of jsonBlobs(await fetchPage(clean))) {
    for (const p of findPosts(blob)) all.set(p.code, normalizePost(p));
  }
  const post = all.get(code) ?? null;
  all.delete(code);
  return { post, replies: [...all.values()] };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&quot;Replies&quot; here means everything else on the page that looks like a post. On some pages that includes the parent post (if you opened a reply) or related posts. If you need strict parent/child relationships, inspect the surrounding &lt;code&gt;thread_items&lt;/code&gt; structure in the blob rather than relying on the flat list.&lt;/p&gt;
&lt;h3 id=&quot;why-not-just-call-the-graphql-api-&quot;&gt;Why not just call the GraphQL API?&lt;/h3&gt;
&lt;p&gt;You'll see older tutorials that POST to &lt;code&gt;https://www.threads.com/api/graphql&lt;/code&gt; with a &lt;code&gt;doc_id&lt;/code&gt; and an &lt;code&gt;lsd&lt;/code&gt; token copied from the page. It can work for a while, but plan for three problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;doc_id&lt;/code&gt; churn.&lt;/strong&gt; Each query is identified by a numeric document ID that changes when Meta ships a new client build. Hard-coded IDs go stale, sometimes within weeks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token coupling.&lt;/strong&gt; The &lt;code&gt;lsd&lt;/code&gt; token and related headers are tied to the page you loaded. You end up scraping the HTML anyway just to call GraphQL.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pagination is where login walls live.&lt;/strong&gt; &quot;Load more posts&quot; and deep reply threads are exactly the calls that tend to need a session. Using a logged-in account for scraping risks that account, and it's clearly against Meta's terms.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For a no-login threads scraper, the embedded JSON is the more stable source, even though you only get the first page.&lt;/p&gt;
&lt;h3 id=&quot;rate-limits-ip-blocks-and-legal-notes&quot;&gt;Rate limits, IP blocks, and legal notes&lt;/h3&gt;
&lt;p&gt;Meta doesn't publish limits for the public web pages. From what people commonly report, a residential IP fetching a few pages a minute is fine, while datacenter IPs get challenged or redirected to login much sooner. I'd stick to these rules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One request every few seconds,&lt;/strong&gt; with random jitter, and back off on 429.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache HTML&lt;/strong&gt; during development so you're not hitting Threads every time you tweak a parser.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Watch for soft blocks.&lt;/strong&gt; A 200 with a login shell is the usual symptom, not a clean 403.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the legal side: Meta's terms prohibit automated collection without permission, and courts in different countries have treated public-data scraping differently. Only collect public data, don't collect more personal data than your use case needs, respect GDPR if you store data about EU users, and talk to a lawyer before building anything commercial. This post is a tutorial, not legal advice.&lt;/p&gt;
&lt;h3 id=&quot;the-official-threads-api&quot;&gt;The official Threads API&lt;/h3&gt;
&lt;p&gt;Meta has an official Threads API (&lt;code&gt;graph.threads.net&lt;/code&gt;). It's the right tool when you're working with &lt;strong&gt;your own&lt;/strong&gt; account: publishing, reading your posts and their insights, managing replies. Meta has since added keyword search and some public profile lookups, but they require app review, specific permissions, and an access token tied to a Threads user, and they come with their own rate limits. Check the current docs before relying on them, because the scope has been changing.&lt;/p&gt;
&lt;p&gt;What it won't give you is an easy &quot;give me any public account's posts&quot; call with no app review. That gap is why people write scrapers in the first place.&lt;/p&gt;
&lt;h3 id=&quot;project-a-threads-engagement-tracker&quot;&gt;Project: a Threads engagement tracker&lt;/h3&gt;
&lt;p&gt;Now the practical bit. This script snapshots a few accounts and appends one line per post per run to &lt;code&gt;snapshots.jsonl&lt;/code&gt;. JSON Lines keeps it dependency-free; swap in SQLite (&lt;code&gt;better-sqlite3&lt;/code&gt;, or &lt;code&gt;node:sqlite&lt;/code&gt; on recent Node versions) when the file gets large.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// track.js
import { appendFile } from &quot;node:fs/promises&quot;;
import { scrapeProfile } from &quot;./profile.js&quot;;

const HANDLES = process.argv.slice(2);
if (!HANDLES.length) {
  console.error(&quot;usage: node track.js handle1 handle2 ...&quot;);
  process.exit(1);
}
const sleep = (ms) =&amp;gt; new Promise((r) =&amp;gt; setTimeout(r, ms));

const runAt = new Date().toISOString();
for (const handle of HANDLES) {
  try {
    const { followers, posts } = await scrapeProfile(handle);
    const lines = posts
      .filter((p) =&amp;gt; !p.isReply)
      .map((p) =&amp;gt; JSON.stringify({ runAt, handle, followers, ...p }));
    if (lines.length) await appendFile(&quot;snapshots.jsonl&quot;, lines.join(&quot;\n&quot;) + &quot;\n&quot;);
    console.log(`${handle}: ${lines.length} posts, ${followers ?? &quot;?&quot;} followers`);
  } catch (err) {
    console.error(`${handle}: ${err.message}`);
  }
  await sleep(4_000 + Math.random() * 3_000);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Schedule it with cron every few hours:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;0 */4 * * * cd /path/to/threads-scraper &amp;amp;&amp;amp; /usr/bin/node track.js handle1 handle2 &amp;gt;&amp;gt; track.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And a report that compares each post's first and latest snapshot:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// report.js
import { readFile } from &quot;node:fs/promises&quot;;

const rows = (await readFile(&quot;snapshots.jsonl&quot;, &quot;utf8&quot;))
  .split(&quot;\n&quot;)
  .filter(Boolean)
  .map((l) =&amp;gt; JSON.parse(l));

const byPost = new Map();
for (const r of rows) {
  const s = byPost.get(r.code) ?? { first: r, last: r };
  if (r.runAt &amp;lt; s.first.runAt) s.first = r;
  if (r.runAt &amp;gt; s.last.runAt) s.last = r;
  byPost.set(r.code, s);
}

const delta = (a, b) =&amp;gt; (a == null || b == null ? null : b - a);
const report = [...byPost.values()]
  .map(({ first, last }) =&amp;gt; ({
    post: last.url,
    snapshots: `${first.runAt.slice(0, 16)} -&amp;gt; ${last.runAt.slice(0, 16)}`,
    likes: last.likes,
    likeGain: delta(first.likes, last.likes),
    replyGain: delta(first.replies, last.replies),
    repostGain: delta(first.reposts, last.reposts),
  }))
  .sort((a, b) =&amp;gt; (b.likeGain ?? -1) - (a.likeGain ?? -1));

console.table(report.slice(0, 15));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One thing to keep in mind when reading this report: because you only see the first page of posts, older posts drop out of the snapshots once newer ones push them off. A post that stops appearing hasn't stopped growing; you just can't see it anymore. If you need to keep following specific posts, feed their URLs through &lt;code&gt;scrapePost()&lt;/code&gt; on each run instead.&lt;/p&gt;
&lt;h3 id=&quot;the-managed-alternative&quot;&gt;The managed alternative&lt;/h3&gt;
&lt;p&gt;If maintaining the parser becomes a chore, or you need more than the first page from datacenter IPs, &lt;a rel=&quot;nofollow&quot; href=&quot;https://scrapeatlas.com&quot;&gt;ScrapeAtlas*&lt;/a&gt; has Threads endpoints that return the same post objects (&lt;code&gt;like_count&lt;/code&gt;, &lt;code&gt;taken_at&lt;/code&gt;, &lt;code&gt;text_post_app_info.direct_reply_count&lt;/code&gt;, and so on). Authentication is an &lt;code&gt;x-api-key&lt;/code&gt; header:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const atlas = (path, params) =&amp;gt;
  fetch(`https://api.scrapeatlas.com/v1/threads/${path}?${new URLSearchParams(params)}`, {
    headers: { &quot;x-api-key&quot;: process.env.SCRAPEATLAS_API_KEY },
  }).then((r) =&amp;gt; r.json());

const profile = await atlas(&quot;profile&quot;, { handle: &quot;zuck&quot; });
console.log(profile.username, profile.follower_count, profile.is_verified);

const feed = await atlas(&quot;user/posts&quot;, { handle: &quot;zuck&quot;, since: &quot;2026-10-01&quot; });
console.log(feed.posts.map((p) =&amp;gt; [p.url, p.like_count, p.text_post_app_info.direct_reply_count]));
console.log(feed.coverage); // e.g. { complete: false, warnings: [&quot;posts_first_page_only&quot;], ... }

const thread = await atlas(&quot;post&quot;, { url: &quot;https://www.threads.com/@zuck/post/&amp;lt;code&amp;gt;&quot; });
console.log(thread.post.like_count, thread.comments.length);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To be clear about the limits: &lt;code&gt;user/posts&lt;/code&gt; is also first page only, which the response tells you in &lt;code&gt;coverage.warnings&lt;/code&gt;, so the tracker's blind spot is the same. &lt;code&gt;post&lt;/code&gt; takes &lt;code&gt;include_comments=false&lt;/code&gt; if you only want the post, and every endpoint accepts &lt;code&gt;trim=true&lt;/code&gt; for a compact projection. There are also &lt;code&gt;user/replies&lt;/code&gt;, &lt;code&gt;post/comments&lt;/code&gt;, &lt;code&gt;search&lt;/code&gt; (keyword search with &lt;code&gt;recent&lt;/code&gt;, &lt;code&gt;start_date&lt;/code&gt;, and &lt;code&gt;end_date&lt;/code&gt;), &lt;code&gt;search/users&lt;/code&gt;, and &lt;code&gt;search/tags&lt;/code&gt;. Search is the part you can't do logged-out on your own.&lt;/p&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Read the preloaded JSON, not the DOM.&lt;/strong&gt; Walk every &lt;code&gt;application/json&lt;/code&gt; script and match on post shape, not a fixed path.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dedupe by &lt;code&gt;code&lt;/code&gt;&lt;/strong&gt; and normalize &lt;code&gt;threads.net&lt;/code&gt; to &lt;code&gt;threads.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expect the first page only&lt;/strong&gt; without logging in, and design your tracker around that.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skip GraphQL &lt;code&gt;doc_id&lt;/code&gt; calls&lt;/strong&gt; unless you're ready to re-capture them every time Meta ships a new build.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Go slow and cache,&lt;/strong&gt; and treat a small 200 response as a block.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For your own account,&lt;/strong&gt; use the official Threads API.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the extractor stops finding posts for you, leave a comment with what the page source looks like now and I'll update the post.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;*I build it.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31945/how-to-scrape-threads-with-node-js-profiles-posts-replies-and-an-engagement-tracker-2026</guid>
<pubDate>Wed, 21 Oct 2026 09:05:56 +0000</pubDate>
</item>
<item>
<title>Stop Battling JavaScript Date Bugs: A Complete Guide to the Temporal API</title>
<link>https://coderlegion.com/31943/stop-battling-javascript-date-bugs-a-complete-guide-to-the-temporal-api</link>
<description>&lt;h2 id=&quot;stop-battling-javascript-date-bugs-a-complete-guide-to-the-temporal-api&quot;&gt;Stop Battling JavaScript Date Bugs: A Complete Guide to the Temporal API&lt;/h2&gt;
&lt;p&gt;The Temporal API is a modern, built-in JavaScript solution designed to overhaul and replace the legacy &lt;code&gt;Date&lt;/code&gt; object. It introduces a comprehensive suite of distinct, immutable objects for managing dates, times, durations, and time zones with exact precision. By cleanly separating wall-clock times from global timestamps, Temporal eliminates decades of common date-handling pitfalls in web applications.&lt;/p&gt;
&lt;h3 id=&quot;the-analogy-mechanical-wall-clocks-vs-modern-atomic-smartwatches&quot;&gt;The Analogy: Mechanical Wall Clocks vs. Modern Atomic Smartwatches&lt;/h3&gt;
&lt;p&gt;To understand why we need the Temporal API, picture a traditional mechanical wall clock hanging in a busy train station. If you manually turn the clock's minute hand forward to adjust for a delay, the internal gears might accidentally shift the hour hand, slip off track, or completely lose alignment with the sun outside. Worse yet, if two people try to adjust the clock at the same time, the clock's physical state gets corrupted.&lt;/p&gt;
&lt;p&gt;This is how JavaScript's legacy &lt;code&gt;Date&lt;/code&gt; object operates: it is mutable (meaning its value can be modified after creation) and constantly tries to guess your local time zone behind the scenes.&lt;/p&gt;
&lt;p&gt;The Temporal API, on the other hand, acts like a synchronized atomic smartwatch network. When you want to see what time it will be in three hours, the smartwatch does not alter your current time display. Instead, it generates a brand-new, unchangeable reading specifically for that future moment. It keeps local time, calendar dates, and exact global timestamps in separate, dedicated digital registers, ensuring that adjusting one setting never corrupts another.&lt;/p&gt;
&lt;h3 id=&quot;why-it-matters-in-daily-web-development&quot;&gt;Why It Matters in Daily Web Development&lt;/h3&gt;
&lt;p&gt;Every day, software engineers write code that schedules notifications, processes monthly recurring subscriptions, or logs critical system events. Using traditional JavaScript &lt;code&gt;Date&lt;/code&gt; handling, these operations frequently trigger subtle, costly bugs in production systems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Accidental Mutations:&lt;/strong&gt; Passing a standard date object into a utility function might accidentally alter the original date variable elsewhere in your codebase.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time Zone Confusions:&lt;/strong&gt; A traditional date object automatically converts timestamps based on the local machine's system settings. This causes a server running in UTC (Coordinated Universal Time, the primary time standard by which the world regulates clocks) to interpret a user's midnight entry as 4:00 PM the previous day.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Daylight Saving Time (DST) Distortions:&lt;/strong&gt; Calculating 24 hours from a given moment during a DST transition can result in events firing an hour early or late.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The Temporal API solves these daily problems by enforcing immutability and introducing specific types like &lt;code&gt;Temporal.PlainDate&lt;/code&gt; (for calendar dates without times) and &lt;code&gt;Temporal.ZonedDateTime&lt;/code&gt; (for exact moments anchored to a specific geographic region).&lt;/p&gt;
&lt;h3 id=&quot;code-example-node-js-date-calculations&quot;&gt;Code Example: Node.js Date Calculations&lt;/h3&gt;
&lt;p&gt;Below is a comparison showing how legacy Node.js date handling leads to unwanted side effects compared to the clean, safe approach provided by the Temporal API.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Legacy JavaScript Date Approach (Prone to bugs)
const eventDate = new Date('2025-05-01T10:00:00Z');

// We want to calculate a follow-up date 7 days later
const followUpDate = eventDate;
followUpDate.setDate(eventDate.getDate() + 7);

// BUG: Mutating followUpDate unexpectedly altered eventDate too!
console.log(eventDate.toISOString()); // Output: 2025-05-08T10:00:00.000Z


// Modern Temporal API Approach (Safe and Predictable)
// Note: Temporal is available in modern JS runtimes or via the @js-temporal/polyfill package
const { Temporal } = require('@js-temporal/polyfill');

// Create an exact zonal date-time for an event in Tokyo
const originalEvent = Temporal.ZonedDateTime.from({
  timeZone: 'Asia/Tokyo',
  year: 2025,
  month: 5,
  day: 1,
  hour: 10,
  minute: 0
});

// Add 7 days cleanly without modifying the original object
const reminderEvent = originalEvent.add({ days: 7 });

// The original object remains entirely untouched
console.log(originalEvent.toString()); // 2025-05-01T10:00:00+09:00[Asia/Tokyo]
console.log(reminderEvent.toString());  // 2025-05-08T10:00:00+09:00[Asia/Tokyo]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;key-takeaway-3&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;Transitioning to the Temporal API shifts date manipulation from a fragile, error-prone chore into a robust, type-safe development experience. By cleanly isolating distinct concepts like calendar dates, wall-clock times, and regional time zones into immutable objects, Temporal guarantees that your application processes time consistently across every server and browser on the planet.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/modernizing-javascript-time-why.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31943/stop-battling-javascript-date-bugs-a-complete-guide-to-the-temporal-api</guid>
<pubDate>Wed, 21 Oct 2026 08:53:20 +0000</pubDate>
</item>
<item>
<title>MyZubster Dev Update: Local AI, GitHub Onboarding &amp; Decentralized Collaboration 🚀</title>
<link>https://coderlegion.com/31922/myzubster-dev-update-local-ai-github-onboarding-decentralized-collaboration</link>
<description>&lt;h2 id=&quot;myzubster-dev-update-local-ai-github-onboarding-amp-decentralized-collaboration-&quot;&gt;MyZubster Dev Update: Local AI, GitHub Onboarding &amp;amp; Decentralized Collaboration 🚀&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;We're building MyZubster, an open-source ecosystem connecting developers, independent projects, services, and communities.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Today we made significant progress on &lt;strong&gt;Zorgax&lt;/strong&gt;, our locally hosted AI assistant.&lt;/p&gt;
&lt;p&gt;Our goal is simple: make it easier for anyone — including people with no programming experience — to join GitHub, contribute to projects, and become part of a collaborative ecosystem.&lt;/p&gt;
&lt;h3 id=&quot;-what-we-built-and-tested&quot;&gt;🔧 What We Built and Tested&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Vercel ↔ VPS Connectivity&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We restored communication between our Vercel Production deployment and the VPS backend using a signed HTTPS gateway.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Production assistant endpoint responding successfully&lt;/li&gt;
&lt;li&gt;HTTP 502 connectivity issue resolved in our tests&lt;/li&gt;
&lt;li&gt;Node.js gateway operational&lt;/li&gt;
&lt;li&gt;MongoDB connected&lt;/li&gt;
&lt;li&gt;Local Ollama inference available&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. GitHub Contributor Onboarding&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We implemented a deterministic onboarding guide in Zorgax.&lt;/p&gt;
&lt;p&gt;The assistant can now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Guide new users to GitHub registration&lt;/li&gt;
&lt;li&gt;Recognize completion messages such as &lt;code&gt;FATTO&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Ask for a GitHub username&lt;/li&gt;
&lt;li&gt;Recognize that username within the conversation&lt;/li&gt;
&lt;li&gt;Advance to Git and GitHub CLI setup&lt;/li&gt;
&lt;li&gt;Avoid claiming an account has been verified without evidence&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. Real Production Testing&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Our regression tests passed: &lt;strong&gt;4/4&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We also tested the public Zorgax chat and confirmed that the assistant correctly advanced from GitHub registration to the username and tooling stages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Local AI Architecture&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Our system currently combines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js backend&lt;/li&gt;
&lt;li&gt;Vercel frontend and gateway&lt;/li&gt;
&lt;li&gt;VPS-hosted services&lt;/li&gt;
&lt;li&gt;Ollama with &lt;code&gt;qwen2.5:3b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Deterministic onboarding rules&lt;/li&gt;
&lt;li&gt;MongoDB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach helps us keep sensitive onboarding workflows predictable while using local AI for more flexible assistance.&lt;/p&gt;
&lt;h3 id=&quot;-what-still-needs-work&quot;&gt;🐛 What Still Needs Work&lt;/h3&gt;
&lt;p&gt;Our tests also revealed that Zorgax can lose track of the onboarding sequence when it falls back to Ollama.&lt;/p&gt;
&lt;p&gt;For example, after Git and GitHub CLI version checks, it may answer a different MyZubster-related question instead of continuing with GitHub authentication.&lt;/p&gt;
&lt;p&gt;We're working toward a complete flow:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GitHub signup → username → Git/CLI checks → authentication → repository → first pull request&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;We want every step to be clear, verifiable, and beginner-friendly.&lt;/p&gt;
&lt;h3 id=&quot;-beyond-a-traditional-marketplace&quot;&gt;🌍 Beyond a Traditional Marketplace&lt;/h3&gt;
&lt;p&gt;MyZubster is also exploring ways to connect:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Independent developer projects&lt;/li&gt;
&lt;li&gt;Skills and services through a marketplace&lt;/li&gt;
&lt;li&gt;Contributor profiles and verifiable contributions&lt;/li&gt;
&lt;li&gt;Local community initiatives&lt;/li&gt;
&lt;li&gt;Decentralized infrastructure and autonomous nodes&lt;/li&gt;
&lt;li&gt;Privacy-conscious payments and collaboration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Our longer-term vision is a federated ecosystem where contributors can develop their own projects while remaining connected to a broader network.&lt;/p&gt;
&lt;p&gt;These are ongoing development goals, not claims that every feature is already production-ready.&lt;/p&gt;
&lt;h3 id=&quot;-looking-for-contributors&quot;&gt;🤝 Looking for Contributors&lt;/h3&gt;
&lt;p&gt;We're particularly interested in developers who want to help with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AI-assisted GitHub onboarding&lt;/li&gt;
&lt;li&gt;Node.js and API testing&lt;/li&gt;
&lt;li&gt;UX/UI simplification&lt;/li&gt;
&lt;li&gt;GitHub automation&lt;/li&gt;
&lt;li&gt;Marketplace workflows&lt;/li&gt;
&lt;li&gt;VPS and decentralized infrastructure&lt;/li&gt;
&lt;li&gt;Open-source documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don't need to build everything. Even a small, well-tested contribution can move the project forward.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Explore MyZubster:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com&quot;&gt;https://www.myzubster.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Try Zorgax:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/zorgax&quot;&gt;https://www.myzubster.com/zorgax&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem&quot;&gt;https://github.com/MyZubster-Ecosystem&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;let-s-collaborate&quot;&gt;Let's collaborate&lt;/h4&gt;
&lt;p&gt;We're interested in connecting independent developers and projects rather than requiring everyone to work inside a single centralized system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Question for the community:&lt;/strong&gt; How would you design an AI onboarding assistant that guides a complete beginner from creating a GitHub account to opening their first pull request?&lt;/p&gt;
&lt;h2 id=&quot;opensource-ai-github-nodejs-decentralization-developercommunity&quot;&gt;OpenSource #AI #GitHub #NodeJS #Decentralization #DeveloperCommunity&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31922/myzubster-dev-update-local-ai-github-onboarding-decentralized-collaboration</guid>
<pubDate>Wed, 21 Oct 2026 05:38:10 +0000</pubDate>
</item>
<item>
<title>Demystifying MongoDB Write Concern and Read Concern in Node.js</title>
<link>https://coderlegion.com/31698/demystifying-mongodb-write-concern-and-read-concern-in-node-js</link>
<description>&lt;h3 id=&quot;what-are-write-concern-and-read-concern-&quot;&gt;What Are Write Concern and Read Concern?&lt;/h3&gt;
&lt;p&gt;In MongoDB, &lt;strong&gt;write concern&lt;/strong&gt; and &lt;strong&gt;read concern&lt;/strong&gt; are configuration settings that specify the level of guarantee you require when saving or retrieving data across a distributed cluster. A write concern determines how many database nodes must successfully record an update—and whether that data must be written to disk—before MongoDB reports success to your application. A read concern determines how fresh and consistent the data returned by a query must be, protecting your application from reading temporary data that might later be reversed.&lt;/p&gt;
&lt;p&gt;Together, these two settings let developers choose where their application sits on the classic spectrum between maximum execution speed and rock-solid data safety.&lt;/p&gt;
&lt;h3 id=&quot;the-real-world-analogy-certified-mail-vs-kitchen-whiteboards&quot;&gt;The Real-World Analogy: Certified Mail vs. Kitchen Whiteboards&lt;/h3&gt;
&lt;p&gt;Imagine you run a bustling catering kitchen with multiple head chefs.&lt;/p&gt;
&lt;p&gt;When taking an order, you have two choices for your &lt;strong&gt;write concern&lt;/strong&gt;. You could scribble the customer's allergy request on a loose scrap of paper and immediately yell &quot;Order placed!&quot; (a fast write, but if you drop the paper, the order is lost). Alternatively, you could walk the order ticket over to the main order board, watch two sous-chefs sign off on it, and wait for them to file it in the metal lockbox before confirming the order with the customer. The second route takes several seconds longer, but you are guaranteed that the kitchen will never forget the order even if one chef walks out.&lt;/p&gt;
&lt;p&gt;Now consider the &lt;strong&gt;read concern&lt;/strong&gt;. If a waiter needs to know the soup of the day, they can either glance at a whiteboard where someone started drafting ideas with an erasable marker, or they can refuse to serve anything until they see the official menu stamped and approved by the head kitchen manager. Glancing at the draft whiteboard is instantaneous, but you might serve an experimental soup that the manager ultimately rejects five minutes later.&lt;/p&gt;
&lt;h3 id=&quot;why-it-matters-in-daily-software-engineering&quot;&gt;Why It Matters in Daily Software Engineering&lt;/h3&gt;
&lt;p&gt;Modern web architectures rarely run on a single database server. Instead, they use replica sets—groups of connected servers where one &quot;primary&quot; node handles writes and multiple &quot;secondary&quot; nodes replicate that data for redundancy. &lt;/p&gt;
&lt;p&gt;If you leave MongoDB on default settings during high-traffic moments, subtle race conditions emerge:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Rollback Catastrophes:&lt;/strong&gt; If an API endpoint confirms an e-commerce order after writing only to the primary node, and that primary node suddenly experiences a hardware failure before syncing to secondary nodes, the cluster elects a new primary. Your customer's order is wiped out, yet your payment processor already charged their credit card.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dirty Reads:&lt;/strong&gt; A background worker updates a bank balance. A microservice queries the secondary replica before the primary finishes replicating. If that transaction aborts or fails over, the microservice has operated on data that never officially existed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By tuning write and read concerns, software engineers intentionally shield critical operations (payments, account transfers, credential updates) from data loss while letting low-risk operations (logging, view counters, user analytics) run with blazing speed.&lt;/p&gt;
&lt;h3 id=&quot;implementation-in-node-js&quot;&gt;Implementation in Node.js&lt;/h3&gt;
&lt;p&gt;Here is how you apply write and read concerns when working with the official MongoDB Node.js driver:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { MongoClient } from 'mongodb';

const client = new MongoClient('mongodb://localhost:27017,localhost:27018/?replicaSet=rs0');

async function processPayment(orderId, paymentDetails) {
  await client.connect();
  const db = client.db('store');

  // Configure high-durability options for financial transactions
  const ordersCollection = db.collection('orders', {
    writeConcern: {
      w: 'majority',      // Must be acknowledged by more than half of the replica set nodes
      j: true,            // Must be written to the on-disk journal before confirming
      wtimeout: 5000      // Abort if replicas do not respond within 5 seconds
    },
    readConcern: {
      level: 'majority'   // Only read data committed by a majority of nodes
    }
  });

  // Step 1: Safely write with acknowledgment
  const writeResult = await ordersCollection.updateOne(
    { _id: orderId },
    { $set: { status: 'PAID', payment: paymentDetails } }
  );

  // Step 2: Read guaranteed, durable data
  const confirmedOrder = await ordersCollection.findOne({ _id: orderId });

  console.log('Payment processed safely:', confirmedOrder._id);
  return confirmedOrder;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, specifying &lt;code&gt;w: 'majority'&lt;/code&gt; and &lt;code&gt;j: true&lt;/code&gt; ensures that the write cannot be rolled back if the primary server abruptly crashes. Setting &lt;code&gt;level: 'majority'&lt;/code&gt; on the read ensures your application reads only finalized records.&lt;/p&gt;
&lt;h3 id=&quot;key-takeaway-4&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;Database speed and data reliability exist on a continuous slider: write concern dictates how hard your cluster works to preserve your updates, while read concern dictates how strictly your cluster verifies the stability of what you fetch. Mastering both allows you to protect financial transactions and vital data without unnecessarily throttling high-volume, low-risk endpoints across your stack.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/how-mongodb-write-concern-and-read.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31698/demystifying-mongodb-write-concern-and-read-concern-in-node-js</guid>
<pubDate>Mon, 19 Oct 2026 09:22:10 +0000</pubDate>
</item>
<item>
<title>Stop Hardcoding Secrets: Master Environment Variables and Dotenv in Node.js</title>
<link>https://coderlegion.com/31504/stop-hardcoding-secrets-master-environment-variables-and-dotenv-in-node-js</link>
<description>&lt;h2 id=&quot;stop-hardcoding-secrets-master-environment-variables-and-dotenv-in-node-js&quot;&gt;Stop Hardcoding Secrets: Master Environment Variables and Dotenv in Node.js&lt;/h2&gt;
&lt;p&gt;When building modern web applications, keeping sensitive information secure is just as critical as writing functional code. One of the most fundamental practices every developer must learn is using environment variables and dotenv configuration.&lt;/p&gt;
&lt;h3 id=&quot;what-are-environment-variables-and-dotenv-configuration-&quot;&gt;What Are Environment Variables and Dotenv Configuration?&lt;/h3&gt;
&lt;p&gt;Environment variables are dynamic key-value pairs stored at the operating system or runtime level outside of an application's codebase. In Node.js development, dotenv configuration refers to utilizing a lightweight library called &lt;code&gt;dotenv&lt;/code&gt; that automatically loads settings from a plain &lt;code&gt;.env&lt;/code&gt; file into Node's runtime environment (&lt;code&gt;process.env&lt;/code&gt;). This mechanism allows developers to customize application behavior and supply secret credentials without modifying source code files.&lt;/p&gt;
&lt;h3 id=&quot;a-real-world-analogy-the-wallet-index-card&quot;&gt;A Real-World Analogy: The Wallet Index Card&lt;/h3&gt;
&lt;p&gt;Imagine you own a custom leather wallet. Instead of permanently embroidering your house address, garage passcode, and emergency contact numbers onto the outer leather where every passerby can read them, you write those sensitive details on a small index card and slide it into an interior pocket.&lt;/p&gt;
&lt;p&gt;When you move to a new apartment or change your passcode, you do not buy a brand-new wallet or destroy the leather stitching. You simply pull out the old index card and slide in an updated one. In this analogy, your wallet is your core application codebase, while the index card inside the pocket acts as your local &lt;code&gt;.env&lt;/code&gt; file holding temporary, site-specific configuration details.&lt;/p&gt;
&lt;h3 id=&quot;why-environment-variables-matter-every-single-day&quot;&gt;Why Environment Variables Matter Every Single Day&lt;/h3&gt;
&lt;p&gt;In professional software engineering, committing raw passwords, secret encryption keys, or API tokens directly into a Git repository is a dangerous vulnerability. Automated web scraping bots constantly crawl public code repositories like GitHub, scanning for exposed database passwords or cloud provider credentials within seconds of a commit. Exposing these keys can lead to compromised database instances, stolen customer data, or massive financial liability from unauthorized cloud usage.&lt;/p&gt;
&lt;p&gt;Furthermore, modern applications run across multiple environments throughout their lifecycle, including local development machines, staging servers for testing, and live production servers handling real user traffic. Environment variables allow engineers to deploy the exact same application code across all these distinct targets. The application simply reads its configuration from whatever local environment variables exist on that specific host server.&lt;/p&gt;
&lt;h3 id=&quot;code-example-configuring-express-with-dotenv&quot;&gt;Code Example: Configuring Express with Dotenv&lt;/h3&gt;
&lt;p&gt;Below is a standard Node.js and Express server setup using the &lt;code&gt;dotenv&lt;/code&gt; package to securely access server configuration values and third-party API credentials.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Step 1: Import dependencies
// Ensure you have installed express and dotenv via npm: npm install express dotenv
require('dotenv').config();
const express = require('express');

const app = express();

// Step 2: Read configuration values from process.env
const PORT = process.env.PORT || 3000;
const API_KEY = process.env.PAYMENT_GATEWAY_KEY;
const DATABASE_URL = process.env.DATABASE_URL;

// Step 3: Define an Express route utilizing the loaded variables
app.get('/api/status', (req, res) =&amp;gt; {
  if (!API_KEY) {
    return res.status(500).json({ error: 'Missing critical API configuration.' });
  }
  
  res.json({
    status: 'Server is running securely',
    connectedDatabase: DATABASE_URL ? 'Connected' : 'Not Configured',
    environment: process.env.NODE_ENV || 'development'
  });
});

// Step 4: Start the server using the dynamic PORT
app.listen(PORT, () =&amp;gt; {
  console.log(`Server listening on port ${PORT}`);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Along with this JavaScript file, you create a file named &lt;code&gt;.env&lt;/code&gt; in the root folder of your project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;PORT=5000
NODE_ENV=development
DATABASE_URL=mysql://user:password@localhost:3306/dev_db
PAYMENT_GATEWAY_KEY=secret_live_key_987654321
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Crucially, you must add &lt;code&gt;.env&lt;/code&gt; to your project's &lt;code&gt;.gitignore&lt;/code&gt; file so it is never tracked by version control or pushed to remote repositories.&lt;/p&gt;
&lt;h3 id=&quot;key-takeaway-5&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;Decoupling application code from environment configuration is a foundational pillar of modern, secure software architecture. By adopting environment variables and dotenv configuration early in your development pipeline, you shield your systems from catastrophic credential leaks and build resilient applications ready to run anywhere without friction.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/the-ultimate-guide-to-environment.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31504/stop-hardcoding-secrets-master-environment-variables-and-dotenv-in-node-js</guid>
<pubDate>Sat, 17 Oct 2026 08:03:00 +0000</pubDate>
</item>
<item>
<title>A very Simple Rate Limiting App ( Node.JS )</title>
<link>https://coderlegion.com/31204/a-very-simple-rate-limiting-app-node-js</link>
<description>&lt;pre&gt;&lt;code&gt;enter code here

const express = require(&quot;express&quot;);

const app = express();

const requests = new Map();

const LIMIT = 5;
const WINDOW = 60 * 1000; // 1 minute

app.use((req, res, next) =&amp;gt; {
  const ip = req.ip;
  const now = Date.now();

  let data = requests.get(ip);

  // First request or expired time window
  if (!data || now - data.startTime &amp;gt;= WINDOW) {
    data = {
      count: 1,
      startTime: now,
    };

    requests.set(ip, data);
    return next();
  }

  // Rate limit exceeded
  if (data.count &amp;gt;= LIMIT) {
    return res.status(429).json({
      message: &quot;Too many requests. Try again later.&quot;,
    });
  }

  data.count++;
  next();
});

app.get(&quot;/&quot;, (req, res) =&amp;gt; {
  res.json({ message: &quot;Request successful!&quot; });
});

app.listen(3000, () =&amp;gt; {
  console.log(&quot;Server running on http://localhost:3000&quot;);
});

&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/31204/a-very-simple-rate-limiting-app-node-js</guid>
<pubDate>Thu, 15 Oct 2026 13:40:19 +0000</pubDate>
</item>
<item>
<title>Demystifying Greedy Algorithms: How Local Choices Solve Global Problems in Node.js</title>
<link>https://coderlegion.com/31180/demystifying-greedy-algorithms-how-local-choices-solve-global-problems-in-node-js</link>
<description>&lt;h3 id=&quot;what-is-a-greedy-algorithm-&quot;&gt;What is a Greedy Algorithm?&lt;/h3&gt;
&lt;p&gt;A greedy algorithm is an algorithmic strategy that solves a problem by making the most optimal decision at each individual step. Instead of looking ahead to long-term consequences or evaluating every possible combination, it focuses exclusively on immediate, short-term gain. If local choices logically compound into the ideal final result, the greedy strategy succeeds with maximum computational efficiency.&lt;/p&gt;
&lt;h3 id=&quot;the-cashier-s-coin-trick&quot;&gt;The Cashier’s Coin Trick&lt;/h3&gt;
&lt;p&gt;Imagine you are a cashier at a grocery store, and you need to give a customer 41 cents in change using the fewest coins possible. You look at your cash register, which contains quarters (25¢), dimes (10¢), nickels (5¢), and pennies (1¢).&lt;/p&gt;
&lt;p&gt;Instead of calculating every conceivable combination of coins, you naturally make the &quot;greedy&quot; choice at each step:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Grab the largest coin that does not exceed 41 cents: a Quarter (25¢). You now owe 16 cents.&lt;/li&gt;
&lt;li&gt;Grab the largest coin that does not exceed 16 cents: a Dime (10¢). You now owe 6 cents.&lt;/li&gt;
&lt;li&gt;Grab the largest coin that does not exceed 6 cents: a Nickel (5¢). You now owe 1 cent.&lt;/li&gt;
&lt;li&gt;Grab the largest coin that does not exceed 1 cent: a Penny (1¢). You now owe 0 cents.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You solved the problem instantly by picking the biggest immediate payoff at every step. That is precisely how a greedy algorithm operates in software.&lt;/p&gt;
&lt;h3 id=&quot;why-greedy-algorithms-matter-in-tech&quot;&gt;Why Greedy Algorithms Matter in Tech&lt;/h3&gt;
&lt;p&gt;In software engineering, computing the mathematically perfect solution for massive datasets can sometimes take hours—or even years—if an application tries to test every possible outcome. Software developers intentionally use greedy algorithms when a guaranteed quick answer that is optimal saves massive amounts of CPU memory and processing time.&lt;/p&gt;
&lt;p&gt;For instance, network routing protocols use greedy choices to route internet data packets along the shortest immediate paths between routers. Database query engines, file compression utilities like ZIP tools, and job schedulers in operating systems all rely on greedy strategies to keep web software responsive and performant under heavy traffic loads.&lt;/p&gt;
&lt;h3 id=&quot;code-example-making-change-in-node-js&quot;&gt;Code Example: Making Change in Node.js&lt;/h3&gt;
&lt;p&gt;Here is a plain Node.js implementation showing how a greedy approach quickly calculates coin change for any given amount:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Function to give change using the minimum number of coins
function makeChange(amount, coins = [25, 10, 5, 1]) {
  const result = {};
  let remaining = amount;

  // Iterate through available coins sorted from highest to lowest
  for (const coin of coins) {
    if (remaining &amp;lt;= 0) break;

    // Calculate how many coins of this denomination fit
    const count = Math.floor(remaining / coin);
    if (count &amp;gt; 0) {
      result[`${coin}c`] = count;
      remaining -= count * coin;
    }
  }

  return result;
}

// Example usage: Change for 87 cents
const change = makeChange(87);
console.log('Change breakdown:', change);
// Output: Change breakdown: { '25c': 3, '10c': 1, '1c': 2 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, the algorithm loops through sorted coin values and takes as many of the highest denomination as possible before moving to smaller coins.&lt;/p&gt;
&lt;h3 id=&quot;the-takeaway&quot;&gt;The Takeaway&lt;/h3&gt;
&lt;p&gt;Greedy algorithms trade exhaustive foresight for immediate speed, proving that making the best local decision at every step can often yield the best global outcome. While they do not work for every single problem—especially when early sacrifices yield better long-term rewards—understanding when to apply a greedy strategy is a fundamental skill for building high-performance backend systems.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/understanding-greedy-algorithms-simple.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31180/demystifying-greedy-algorithms-how-local-choices-solve-global-problems-in-node-js</guid>
<pubDate>Thu, 15 Oct 2026 08:35:56 +0000</pubDate>
</item>
<item>
<title>Demystifying Graph Databases: Beyond the Table and Column</title>
<link>https://coderlegion.com/30918/demystifying-graph-databases-beyond-the-table-and-column</link>
<description>&lt;h4 id=&quot;what-is-a-graph-database-&quot;&gt;What Is a Graph Database?&lt;/h4&gt;
&lt;p&gt;A graph database is a specialized storage engine designed to treat relationships between data points with the exact same importance as the data points themselves. Instead of storing information in rigid rows, columns, and isolated tables, it organizes data into distinct items called &lt;strong&gt;nodes&lt;/strong&gt; and explicit connections called &lt;strong&gt;edges&lt;/strong&gt;. This architectural design means finding how two items connect requires following a direct pointer rather than calculating expensive match-ups across disparate tables.&lt;/p&gt;
&lt;h4 id=&quot;the-real-life-analogy-the-train-route-map&quot;&gt;The Real-Life Analogy: The Train Route Map&lt;/h4&gt;
&lt;p&gt;Imagine planning a trip across Europe using a subway or train route map. Each city station is an actual physical location (a node), and the train tracks running directly between them are the routes (the edges).&lt;/p&gt;
&lt;p&gt;If you want to travel from Lisbon to Vienna, you trace your finger directly along the tracks that link each connecting station. You do not need to scan an enormous phone directory listing every citizen living in Madrid, Paris, or Berlin just to see if their tracks align. You follow the direct physical lines already drawn on the map. A traditional relational database acts like that giant phone directory, forcing you to cross-reference multiple massive lists to deduce who connects to whom. A graph database simply follows the drawn train tracks.&lt;/p&gt;
&lt;h4 id=&quot;why-it-matters-daily-in-software-engineering&quot;&gt;Why It Matters Daily in Software Engineering&lt;/h4&gt;
&lt;p&gt;In standard relational systems like MySQL, finding indirect relationships—such as &quot;friends of friends&quot; or &quot;fraudulent transactions linked by shared IP addresses&quot;—requires combining tables using operations called &lt;strong&gt;JOINs&lt;/strong&gt;. When you join a table to itself three, four, or five levels deep, query execution time degrades exponentially. The database engine must scan and compare millions of rows across foreign keys, causing CPU spikes, connection pool exhaustion, and slow API responses.&lt;/p&gt;
&lt;p&gt;Engineers reach for graph databases (or graph data modeling patterns) to prevent these multi-hop queries from bringing production services to a halt. When building recommendation feeds, access control lists (RBAC/ABAC), or fraud rings, graph structures guarantee index-free adjacency. That means traversing from one record to its neighbor takes constant time, regardless of whether your database holds ten thousand records or ten billion.&lt;/p&gt;
&lt;h4 id=&quot;code-demonstration-graph-traversal-in-node-js&quot;&gt;Code Demonstration: Graph Traversal in Node.js&lt;/h4&gt;
&lt;p&gt;To see how naturally relationships can be navigated compared to complex SQL recursive joins, here is a pure Node.js in-memory graph implementation showing how to find second-degree network connections (friends of friends):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// A lightweight graph implementation using Node.js Maps and Sets
class SocialGraph {
  constructor() {
    this.adjacencyList = new Map();
  }

  addUser(userId) {
    if (!this.adjacencyList.has(userId)) {
      this.adjacencyList.set(userId, new Set());
    }
  }

  // An edge represents a bidirectional friendship
  addFriendship(userA, userB) {
    this.addUser(userA);
    this.addUser(userB);
    this.adjacencyList.get(userA).add(userB);
    this.adjacencyList.get(userB).add(userA);
  }

  // Find recommendations: friends of friends who are not direct friends
  getFriendRecommendations(userId) {
    if (!this.adjacencyList.has(userId)) return [];

    const directFriends = this.adjacencyList.get(userId);
    const recommendations = new Set();

    for (const friend of directFriends) {
      const friendsOfFriend = this.adjacencyList.get(friend);
      for (const candidate of friendsOfFriend) {
        // Exclude the user themselves and existing direct friends
        if (candidate !== userId &amp;amp;&amp;amp; !directFriends.has(candidate)) {
          recommendations.add(candidate);
        }
      }
    }

    return Array.from(recommendations);
  }
}

// Usage example
const network = new SocialGraph();
network.addFriendship('Alice', 'Bob');
network.addFriendship('Bob', 'Charlie');
network.addFriendship('Charlie', 'Diana');

const suggestionsForAlice = network.getFriendRecommendations('Alice');
console.log('Suggestions for Alice:', suggestionsForAlice); 
// Output: [ 'Charlie' ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that this requires zero nested scans over an entire table of users. We simply jump along existing references.&lt;/p&gt;
&lt;h4 id=&quot;key-takeaway-6&quot;&gt;Key Takeaway&lt;/h4&gt;
&lt;p&gt;Relational databases excel at structured transactions and financial ledgers, but when your application's core value depends on discovering how entities connect, forcing that data into two-dimensional tables creates severe architectural friction. Choosing a graph model shifts the focus from data isolated in rows to the living connections between them, turning sluggish multi-table bottlenecks into instant pointer hops.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/understanding-graph-databases.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30918/demystifying-graph-databases-beyond-the-table-and-column</guid>
<pubDate>Tue, 13 Oct 2026 11:07:58 +0000</pubDate>
</item>
<item>
<title>Fix pnpm Migration Phantom Dependency Errors</title>
<link>https://coderlegion.com/30643/fix-pnpm-migration-phantom-dependency-errors</link>
<description>&lt;p&gt;Migrating a Node.js project from package managers that use hoisted &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/optimizing-node-modules-templates-for-faster-deployments/&quot;&gt;node_modules&lt;/a&gt; structures to strict content-addressable package managers often exposes hidden import assumptions that break continuous integration runners.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quick solution:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pnpm add sharp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When a Node script fails with &lt;code&gt;Error [ERR_MODULE_NOT_FOUND]&lt;/code&gt; for a package like &lt;code&gt;sharp&lt;/code&gt; imported from &lt;code&gt;scripts/medium-package.mjs&lt;/code&gt; after a clean continuous integration install, add the missing package to your manifest using &lt;code&gt;pnpm add sharp&lt;/code&gt; for runtime dependencies or &lt;code&gt;pnpm add -D sharp&lt;/code&gt; for build tools. Prerequisites include running a clean environment with a lockfile and running &lt;code&gt;pnpm install --frozen-lockfile&lt;/code&gt; to ensure accurate dependency trees after committing updates.&lt;/p&gt;
&lt;h3 id=&quot;diagnosing-module-resolution-versus-build-failures&quot;&gt;Diagnosing Module Resolution Versus Build Failures&lt;/h3&gt;
&lt;p&gt;Traditional package managers often rely on node_modules layouts where dependencies of dependencies are hoisted to the root directory, allowing scripts or build tools to import packages without explicitly declaring them in the project manifest. When transitioning to pnpm, its strict symlinked and isolated structure removes this hoisting behavior. Undeclared imports that worked locally due to cached modules or ambient hoisting suddenly fail in clean continuous integration environments.&lt;/p&gt;
&lt;h3 id=&quot;symptom-to-resolution-guide&quot;&gt;Symptom to Resolution Guide&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Option &lt;/th&gt;&lt;th&gt;When to Use It &lt;/th&gt;&lt;th&gt;Trade-off or Failure Mode &lt;/th&gt;&lt;th&gt;Recommendation&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;pnpm add &amp;lt;package&amp;gt;&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Runtime dependency is missing from the manifest &lt;/td&gt;&lt;td&gt;Increases direct dependency count &lt;/td&gt;&lt;td&gt;Explicitly declare all runtime packages in dependencies&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;pnpm add -D &amp;lt;package&amp;gt;&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Build or script tool is missing from the manifest &lt;/td&gt;&lt;td&gt;Might bloat devDependencies if misused &lt;/td&gt;&lt;td&gt;Use for local build scripts and automation tasks&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Frozen Lockfile Update &lt;/td&gt;&lt;td&gt;Pipeline throws lockfile mismatch errors &lt;/td&gt;&lt;td&gt;Requires committing workspace changes &lt;/td&gt;&lt;td&gt;Always commit lockfile updates before pushing to remote CI&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;auditing-and-fixing-missing-direct-imports&quot;&gt;Auditing and Fixing Missing Direct Imports&lt;/h3&gt;
&lt;p&gt;To prevent continuous integration failures, audit all custom scripts, build hooks, and automation tools located in directories like scripts or tools. Ensure every package imported via ES module syntax or CommonJS require statements is explicitly listed under dependencies or devDependencies in your project configuration file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;devDependencies&quot;: {
    &quot;sharp&quot;: &quot;*&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After declaring the missing dependencies, update and commit the correct workspace lockfile, rerun a clean frozen install in a fresh checkout or clean workspace, and then rerun the exact failing script to verify that module resolution succeeds without relying on residual global state.&lt;/p&gt;
&lt;h3 id=&quot;verification-and-execution-steps&quot;&gt;Verification and Execution Steps&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;pnpm install --frozen-lockfile&lt;/code&gt; in a fresh checkout or clean workspace to simulate a clean installation and verify that the expected result is a successfully generated dependency tree without unexpected lockfile modifications.&lt;/li&gt;
&lt;li&gt;Locate the specific importing workspace or package manifest and verify that any required module is explicitly declared under dependencies or devDependencies.&lt;/li&gt;
&lt;li&gt;Execute the exact failing script locally using &lt;code&gt;node scripts/medium-package.mjs&lt;/code&gt; to confirm that the previous module resolution error no longer occurs.&lt;/li&gt;
&lt;li&gt;Review continuous integration pipeline logs after pushing your updated lockfile and manifest changes to ensure artifact generation completes successfully.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30643/fix-pnpm-migration-phantom-dependency-errors</guid>
<pubDate>Sat, 10 Oct 2026 20:36:36 +0000</pubDate>
</item>
<item>
<title>Caching Strategies Demystified: Cache-Aside, Write-Through, and Write-Behind</title>
<link>https://coderlegion.com/30441/caching-strategies-demystified-cache-aside-write-through-and-write-behind</link>
<description>&lt;p&gt;In software development, caching is the practice of storing frequently used data in a high-speed, temporary storage layer so future requests can be served faster. &lt;strong&gt;Caching strategies&lt;/strong&gt; are the architectural patterns that dictate how your application coordinates the movement of data between this high-speed temporary memory (the cache) and your permanent, slower storage system (the database). Choosing the right strategy ensures your application stays incredibly fast while keeping data accurate and consistent.&lt;/p&gt;
&lt;h4 id=&quot;the-notebook-and-the-library-analogy&quot;&gt;The Notebook and the Library Analogy&lt;/h4&gt;
&lt;p&gt;Imagine you are a researcher studying in a massive university library (the database) containing millions of books. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cache-Aside (Lazy Loading):&lt;/strong&gt; You keep a blank personal notebook (the cache) on your desk. When you need a fact, you look in your notebook first. If it is there (a cache hit), you read it instantly. If not (a cache miss), you walk into the library archives, search the shelves, find the fact, write it down in your notebook, and then read it. &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write-Through:&lt;/strong&gt; Every time you discover a new piece of information, you write it in your personal notebook and immediately send a copy to the library's archiving desk to ensure both records are updated simultaneously before you proceed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write-Behind (Write-Back):&lt;/strong&gt; When you are brainstorming rapidly, you scribble your notes exclusively in your notebook. At the end of the day, you hand your notebook to a library assistant who updates the main archives in the background while you sleep.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;why-caching-strategies-matter-in-the-tech-industry&quot;&gt;Why Caching Strategies Matter in the Tech Industry&lt;/h4&gt;
&lt;p&gt;Without these strategies, modern platforms would collapse. If a viral social media post forces millions of users to fetch the same user profile directly from a database, the database will exhaust its connection pool, slow down, and eventually crash. &lt;/p&gt;
&lt;p&gt;Software engineers use these patterns strategically to balance speed and data integrity:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cache-Aside&lt;/strong&gt; prevents databases from being choked by heavy read traffic while keeping implementation simple.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write-Through&lt;/strong&gt; avoids data discrepancies, ensuring that critical data (like bank account balances) is never out of sync between the memory and the disk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write-Behind&lt;/strong&gt; is utilized in write-heavy systems (like logging user activity or IoT sensor tracking) to group thousands of database writes into batch updates, protecting disk-based storage from wearing out or bottlenecking.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;implementing-cache-aside-in-node-js-amp-express&quot;&gt;Implementing Cache-Aside in Node.js &amp;amp; Express&lt;/h4&gt;
&lt;p&gt;Here is how you can implement a standard &lt;strong&gt;Cache-Aside&lt;/strong&gt; strategy using Express.js and MySQL. In this scenario, we check an in-memory cache object before querying our relational database.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require('express');
const mysql = require('mysql2/promise');
const app = express();

// Mocking our fast memory cache (e.g., Redis)
const memoryCache = new Map();

// MySQL connection pool configuration
const dbPool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  database: 'production_db'
});

// Route utilizing Cache-Aside
app.get('/api/users/:id', async (req, res) =&amp;gt; {
  const userId = req.params.id;
  const cacheKey = `user:${userId}`;

  try {
    // 1. Check if the data exists in the high-speed cache
    if (memoryCache.has(cacheKey)) {
      console.log('Cache Hit! Serving from memory.');
      return res.json(memoryCache.get(cacheKey));
    }

    // 2. Cache Miss: Retrieve data from the slow MySQL database
    console.log('Cache Miss. Fetching from MySQL database...');
    const [rows] = await dbPool.query('SELECT id, name, email FROM users WHERE id = ?', [userId]);

    if (rows.length === 0) {
      return res.status(404).json({ error: 'User not found' });
    }

    const userData = rows[0];

    // 3. Write data to the cache so the next request is instant
    memoryCache.set(cacheKey, userData);

    return res.json(userData);
  } catch (error) {
    return res.status(500).json({ error: 'Internal Server Error' });
  }
});

app.listen(3000, () =&amp;gt; console.log('Server running on port 3000'));
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;key-takeaway-7&quot;&gt;Key Takeaway&lt;/h4&gt;
&lt;p&gt;Caching is not a one-size-fits-all solution; it is a careful compromise between performance and accuracy. While caching can make your application feel instantaneous, selecting the wrong update strategy can lead to stale data displays or lost records, making it vital to match your architectural pattern to your application's specific read-and-write ratios.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/how-to-scale-your-database-guide-to.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30441/caching-strategies-demystified-cache-aside-write-through-and-write-behind</guid>
<pubDate>Fri, 09 Oct 2026 10:55:06 +0000</pubDate>
</item>
<item>
<title>🧠 **Coder Legion Dev Log — Building the MyZubster Metaverse from the infrastructure up**</title>
<link>https://coderlegion.com/30192/coder-legion-dev-log-building-the-myzubster-metaverse-from-the-infrastructure-up</link>
<description>&lt;p&gt;🧠 &lt;strong&gt;Coder Legion Dev Log — Building the MyZubster Metaverse from the infrastructure up&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I’ve been working on the MyZubster Metaverse with a very specific engineering approach:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;do not start from the 3D layer. Start from identity, authorization, state, lifecycle and failure handling.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The current stack already includes several production-oriented building blocks.&lt;/p&gt;
&lt;h4 id=&quot;identity-and-session-model&quot;&gt;Identity and session model&lt;/h4&gt;
&lt;p&gt;The Metaverse supports both:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;guest characters&lt;/li&gt;
&lt;li&gt;authenticated, account-linked identities&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For authenticated users, canonical character data is resolved server-side so the client cannot freely override trusted identity state.&lt;/p&gt;
&lt;p&gt;This matters because virtual-world logic becomes much harder to secure once identity, permissions and session state are mixed together.&lt;/p&gt;
&lt;h4 id=&quot;realtime-authorization&quot;&gt;Realtime authorization&lt;/h4&gt;
&lt;p&gt;One of the more important backend changes was introducing a &lt;strong&gt;fail-closed authorization path&lt;/strong&gt; for community realtime channels.&lt;/p&gt;
&lt;p&gt;If the membership authority is unavailable, access is denied instead of waiting on buffered database operations or leaving socket authorization in an ambiguous state.&lt;/p&gt;
&lt;p&gt;I added coverage for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;disconnected MongoDB state&lt;/li&gt;
&lt;li&gt;active persisted membership&lt;/li&gt;
&lt;li&gt;membership query failures&lt;/li&gt;
&lt;li&gt;unauthorized access attempts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a small change in code, but a meaningful system-design decision.&lt;/p&gt;
&lt;h4 id=&quot;room-lifecycle&quot;&gt;Room lifecycle&lt;/h4&gt;
&lt;p&gt;The room layer already supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;public / authenticated / private access policies&lt;/li&gt;
&lt;li&gt;invitations&lt;/li&gt;
&lt;li&gt;session creation&lt;/li&gt;
&lt;li&gt;participant join/leave&lt;/li&gt;
&lt;li&gt;host controls&lt;/li&gt;
&lt;li&gt;stage requests&lt;/li&gt;
&lt;li&gt;speaker approval/revocation&lt;/li&gt;
&lt;li&gt;participant moderation&lt;/li&gt;
&lt;li&gt;message reporting&lt;/li&gt;
&lt;li&gt;bounded moderation history&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The important point is that the room is treated as a stateful system, not just a visual container.&lt;/p&gt;
&lt;h4 id=&quot;frontend-state-hardening&quot;&gt;Frontend state hardening&lt;/h4&gt;
&lt;p&gt;I’ve also been cleaning up React lifecycle behavior.&lt;/p&gt;
&lt;p&gt;Recent fixes include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reloading room state when authentication changes&lt;/li&gt;
&lt;li&gt;stabilizing async data loading with &lt;code&gt;useCallback&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;tightening effect dependencies&lt;/li&gt;
&lt;li&gt;keeping UI state aligned with server-side session state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example recent commits:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0b5e7fa&lt;/code&gt; — refresh Metaverse room when auth state changes&lt;br&gt;
&lt;code&gt;fa94a88&lt;/code&gt; — stabilize frontend loading callback&lt;br&gt;
&lt;code&gt;0347827&lt;/code&gt; — make paid analytics explicit instead of automatic&lt;/p&gt;
&lt;h4 id=&quot;auth-and-observability-testing&quot;&gt;Auth and observability testing&lt;/h4&gt;
&lt;p&gt;I also aligned tests around:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;authenticated Metaverse flows&lt;/li&gt;
&lt;li&gt;server-backed sessions&lt;/li&gt;
&lt;li&gt;auth expiry&lt;/li&gt;
&lt;li&gt;same-origin authenticated requests&lt;/li&gt;
&lt;li&gt;health endpoint behavior&lt;/li&gt;
&lt;li&gt;storage failure semantics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A previous checkpoint also hardened realtime membership authorization and added test coverage around failure states:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0af298e&lt;/code&gt; — fail-closed realtime auth&lt;br&gt;
&lt;code&gt;9ac9f53&lt;/code&gt; — auth session test alignment&lt;br&gt;
&lt;code&gt;5bf365d&lt;/code&gt; — auth + observability test alignment&lt;/p&gt;
&lt;h4 id=&quot;deployment-discipline&quot;&gt;Deployment discipline&lt;/h4&gt;
&lt;p&gt;Another part of the work was CI/CD hygiene.&lt;/p&gt;
&lt;p&gt;I removed automatic activation of paid Vercel Web Analytics from the deployment workflow.&lt;/p&gt;
&lt;p&gt;That means deployment stays deterministic and billing-related features remain explicit operational decisions.&lt;/p&gt;
&lt;h4 id=&quot;current-architecture-direction&quot;&gt;Current architecture direction&lt;/h4&gt;
&lt;p&gt;The development sequence I’m following is:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;identity&lt;br&gt;
→ authorization&lt;br&gt;
→ state&lt;br&gt;
→ room lifecycle&lt;br&gt;
→ moderation&lt;br&gt;
→ observability&lt;br&gt;
→ realtime rendering&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is intentional.&lt;/p&gt;
&lt;p&gt;A visually impressive world with weak session handling, unclear authorization or fragile failure modes is still a fragile system.&lt;/p&gt;
&lt;p&gt;So the current priority is to make the platform behavior coherent before connecting the final immersive realtime rendering layer.&lt;/p&gt;
&lt;h4 id=&quot;skills-this-work-is-exercising&quot;&gt;Skills this work is exercising&lt;/h4&gt;
&lt;p&gt;This project has been a practical exercise in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React hooks and state synchronization&lt;/li&gt;
&lt;li&gt;Node.js backend architecture&lt;/li&gt;
&lt;li&gt;REST API design&lt;/li&gt;
&lt;li&gt;realtime authorization&lt;/li&gt;
&lt;li&gt;authentication and session handling&lt;/li&gt;
&lt;li&gt;MongoDB / Mongoose&lt;/li&gt;
&lt;li&gt;fail-closed security patterns&lt;/li&gt;
&lt;li&gt;moderation systems&lt;/li&gt;
&lt;li&gt;automated testing&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;li&gt;CI/CD&lt;/li&gt;
&lt;li&gt;Vercel deployment workflows&lt;/li&gt;
&lt;li&gt;production debugging&lt;/li&gt;
&lt;li&gt;incremental system hardening&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The immersive layer is still the next milestone.&lt;/p&gt;
&lt;p&gt;But the system underneath it is becoming much more real.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build the world after the rules of the world are reliable.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;coderlegion-softwareengineering-react-nodejs-mongodb-realtime-systemdesign-devops-webdevelopment-metaverse-opensource&quot;&gt;CoderLegion #SoftwareEngineering #React #NodeJS #MongoDB #Realtime #SystemDesign #DevOps #WebDevelopment #Metaverse #OpenSource&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30192/coder-legion-dev-log-building-the-myzubster-metaverse-from-the-infrastructure-up</guid>
<pubDate>Thu, 08 Oct 2026 02:32:06 +0000</pubDate>
</item>
<item>
<title>Hardening a Contributor Verification VPS Without Breaking Production</title>
<link>https://coderlegion.com/30124/hardening-a-contributor-verification-vps-without-breaking-production</link>
<description>&lt;h2 id=&quot;building-a-safer-contributor-verification-vps-without-breaking-production&quot;&gt;Building a Safer Contributor Verification VPS Without Breaking Production&lt;/h2&gt;
&lt;p&gt;Hardening a live VPS is rarely about applying a single security checklist.&lt;/p&gt;
&lt;p&gt;The harder problem is this:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;How do you reduce attack surface on a server that is already running production services, contributor verification workloads, Docker environments, databases, reverse proxies, and experimental integrations — without breaking any of them?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That was the situation on a MyZubster VPS we are evolving into a more structured Contributor Verification Node.&lt;/p&gt;
&lt;p&gt;The objective was deliberately conservative:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;identify what is actually running&lt;/li&gt;
&lt;li&gt;reduce unnecessary public binds&lt;/li&gt;
&lt;li&gt;preserve all live workloads&lt;/li&gt;
&lt;li&gt;distinguish repository architecture from runtime architecture&lt;/li&gt;
&lt;li&gt;document every conclusion with reproducible technical evidence&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No mass restarts. No destructive Git operations. No speculative rewiring.&lt;/p&gt;
&lt;p&gt;Just one dependency at a time.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-vps-was-doing-too-many-jobs-at-once&quot;&gt;The VPS was doing too many jobs at once&lt;/h3&gt;
&lt;p&gt;The server was already hosting a mix of services:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;nginx&lt;/li&gt;
&lt;li&gt;MyZubster production gateway&lt;/li&gt;
&lt;li&gt;PM2 applications&lt;/li&gt;
&lt;li&gt;Docker contributor environments&lt;/li&gt;
&lt;li&gt;MongoDB&lt;/li&gt;
&lt;li&gt;Monero services&lt;/li&gt;
&lt;li&gt;Bitcoin verification services&lt;/li&gt;
&lt;li&gt;IPFS&lt;/li&gt;
&lt;li&gt;Qdrant&lt;/li&gt;
&lt;li&gt;Ollama&lt;/li&gt;
&lt;li&gt;n8n&lt;/li&gt;
&lt;li&gt;contributor bridges and pilot services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This creates a common infrastructure trap.&lt;/p&gt;
&lt;p&gt;A port may look unnecessary.&lt;/p&gt;
&lt;p&gt;A process may look duplicated.&lt;/p&gt;
&lt;p&gt;A service may appear safe to restart.&lt;/p&gt;
&lt;p&gt;But if you do not first understand the dependency graph, a cleanup command can become an outage.&lt;/p&gt;
&lt;p&gt;That immediately ruled out broad actions such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pm2 restart all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reset --hard
docker system prune
ufw reset
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The working rule became:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;observe
→ identify ownership
→ map dependencies
→ change one thing
→ verify
→ persist
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;start-from-sockets-not-assumptions&quot;&gt;Start from sockets, not assumptions&lt;/h3&gt;
&lt;p&gt;The first meaningful artifact was a listener inventory.&lt;/p&gt;
&lt;p&gt;Using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ss -lntp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;we classified services into two groups.&lt;/p&gt;
&lt;p&gt;Some were already correctly restricted:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;127.0.0.1:5003   MyZubster Gateway
127.0.0.1:8787   BTC verifier
127.0.0.1:27017  MongoDB
127.0.0.1:6333   Qdrant
127.0.0.1:5678   n8n
127.0.0.1:8092   contributor bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Others were bound globally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;*:5002
0.0.0.0:5005
0.0.0.0:5173
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A global bind does not automatically mean “publicly exposed.”&lt;/p&gt;
&lt;p&gt;A firewall may still block traffic.&lt;/p&gt;
&lt;p&gt;But it does mean the application is willing to accept traffic on every interface, which increases ambiguity and weakens least-privilege design.&lt;/p&gt;
&lt;p&gt;So the next question was not:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Can we close this port?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It was:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Who actually needs to reach this service?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;hardening-port-5173&quot;&gt;Hardening port 5173&lt;/h3&gt;
&lt;p&gt;The frontend process was listening on:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0.0.0.0:5173
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But nginx already proxied to it locally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;location / {
    proxy_pass http://localhost:5173;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That gave us the actual runtime graph:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Internet
   ↓
nginx :443
   ↓
127.0.0.1:5173
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There was no need for the Node process itself to listen on every interface.&lt;/p&gt;
&lt;p&gt;The original code contained:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;server.listen(PORT, '0.0.0.0', () =&amp;gt; {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We changed only the bind address:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;server.listen(PORT, '127.0.0.1', () =&amp;gt; {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then checked syntax:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;node --check server-static.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and restarted only that PM2 process:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pm2 restart myzubster-web
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not the entire PM2 stack.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;verify-before-persisting&quot;&gt;Verify before persisting&lt;/h3&gt;
&lt;p&gt;After the restart:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ss -lntp | grep ':5173'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;showed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;127.0.0.1:5173
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The local service still returned &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then nginx HTTP returned the expected redirect.&lt;/p&gt;
&lt;p&gt;Finally HTTPS was tested directly against the local nginx instance using the real virtual host:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -k -sS -o /dev/null \
  --resolve myzubster.com:443:127.0.0.1 \
  -w 'HTTPS nginx -&amp;gt; %{http_code}\n' \
  https://myzubster.com/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;HTTPS nginx -&amp;gt; 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only after that did we run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pm2 save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This sequencing matters.&lt;/p&gt;
&lt;p&gt;You do not persist a new runtime state before proving that the change is healthy.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;hardening-port-5002&quot;&gt;Hardening port 5002&lt;/h3&gt;
&lt;p&gt;Port &lt;code&gt;5002&lt;/code&gt; belonged to another Node application.&lt;/p&gt;
&lt;p&gt;It was listening globally because the server was started without an explicit host:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;httpServer.listen(PORT, () =&amp;gt; {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before changing it, we checked:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;nginx references&lt;/li&gt;
&lt;li&gt;frontend references&lt;/li&gt;
&lt;li&gt;active TCP connections&lt;/li&gt;
&lt;li&gt;WebSocket usage&lt;/li&gt;
&lt;li&gt;service routes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The frontend already targeted the service internally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const API_TARGET = 'http://127.0.0.1:5002';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There was no nginx route sending external traffic directly to &lt;code&gt;5002&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;No active external dependency was visible.&lt;/p&gt;
&lt;p&gt;So we changed the listener to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;httpServer.listen(PORT, '127.0.0.1', () =&amp;gt; {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Again:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;backup
→ syntax check
→ restart one process
→ verify socket
→ verify health
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The service remained reachable locally.&lt;/p&gt;
&lt;p&gt;The frontend proxy returned a &lt;code&gt;404&lt;/code&gt; for one test route rather than &lt;code&gt;502&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That difference was useful.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;404&lt;/code&gt; meant:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;upstream reachable, route missing&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A &lt;code&gt;502&lt;/code&gt; would have meant:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;upstream unreachable&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That distinction prevented us from treating a valid hardening step as a regression.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-interesting-part-a-broken-taz-contract&quot;&gt;The interesting part: a broken TAZ contract&lt;/h3&gt;
&lt;p&gt;During the dependency review, we found a frontend WebSocket client that constructed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;`${protocol}//${window.location.host}/api/taz/live`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The TAZ interface also expected:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GET /api/taz/dashboard
GET /api/taz/xmr/summary
WS  /api/taz/live
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first this looked like something that might have been affected by our network changes.&lt;/p&gt;
&lt;p&gt;So we tested the production gateway directly.&lt;/p&gt;
&lt;p&gt;The results were:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;404 Cannot GET /api/taz/dashboard
404 Cannot GET /api/taz/xmr/summary
404 Cannot GET /api/taz/live
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was important.&lt;/p&gt;
&lt;p&gt;The feature was already incomplete.&lt;/p&gt;
&lt;p&gt;The hardening work had not broken it.&lt;/p&gt;
&lt;p&gt;That leads to one of the most useful lessons from the whole exercise:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;A failure discovered during infrastructure work is not automatically caused by the infrastructure change.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Always reproduce the behavior against the actual upstream service.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;nginx-was-doing-exactly-what-it-should&quot;&gt;Nginx was doing exactly what it should&lt;/h3&gt;
&lt;p&gt;The production nginx configuration routed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;location / {
    proxy_pass http://localhost:5173;
}

location /api/ {
    proxy_pass http://localhost:5003;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So &lt;code&gt;/api/taz/live&lt;/code&gt; already reached the production gateway.&lt;/p&gt;
&lt;p&gt;The gateway returned the &lt;code&gt;404&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That meant blindly adding WebSocket proxy headers such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection &quot;upgrade&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;would not solve the real problem.&lt;/p&gt;
&lt;p&gt;A reverse proxy cannot invent an application endpoint.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;there-was-already-a-realtime-subsystem&quot;&gt;There was already a realtime subsystem&lt;/h3&gt;
&lt;p&gt;Repository inspection revealed something more interesting.&lt;/p&gt;
&lt;p&gt;MyZubster already contains a realtime implementation based on Socket.IO.&lt;/p&gt;
&lt;p&gt;Its server path is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/realtime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and supporting HTTP routes are mounted under:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/api/realtime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The realtime code exposes an explicit attachment function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;attachRealtimeServer(httpServer)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This changed the architecture question completely.&lt;/p&gt;
&lt;p&gt;Instead of asking:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;How do we build a new WebSocket service for TAZ?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;the better question became:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Can TAZ reuse the existing realtime stack?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Maintaining two separate realtime systems would add unnecessary complexity unless there is a strong protocol reason.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;source-code-and-production-runtime-were-not-the-same-thing&quot;&gt;Source code and production runtime were not the same thing&lt;/h3&gt;
&lt;p&gt;There was another subtle mismatch.&lt;/p&gt;
&lt;p&gt;The repository's normal backend startup path includes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;attachRealtimeServer(server)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the active systemd service starts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/root/myzubster/scripts/start-gateway-systemd.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and that startup path currently creates the server with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const server = app.listen(port, host, ...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;without obviously attaching the realtime server.&lt;/p&gt;
&lt;p&gt;So the repository may support realtime while production does not actually expose it.&lt;/p&gt;
&lt;p&gt;This is a recurring infrastructure lesson:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Repository architecture is not runtime architecture.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A feature being present in source code does not prove that the active service manager actually executes it.&lt;/p&gt;
&lt;p&gt;The authoritative chain is closer to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;systemd / PM2
→ executable
→ startup script
→ imported modules
→ bound socket
→ reverse proxy
→ externally observed response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That chain matters more than a repository grep.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;do-not-fabricate-telemetry-to-make-a-dashboard-look-alive&quot;&gt;Do not fabricate telemetry to make a dashboard look alive&lt;/h3&gt;
&lt;p&gt;The TAZ frontend expects values like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;drinksServed
xmrReceived
robot.status
transactions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;During repository searches, we found unrelated simulation and robot data.&lt;/p&gt;
&lt;p&gt;It would have been technically easy to wire some of that into the dashboard.&lt;/p&gt;
&lt;p&gt;We deliberately did not.&lt;/p&gt;
&lt;p&gt;A live interface must not display simulation values as if they were production evidence.&lt;/p&gt;
&lt;p&gt;Our rule was:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;no authoritative source → no authoritative metric&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A degraded dashboard is more honest than a beautiful dashboard with fabricated telemetry.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-changed&quot;&gt;What changed&lt;/h3&gt;
&lt;p&gt;The hardening work produced two concrete improvements:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;MyZubsterWeb
0.0.0.0:5173
→
127.0.0.1:5173
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Urban Lab
*:5002
→
127.0.0.1:5002
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The main gateway was already:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;127.0.0.1:5003
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nginx HTTPS continued to work.&lt;/p&gt;
&lt;p&gt;Docker verification environments were not disturbed.&lt;/p&gt;
&lt;p&gt;PM2 state was persisted only after verification.&lt;/p&gt;
&lt;p&gt;No destructive Git operation touched the live checkout.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-we-deliberately-did-not-do&quot;&gt;What we deliberately did not do&lt;/h3&gt;
&lt;p&gt;A large part of safe infrastructure work is knowing what not to touch.&lt;/p&gt;
&lt;p&gt;We did not:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;restart every PM2 application&lt;/li&gt;
&lt;li&gt;reset firewall configuration&lt;/li&gt;
&lt;li&gt;expose additional ports&lt;/li&gt;
&lt;li&gt;modify nginx before proving nginx was the problem&lt;/li&gt;
&lt;li&gt;connect TAZ to an unrelated WebSocket server&lt;/li&gt;
&lt;li&gt;feed simulation values into production telemetry&lt;/li&gt;
&lt;li&gt;reset or pull the live Git checkout&lt;/li&gt;
&lt;li&gt;treat code presence as runtime proof&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those non-actions were part of the engineering result.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-contributor-verification-node-model&quot;&gt;The Contributor Verification Node model&lt;/h3&gt;
&lt;p&gt;The broader goal is to evolve the VPS toward a structured Contributor Verification Node.&lt;/p&gt;
&lt;p&gt;That should not mean giving every contributor unrestricted shell access.&lt;/p&gt;
&lt;p&gt;A safer pattern looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;SSH identity
   ↓
dedicated Unix account
   ↓
dedicated workspace
   ↓
container / sandbox
   ↓
explicit mounts
   ↓
explicit network permissions
   ↓
reproducible verifier
   ↓
immutable evidence
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The node should prove that a technical checkpoint can be reproduced.&lt;/p&gt;
&lt;p&gt;It should not silently become a shared production administration box.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;current-state&quot;&gt;Current state&lt;/h3&gt;
&lt;p&gt;At the current checkpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;5173  localhost-only  ✅
5002  localhost-only  ✅
5003  localhost-only  ✅
nginx HTTPS           ✅
PM2 persistence       ✅
TAZ HTTP endpoints    missing
TAZ live WebSocket    missing
realtime subsystem    present in source
production realtime   wiring under review
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not a flashy infrastructure milestone.&lt;/p&gt;
&lt;p&gt;But it is a useful one.&lt;/p&gt;
&lt;p&gt;We now know more precisely:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;what is actually listening&lt;/li&gt;
&lt;li&gt;which services need external exposure&lt;/li&gt;
&lt;li&gt;which assumptions in the frontend are currently false&lt;/li&gt;
&lt;li&gt;which realtime infrastructure already exists&lt;/li&gt;
&lt;li&gt;where production startup differs from repository design&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is enough to make the next change smaller and safer.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-takeaway&quot;&gt;Final takeaway&lt;/h3&gt;
&lt;p&gt;The most valuable part of production hardening is often not the final configuration.&lt;/p&gt;
&lt;p&gt;It is the discipline of reducing uncertainty.&lt;/p&gt;
&lt;p&gt;For every service, ask:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What process owns it?

Who actually needs to reach it?

Does it need to bind globally?

Which reverse proxy reaches it?

What does production really execute?

How will I prove the change did not break anything?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result is not just a smaller attack surface.&lt;/p&gt;
&lt;p&gt;It is a better mental model of the system.&lt;/p&gt;
&lt;p&gt;And once the runtime model is accurate, contributor verification becomes much easier to isolate, reproduce, and trust.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30124/hardening-a-contributor-verification-vps-without-breaking-production</guid>
<pubDate>Wed, 07 Oct 2026 11:17:48 +0000</pubDate>
</item>
<item>
<title>The Developer's Safety Net: Understanding CI/CD Pipelines</title>
<link>https://coderlegion.com/30110/the-developers-safety-net-understanding-ci-cd-pipelines</link>
<description>&lt;p&gt;A &lt;strong&gt;CI/CD pipeline&lt;/strong&gt; (Continuous Integration and Continuous Delivery or Continuous Deployment) is an automated sequence of steps that software code passes through on its way to production. It acts as an automated quality-control assembly line, taking raw code written by a developer, testing it for bugs, compiling it, and safely delivering the finished product to the end users. By removing human error from the deployment process, it ensures software is delivered rapidly, frequently, and reliably.&lt;/p&gt;
&lt;h4 id=&quot;the-analogy-the-modern-car-assembly-line&quot;&gt;The Analogy: The Modern Car Assembly Line&lt;/h4&gt;
&lt;p&gt;Imagine building cars in a high-tech factory. Instead of having a single mechanic manually build a car from scratch, test-drive it on open roads, and paint it by hand, you use a highly optimized, automated assembly line.&lt;/p&gt;
&lt;p&gt;When an engineer designs a new steering wheel (analogous to a developer writing a new feature), they place it on the conveyer belt. Robotic arms instantly run physical stress tests on the wheel to ensure it doesn't break under pressure (automated testing). Next, other machines check if the wheel fits the steering column perfectly without disrupting the dashboard electronics (integration testing). If any test fails, the assembly line immediately halts, alarms ring, and the faulty wheel is rejected before it can ever be installed in a completed car. If everything passes, the car is automatically painted and loaded onto a transport truck, ready for the dealership (automated deployment). &lt;/p&gt;
&lt;h4 id=&quot;why-it-matters-daily-in-the-tech-industry&quot;&gt;Why It Matters Daily in the Tech Industry&lt;/h4&gt;
&lt;p&gt;In the daily grind of software development, engineering teams use CI/CD pipelines to prevent &quot;integration hell&quot;—the nightmarish scenario where multiple developers work on separate features for weeks, only to find that everything breaks when they try to combine their work at the last minute.&lt;/p&gt;
&lt;p&gt;Without a pipeline, a developer might accidentally upload code that contains a syntax error or a broken database query, instantly crashing the live website for thousands of customers. A CI/CD pipeline prevents this disaster by intercepting the code changes. Before the new code ever touches the live production server, the pipeline automatically spins up an isolated test environment, runs hundreds of checks in seconds, and rejects the upload if even a single test fails. This keeps the application online, protects the business from costly downtime, and saves developers from stressful midnight firefighting sessions.&lt;/p&gt;
&lt;h4 id=&quot;putting-it-into-code-an-automated-test-script&quot;&gt;Putting It Into Code: An Automated Test Script&lt;/h4&gt;
&lt;p&gt;At the heart of any CI/CD pipeline is the test phase. Below is a simple Node.js and Express.js test using a popular testing framework called Jest. This script automatically checks if our web server is running and responding correctly. The CI/CD pipeline will execute this code every time a developer tries to push an update.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// server.js - Our simple Express server
const express = require('express');
const app = express();

app.get('/api/health', (req, res) =&amp;gt; {
  res.status(200).json({ status: 'healthy', database: 'connected' });
});

module.exports = app;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// server.test.js - The test script run by our CI/CD pipeline
const request = require('supertest');
const app = require('./server');

describe('GET /api/health', () =&amp;gt; {
  it('should return a 200 OK status and verify backend health', async () =&amp;gt; {
    const response = await request(app).get('/api/health');
    
    // Verify the response code is exactly 200 (Success)
    expect(response.statusCode).toBe(200);
    
    // Verify the payload contains the expected health details
    expect(response.body.status).toBe('healthy');
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When a developer pushes code, the pipeline automatically runs &lt;code&gt;npm test&lt;/code&gt;. If our code fails to return a 200 status code, the pipeline aborts, preventing the broken server from being deployed to production.&lt;/p&gt;
&lt;h4 id=&quot;the-takeaway-1&quot;&gt;The Takeaway&lt;/h4&gt;
&lt;p&gt;CI/CD pipelines turn software shipping from a stressful, high-stakes manual event into a quiet, routine background process. By automating the repetitive steps of testing and deployment, they provide developers with an immediate feedback loop and a reliable safety net, allowing businesses to innovate rapidly without sacrificing system stability.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/why-modern-software-never-sleeps.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30110/the-developers-safety-net-understanding-ci-cd-pipelines</guid>
<pubDate>Wed, 07 Oct 2026 10:36:33 +0000</pubDate>
</item>
<item>
<title>From Contributor Research to a Verifiable Open-Source Node</title>
<link>https://coderlegion.com/29840/from-contributor-research-to-a-verifiable-open-source-node</link>
<description>&lt;p&gt;&lt;strong&gt;From Contributor Research to a Verifiable Open-Source Node&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How we built the MyZubster Open Period Care pilot with Node.js, Nginx, deterministic SHA-256, provenance, Evidence Payloads and a public read-only manifest&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Over the last few days I’ve been working on a MyZubster pilot called &lt;strong&gt;Open Period Care&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The original objective was relatively simple: take a contributor’s research package and make it accessible inside the MyZubster ecosystem.&lt;/p&gt;
&lt;p&gt;But the project quickly evolved into something more interesting.&lt;/p&gt;
&lt;p&gt;Instead of treating the contributor’s work as static documentation, we started building a small &lt;strong&gt;verifiable research node&lt;/strong&gt;: a service capable of exposing Knowledge Cards, provenance, evidence references, validation states and deterministic cryptographic fingerprints through a read-only API.&lt;/p&gt;
&lt;p&gt;The result is now a working Node.js service running on a VPS behind Nginx, with the application itself bound only to localhost.&lt;/p&gt;
&lt;p&gt;The architecture is intentionally conservative:&lt;/p&gt;
&lt;p&gt;Contributor research&lt;br&gt;
→ Knowledge Cards&lt;br&gt;
→ provenance&lt;br&gt;
→ evidence references&lt;br&gt;
→ deterministic canonical JSON&lt;br&gt;
→ SHA-256 fingerprints&lt;br&gt;
→ Evidence Payload v1&lt;br&gt;
→ private Node.js service&lt;br&gt;
→ Nginx HTTPS reverse proxy&lt;br&gt;
→ public read-only node manifest&lt;/p&gt;
&lt;p&gt;The goal is not to claim that decentralization, blockchain or hashing magically makes research “true”.&lt;/p&gt;
&lt;p&gt;The goal is much narrower and more useful:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;make the origin, structure, version and state of evidence easier to inspect and reproduce.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;starting-from-real-contributor-work&quot;&gt;Starting from real contributor work&lt;/h3&gt;
&lt;p&gt;The node is based on an actual Open Period Care research contribution produced inside the MyZubster contributor workflow.&lt;/p&gt;
&lt;p&gt;The contribution includes two Knowledge Cards:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;KC-OPC-001&lt;/code&gt; — Multi-Layer Biomaterial Architecture for Reusable Textile Absorbents&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KC-OPC-002&lt;/code&gt; — Contributor Privacy, Data Minimization &amp;amp; Clinical Claim Boundaries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The research package also includes an evidence matrix, scientific and technical references, product requirements and explicit evidence states.&lt;/p&gt;
&lt;p&gt;An important part of the design is that we do not flatten everything into a generic “verified” label.&lt;/p&gt;
&lt;p&gt;Each piece of information maintains an epistemic state such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PROPOSED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SUPPORTED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TESTED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VERIFIED&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the current cards, the state is:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SUPPORTED&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This distinction matters.&lt;/p&gt;
&lt;p&gt;A research document supported by literature is not the same thing as a laboratory-tested prototype.&lt;/p&gt;
&lt;p&gt;A laboratory-tested prototype is not the same thing as a clinically validated product.&lt;/p&gt;
&lt;p&gt;A merged GitHub contribution is evidence of contribution provenance, not automatic scientific certification.&lt;/p&gt;
&lt;p&gt;That boundary is intentionally preserved in the node.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-a-node-instead-of-just-markdown-files-&quot;&gt;Why a node instead of just Markdown files?&lt;/h3&gt;
&lt;p&gt;Markdown documentation is useful, but it becomes harder to integrate when multiple systems need to consume the same information.&lt;/p&gt;
&lt;p&gt;A node gives us a structured API layer.&lt;/p&gt;
&lt;p&gt;For example, the service currently exposes endpoints for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;health status&lt;/li&gt;
&lt;li&gt;service metadata&lt;/li&gt;
&lt;li&gt;Knowledge Cards&lt;/li&gt;
&lt;li&gt;full-text search&lt;/li&gt;
&lt;li&gt;contributor provenance&lt;/li&gt;
&lt;li&gt;evidence references&lt;/li&gt;
&lt;li&gt;content hashes&lt;/li&gt;
&lt;li&gt;Evidence Payloads&lt;/li&gt;
&lt;li&gt;node manifest&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This means another tool does not need to scrape a Markdown file to understand the contribution.&lt;/p&gt;
&lt;p&gt;It can ask directly:&lt;/p&gt;
&lt;p&gt;Which cards exist?&lt;/p&gt;
&lt;p&gt;Who contributed them?&lt;/p&gt;
&lt;p&gt;What evidence state do they have?&lt;/p&gt;
&lt;p&gt;Which sources support them?&lt;/p&gt;
&lt;p&gt;What is the SHA-256 of the current content?&lt;/p&gt;
&lt;p&gt;Has the payload changed?&lt;/p&gt;
&lt;p&gt;Which API routes does this node support?&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;node-js-service-design&quot;&gt;Node.js service design&lt;/h3&gt;
&lt;p&gt;The service is intentionally small.&lt;/p&gt;
&lt;p&gt;It runs on Node.js and currently does not depend on a large external framework.&lt;/p&gt;
&lt;p&gt;The application is bound to:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;127.0.0.1:3101&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;not:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0.0.0.0:3101&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This means the Node.js process itself is not directly reachable from the public Internet.&lt;/p&gt;
&lt;p&gt;That is an important design decision.&lt;/p&gt;
&lt;p&gt;The VPS already hosts other MyZubster infrastructure, so we wanted the Open Period Care pilot to remain isolated from existing services.&lt;/p&gt;
&lt;p&gt;The existing MyZubster bridge continues to use its own local service and port.&lt;/p&gt;
&lt;p&gt;Open Period Care has its own process, its own systemd service, its own data directory and its own application port.&lt;/p&gt;
&lt;p&gt;The application also runs under a dedicated system user rather than root.&lt;/p&gt;
&lt;p&gt;That gives us a clean separation between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the operating system&lt;/li&gt;
&lt;li&gt;Nginx&lt;/li&gt;
&lt;li&gt;the existing MyZubster bridge&lt;/li&gt;
&lt;li&gt;the Open Period Care node&lt;/li&gt;
&lt;li&gt;the research data&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;read-only-by-design&quot;&gt;Read-only by design&lt;/h3&gt;
&lt;p&gt;The current Open Period Care node is deliberately &lt;strong&gt;read-only&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That is not a limitation we are trying to hide.&lt;/p&gt;
&lt;p&gt;It is part of the security model.&lt;/p&gt;
&lt;p&gt;The node is currently meant to publish sanitized evidence and structured knowledge, not accept arbitrary public modifications.&lt;/p&gt;
&lt;p&gt;Nginx only exposes approved read routes.&lt;/p&gt;
&lt;p&gt;Write attempts are blocked at the reverse proxy.&lt;/p&gt;
&lt;p&gt;For example, a POST request to the public API returns:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HTTP 403&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This gives us defense in depth.&lt;/p&gt;
&lt;p&gt;Even if application behavior changes later, the reverse proxy still enforces the public read-only boundary.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;nginx-as-the-public-boundary&quot;&gt;Nginx as the public boundary&lt;/h3&gt;
&lt;p&gt;The Node.js service remains private on localhost.&lt;/p&gt;
&lt;p&gt;Nginx provides the public HTTPS interface.&lt;/p&gt;
&lt;p&gt;The public path is exposed under the existing MyZubster bridge domain:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/open-period-care/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The request flow looks like this:&lt;/p&gt;
&lt;p&gt;Internet&lt;br&gt;
→ HTTPS port 443&lt;br&gt;
→ Nginx&lt;br&gt;
→ &lt;code&gt;/open-period-care/&lt;/code&gt;&lt;br&gt;
→ localhost port 3101&lt;br&gt;
→ Open Period Care Node&lt;/p&gt;
&lt;p&gt;The private application port remains inaccessible directly from the Internet.&lt;/p&gt;
&lt;p&gt;This is much better than simply opening port 3101 in the firewall.&lt;/p&gt;
&lt;p&gt;It lets us centralize TLS, request restrictions, headers and routing in Nginx.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;structured-knowledge-cards&quot;&gt;Structured Knowledge Cards&lt;/h3&gt;
&lt;p&gt;The first card focuses on reusable textile absorbent architecture.&lt;/p&gt;
&lt;p&gt;It contains structured information about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;contact layer materials&lt;/li&gt;
&lt;li&gt;absorbent core materials&lt;/li&gt;
&lt;li&gt;waterproof barrier&lt;/li&gt;
&lt;li&gt;fasteners&lt;/li&gt;
&lt;li&gt;durability targets&lt;/li&gt;
&lt;li&gt;circular-economy targets&lt;/li&gt;
&lt;li&gt;product requirements&lt;/li&gt;
&lt;li&gt;evidence references&lt;/li&gt;
&lt;li&gt;provenance&lt;/li&gt;
&lt;li&gt;validation state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The second card focuses on governance and privacy.&lt;/p&gt;
&lt;p&gt;It contains rules around:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;prohibited sensitive-data collection&lt;/li&gt;
&lt;li&gt;anonymized feedback&lt;/li&gt;
&lt;li&gt;epistemological classification&lt;/li&gt;
&lt;li&gt;clinical claim boundaries&lt;/li&gt;
&lt;li&gt;medical claim restrictions&lt;/li&gt;
&lt;li&gt;privacy-by-design principles&lt;/li&gt;
&lt;li&gt;GDPR-related references&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is important because Open Period Care is health-adjacent.&lt;/p&gt;
&lt;p&gt;The software should help structure evidence without pretending to replace professional, laboratory or regulatory review.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;provenance-as-a-first-class-field&quot;&gt;Provenance as a first-class field&lt;/h3&gt;
&lt;p&gt;One of the main features of the node is contributor provenance.&lt;/p&gt;
&lt;p&gt;Each card contains explicit provenance information such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;contributor identifier&lt;/li&gt;
&lt;li&gt;source contribution&lt;/li&gt;
&lt;li&gt;source branch&lt;/li&gt;
&lt;li&gt;pull request reference&lt;/li&gt;
&lt;li&gt;evidence origin&lt;/li&gt;
&lt;li&gt;merge status&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the current research package, the contributor is:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;khongten124&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;and the source contribution is connected to the MyZubster Open Period Care pull request.&lt;/p&gt;
&lt;p&gt;This means provenance is not an afterthought or a comment hidden inside documentation.&lt;/p&gt;
&lt;p&gt;It is machine-readable.&lt;/p&gt;
&lt;p&gt;Another node or reviewer can retrieve it directly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;deterministic-sha-256&quot;&gt;Deterministic SHA-256&lt;/h3&gt;
&lt;p&gt;One of the most useful technical milestones was implementing deterministic SHA-256 fingerprints.&lt;/p&gt;
&lt;p&gt;Hashing JSON sounds simple until object key ordering becomes relevant.&lt;/p&gt;
&lt;p&gt;Two JSON objects can represent the same information while having different key order.&lt;/p&gt;
&lt;p&gt;If you hash their raw serialized representation, you may get different hashes.&lt;/p&gt;
&lt;p&gt;To avoid that, the node recursively sorts object keys before serialization.&lt;/p&gt;
&lt;p&gt;The process is:&lt;/p&gt;
&lt;p&gt;structured object&lt;br&gt;
→ recursive key sorting&lt;br&gt;
→ canonical JSON&lt;br&gt;
→ SHA-256&lt;/p&gt;
&lt;p&gt;This means repeated requests for the same unchanged card produce the same hash.&lt;/p&gt;
&lt;p&gt;For example, the current hash for &lt;code&gt;KC-OPC-001&lt;/code&gt; is:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;360b98cce04f34f56f479dd51122c8e46abeacb1b467ff602cb5d7c51dc213b0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The second card currently has:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;6a3c346501205328a00f4aa3f5d01e8dc3753d4001a7c7869f58556c22b47e47&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If the underlying card changes, the hash changes.&lt;/p&gt;
&lt;p&gt;That gives us a simple way to detect content drift between nodes or over time.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;evidence-payload-v1&quot;&gt;Evidence Payload v1&lt;/h3&gt;
&lt;p&gt;The next step was to create a portable Evidence Payload.&lt;/p&gt;
&lt;p&gt;Instead of returning only the Knowledge Card, the node can build a normalized payload containing fields such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;schema&quot;: &quot;myzubster.circular-care.evidence.v1&quot;,
  &quot;pilotId&quot;: &quot;open-period-care&quot;,
  &quot;projectId&quot;: &quot;open-period-care-research&quot;,
  &quot;knowledgeCardId&quot;: &quot;KC-OPC-001&quot;,
  &quot;actorRef&quot;: &quot;khongten124&quot;,
  &quot;actorRole&quot;: &quot;researcher&quot;,
  &quot;messageType&quot;: &quot;EVIDENCE_SUBMISSION&quot;,
  &quot;evidenceState&quot;: &quot;SUPPORTED&quot;,
  &quot;privacyClass&quot;: &quot;PUBLIC_SANITIZED&quot;,
  &quot;payloadSha256&quot;: &quot;73d864cd49295408859f197bba7f170b3722a978909a27e90a82d04683eb3d85&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The payload also includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;evidence references&lt;/li&gt;
&lt;li&gt;claim references&lt;/li&gt;
&lt;li&gt;summary notes&lt;/li&gt;
&lt;li&gt;review references&lt;/li&gt;
&lt;li&gt;creation timestamp&lt;/li&gt;
&lt;li&gt;privacy classification&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The payload itself has its own deterministic SHA-256.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;KC-OPC-001&lt;/code&gt;, the current payload hash is:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;73d864cd49295408859f197bba7f170b3722a978909a27e90a82d04683eb3d85&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;KC-OPC-002&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;a576fc507109a2db4507d8abdafbce6dac151bb5b57cebccba8a331dcf1f5ae7&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Repeated requests produce the same hashes as long as the payload content remains unchanged.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-hash-the-payload-separately-from-the-card-&quot;&gt;Why hash the payload separately from the card?&lt;/h3&gt;
&lt;p&gt;The Knowledge Card and the Evidence Payload serve different purposes.&lt;/p&gt;
&lt;p&gt;The card represents structured knowledge.&lt;/p&gt;
&lt;p&gt;The payload represents a transportable evidence record.&lt;/p&gt;
&lt;p&gt;A future node may not need every internal field from the original Knowledge Card.&lt;/p&gt;
&lt;p&gt;It may only need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;project&lt;/li&gt;
&lt;li&gt;contributor&lt;/li&gt;
&lt;li&gt;evidence state&lt;/li&gt;
&lt;li&gt;evidence references&lt;/li&gt;
&lt;li&gt;claims&lt;/li&gt;
&lt;li&gt;privacy class&lt;/li&gt;
&lt;li&gt;hash&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By hashing both independently, we can verify:&lt;/p&gt;
&lt;p&gt;the exact Knowledge Card content&lt;/p&gt;
&lt;p&gt;and&lt;/p&gt;
&lt;p&gt;the normalized evidence record derived from it.&lt;/p&gt;
&lt;p&gt;This creates a cleaner interoperability boundary.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-public-node-manifest&quot;&gt;A public node manifest&lt;/h3&gt;
&lt;p&gt;The latest milestone was adding a public node manifest.&lt;/p&gt;
&lt;p&gt;The manifest describes the node itself.&lt;/p&gt;
&lt;p&gt;It includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;node identifier&lt;/li&gt;
&lt;li&gt;service name&lt;/li&gt;
&lt;li&gt;version&lt;/li&gt;
&lt;li&gt;operating mode&lt;/li&gt;
&lt;li&gt;scope&lt;/li&gt;
&lt;li&gt;evidence schema&lt;/li&gt;
&lt;li&gt;privacy class&lt;/li&gt;
&lt;li&gt;public base path&lt;/li&gt;
&lt;li&gt;available endpoints&lt;/li&gt;
&lt;li&gt;Knowledge Cards&lt;/li&gt;
&lt;li&gt;card hashes&lt;/li&gt;
&lt;li&gt;payload hashes&lt;/li&gt;
&lt;li&gt;contributor references&lt;/li&gt;
&lt;li&gt;pull request references&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The manifest schema is currently:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;myzubster.node.manifest.v1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The service version is:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0.9.0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The node mode is:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;read-only&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The manifest itself is also hashed.&lt;/p&gt;
&lt;p&gt;Current manifest SHA-256:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;a173b0c4253643b68f692fb35b0cf49ee60f41c9c4d741de389fc26ecc52c43d&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;We tested the manifest locally, through HTTPS and through repeated requests.&lt;/p&gt;
&lt;p&gt;The hash remained identical.&lt;/p&gt;
&lt;p&gt;That means the manifest can act as a compact fingerprint of what the node currently publishes.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-the-manifest-matters&quot;&gt;Why the manifest matters&lt;/h3&gt;
&lt;p&gt;Imagine two independent nodes.&lt;/p&gt;
&lt;p&gt;Node A publishes an Open Period Care manifest.&lt;/p&gt;
&lt;p&gt;Node B retrieves it.&lt;/p&gt;
&lt;p&gt;Node B can check:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;service version&lt;/li&gt;
&lt;li&gt;supported schema&lt;/li&gt;
&lt;li&gt;available cards&lt;/li&gt;
&lt;li&gt;expected card hashes&lt;/li&gt;
&lt;li&gt;expected payload hashes&lt;/li&gt;
&lt;li&gt;manifest hash&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If one of those values changes, Node B can detect that the state changed.&lt;/p&gt;
&lt;p&gt;This does not prove the scientific truth of the content.&lt;/p&gt;
&lt;p&gt;It proves something more limited:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;the published digital state is identifiable and reproducible.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That is a useful primitive for decentralized research infrastructure.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;privacy-boundaries&quot;&gt;Privacy boundaries&lt;/h3&gt;
&lt;p&gt;Privacy is especially important for this pilot.&lt;/p&gt;
&lt;p&gt;The node is not designed to collect menstrual histories, clinical records or intimate participant data.&lt;/p&gt;
&lt;p&gt;The current payload classification is:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PUBLIC_SANITIZED&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The research workflow explicitly separates product and usability evidence from identifiable personal health information.&lt;/p&gt;
&lt;p&gt;The governance Knowledge Card prohibits or restricts collection of information such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;clinical diagnoses&lt;/li&gt;
&lt;li&gt;medication histories&lt;/li&gt;
&lt;li&gt;intimate photographs&lt;/li&gt;
&lt;li&gt;personal identifiers&lt;/li&gt;
&lt;li&gt;IP or location metadata where inappropriate&lt;/li&gt;
&lt;li&gt;unsupported medical claims&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal is to make research and product requirements traceable without turning a public repository into a health-data database.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;scientific-boundaries&quot;&gt;Scientific boundaries&lt;/h3&gt;
&lt;p&gt;Another key principle is that a cryptographic hash is not scientific validation.&lt;/p&gt;
&lt;p&gt;A SHA-256 digest proves that some bytes match some other bytes.&lt;/p&gt;
&lt;p&gt;It does not prove:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;material safety&lt;/li&gt;
&lt;li&gt;product effectiveness&lt;/li&gt;
&lt;li&gt;laboratory performance&lt;/li&gt;
&lt;li&gt;regulatory compliance&lt;/li&gt;
&lt;li&gt;clinical validity&lt;/li&gt;
&lt;li&gt;physical recycling&lt;/li&gt;
&lt;li&gt;professional credentials&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Likewise, Git provenance proves that a contribution exists in a repository history.&lt;/p&gt;
&lt;p&gt;It does not automatically certify the contributor as a medical or scientific authority.&lt;/p&gt;
&lt;p&gt;The current system therefore keeps explicit states such as:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SUPPORTED&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;rather than automatically promoting research to:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;VERIFIED&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Future transitions to &lt;code&gt;TESTED&lt;/code&gt; or &lt;code&gt;VERIFIED&lt;/code&gt; should require actual evidence supporting those states.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;reproducibility-over-hype&quot;&gt;Reproducibility over hype&lt;/h3&gt;
&lt;p&gt;One of the goals of MyZubster is to experiment with decentralized and contributor-driven infrastructure.&lt;/p&gt;
&lt;p&gt;But I think it is important not to confuse decentralization with trustlessness or automatic truth.&lt;/p&gt;
&lt;p&gt;For this pilot, the practical focus has been:&lt;/p&gt;
&lt;p&gt;Can another person inspect the source?&lt;/p&gt;
&lt;p&gt;Can another node retrieve the same payload?&lt;/p&gt;
&lt;p&gt;Can we reproduce the same hash?&lt;/p&gt;
&lt;p&gt;Can we identify who submitted the research?&lt;/p&gt;
&lt;p&gt;Can we distinguish literature support from laboratory testing?&lt;/p&gt;
&lt;p&gt;Can we expose data without exposing the VPS?&lt;/p&gt;
&lt;p&gt;Can we keep sensitive data out of the public evidence layer?&lt;/p&gt;
&lt;p&gt;Those questions are more useful to me than simply saying “this is decentralized”.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-worked-well&quot;&gt;What worked well&lt;/h3&gt;
&lt;p&gt;A few architectural decisions have worked well so far.&lt;/p&gt;
&lt;p&gt;First, keeping the Node.js process private on localhost made deployment much safer.&lt;/p&gt;
&lt;p&gt;Second, using Nginx as the public boundary let us expose only a controlled read-only interface.&lt;/p&gt;
&lt;p&gt;Third, separating data from application code made it much easier to update Knowledge Cards without changing server logic.&lt;/p&gt;
&lt;p&gt;Fourth, deterministic canonical JSON gave us stable hashes.&lt;/p&gt;
&lt;p&gt;Fifth, separating card hashes, payload hashes and manifest hashes created clear integrity layers.&lt;/p&gt;
&lt;p&gt;And finally, keeping explicit evidence states prevents the API from overstating the certainty of the underlying research.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;current-architecture&quot;&gt;Current architecture&lt;/h3&gt;
&lt;p&gt;The current Open Period Care architecture can be summarized like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Contributor
    │
    ▼
Research contribution
    │
    ▼
Knowledge Cards
    │
    ├── provenance
    ├── evidence references
    ├── validation state
    └── requirements
    │
    ▼
Canonical JSON
    │
    ▼
SHA-256
    │
    ├── Card hash
    └── Evidence Payload hash
    │
    ▼
Node.js service
127.0.0.1:3101
    │
    ▼
Nginx reverse proxy
    │
    ▼
HTTPS read-only API
    │
    ▼
Public Node Manifest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application port remains private.&lt;/p&gt;
&lt;p&gt;Only the Nginx HTTPS endpoint is public.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-comes-next&quot;&gt;What comes next&lt;/h3&gt;
&lt;p&gt;The next step is interoperability.&lt;/p&gt;
&lt;p&gt;The idea is to let independent contributors or reviewers run their own nodes and exchange sanitized evidence records through a documented interface.&lt;/p&gt;
&lt;p&gt;A possible future workflow looks like:&lt;/p&gt;
&lt;p&gt;Researcher node&lt;br&gt;
→ Evidence Payload&lt;br&gt;
→ MyZubster bridge&lt;br&gt;
→ reviewer node&lt;br&gt;
→ review response&lt;br&gt;
→ updated evidence state&lt;br&gt;
→ Knowledge Graph&lt;/p&gt;
&lt;p&gt;Possible message types could include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EVIDENCE_SUBMISSION&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REVIEW_REQUEST&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REVIEW_RESPONSE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SOURCE_CHALLENGE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CORRECTION&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal would be to preserve provenance and state transitions without giving external contributors access to the VPS itself.&lt;/p&gt;
&lt;p&gt;No SSH credentials.&lt;/p&gt;
&lt;p&gt;No server passwords.&lt;/p&gt;
&lt;p&gt;No database credentials.&lt;/p&gt;
&lt;p&gt;No wallet keys.&lt;/p&gt;
&lt;p&gt;No health records.&lt;/p&gt;
&lt;p&gt;Only sanitized evidence payloads.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;possible-future-attestations&quot;&gt;Possible future attestations&lt;/h3&gt;
&lt;p&gt;The deterministic hashes also make optional external attestations possible.&lt;/p&gt;
&lt;p&gt;For example, a payload hash could eventually be anchored to an external timestamping or blockchain system.&lt;/p&gt;
&lt;p&gt;But the distinction must remain clear:&lt;/p&gt;
&lt;p&gt;A blockchain anchor can prove that a hash existed at a certain point.&lt;/p&gt;
&lt;p&gt;It cannot prove that the associated physical event actually happened.&lt;/p&gt;
&lt;p&gt;If someone claims that a material was recycled, tested or clinically validated, the hash alone is not evidence of that physical claim.&lt;/p&gt;
&lt;p&gt;The supporting evidence still needs to exist independently.&lt;/p&gt;
&lt;p&gt;That distinction is critical.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lessons-from-this-build&quot;&gt;Lessons from this build&lt;/h3&gt;
&lt;p&gt;This pilot reinforced a few ideas for me.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Provenance should be structured, not implied.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If provenance matters, put it into the data model.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Evidence states should be explicit.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Do not make consumers guess whether something is proposed, supported, tested or verified.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Cryptographic integrity and scientific validity are different concepts.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Both matter, but they solve different problems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Public APIs should expose less than the internal infrastructure.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The VPS does not need to become part of the contributor interface.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Read-only is a useful first interoperability milestone.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Before accepting external writes, prove that sanitized retrieval and validation work correctly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. Small services can be easier to reason about.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For this pilot, a lightweight Node.js service behind Nginx has been enough to validate the architecture without adding unnecessary complexity.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;technology-used&quot;&gt;Technology used&lt;/h3&gt;
&lt;p&gt;The current implementation uses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;native Node.js HTTP APIs&lt;/li&gt;
&lt;li&gt;Node.js &lt;code&gt;crypto&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;JSON-based Knowledge Cards&lt;/li&gt;
&lt;li&gt;deterministic recursive JSON key sorting&lt;/li&gt;
&lt;li&gt;SHA-256&lt;/li&gt;
&lt;li&gt;systemd&lt;/li&gt;
&lt;li&gt;Nginx&lt;/li&gt;
&lt;li&gt;HTTPS&lt;/li&gt;
&lt;li&gt;Linux VPS&lt;/li&gt;
&lt;li&gt;GitHub contribution provenance&lt;/li&gt;
&lt;li&gt;MyZubster Evidence Payload v1&lt;/li&gt;
&lt;li&gt;MyZubster Node Manifest v1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No public database is required for the current read-only pilot.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;current-milestone&quot;&gt;Current milestone&lt;/h3&gt;
&lt;p&gt;At the current milestone, the node can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;serve real contributor Knowledge Cards&lt;/li&gt;
&lt;li&gt;search card contents recursively&lt;/li&gt;
&lt;li&gt;expose evidence references&lt;/li&gt;
&lt;li&gt;expose contributor provenance&lt;/li&gt;
&lt;li&gt;expose explicit validation states&lt;/li&gt;
&lt;li&gt;calculate deterministic card hashes&lt;/li&gt;
&lt;li&gt;generate deterministic Evidence Payloads&lt;/li&gt;
&lt;li&gt;publish a verifiable node manifest&lt;/li&gt;
&lt;li&gt;operate behind HTTPS&lt;/li&gt;
&lt;li&gt;reject public write requests&lt;/li&gt;
&lt;li&gt;keep its application port private&lt;/li&gt;
&lt;li&gt;keep sensitive health information outside the public data model&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is still a pilot.&lt;/p&gt;
&lt;p&gt;It is not a production clinical platform.&lt;/p&gt;
&lt;p&gt;It is not a medical device.&lt;/p&gt;
&lt;p&gt;It does not establish regulatory compliance.&lt;/p&gt;
&lt;p&gt;It does not prove laboratory validation.&lt;/p&gt;
&lt;p&gt;But it is now a working example of how contributor research can move from static documentation toward a structured, verifiable and interoperable evidence layer.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-i-think-this-is-interesting&quot;&gt;Why I think this is interesting&lt;/h3&gt;
&lt;p&gt;Open-source projects are very good at tracking code provenance.&lt;/p&gt;
&lt;p&gt;Git already gives us commits, authors, diffs and history.&lt;/p&gt;
&lt;p&gt;Research-oriented projects often need something similar for knowledge.&lt;/p&gt;
&lt;p&gt;Where did this claim come from?&lt;/p&gt;
&lt;p&gt;Which source supports it?&lt;/p&gt;
&lt;p&gt;Who structured it?&lt;/p&gt;
&lt;p&gt;Has it been tested?&lt;/p&gt;
&lt;p&gt;Has it been reviewed?&lt;/p&gt;
&lt;p&gt;Did the payload change?&lt;/p&gt;
&lt;p&gt;Can another machine reproduce the same state?&lt;/p&gt;
&lt;p&gt;The Open Period Care node is a small attempt to explore that problem.&lt;/p&gt;
&lt;p&gt;Instead of replacing peer review, laboratories or domain experts, software can make the evidence path easier to inspect.&lt;/p&gt;
&lt;p&gt;That is the direction we are experimenting with in MyZubster.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I’d be very interested in feedback from developers and researchers working on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js infrastructure&lt;/li&gt;
&lt;li&gt;reproducible research&lt;/li&gt;
&lt;li&gt;knowledge graphs&lt;/li&gt;
&lt;li&gt;provenance systems&lt;/li&gt;
&lt;li&gt;decentralized architectures&lt;/li&gt;
&lt;li&gt;content-addressed data&lt;/li&gt;
&lt;li&gt;cryptographic attestations&lt;/li&gt;
&lt;li&gt;DevOps&lt;/li&gt;
&lt;li&gt;open-source hardware&lt;/li&gt;
&lt;li&gt;evidence-based product development&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In particular, I’m interested in how others would design the next interoperability layer between independent contributor and reviewer nodes while keeping the security boundary small.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MyZubster Open Period Care Node — current service version: &lt;code&gt;0.9.0&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Current manifest SHA-256:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;a173b0c4253643b68f692fb35b0cf49ee60f41c9c4d741de389fc26ecc52c43d&lt;/code&gt;&lt;/p&gt;
&lt;h2 id=&quot;opensource-nodejs-devops-sha256-provenance-research-reproducibility-knowledgegraph-cybersecurity-myzubster&quot;&gt;OpenSource #NodeJS #DevOps #SHA256 #Provenance #Research #Reproducibility #KnowledgeGraph #CyberSecurity #MyZubster&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29840/from-contributor-research-to-a-verifiable-open-source-node</guid>
<pubDate>Mon, 05 Oct 2026 10:14:47 +0000</pubDate>
</item>
<item>
<title>From GitHub Contributions to a Public Knowledge Graph: What We're Building with MyZubster</title>
<link>https://coderlegion.com/29561/from-github-contributions-to-a-public-knowledge-graph-what-were-building-with-myzubster</link>
<description>&lt;h2 id=&quot;from-github-contributions-to-a-public-knowledge-graph-what-we-re-building-with-myzubster&quot;&gt;From GitHub Contributions to a Public Knowledge Graph: What We're Building with MyZubster&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;How we connected independent contributors, public development evidence, Knowledge Cards, and an interactive graph — and what we learned along the way.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;By Daniel Ioni | MyZubster | October 2026&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Topics:&lt;/strong&gt; Open Source, JavaScript, Node.js, GitHub, Knowledge Graphs, Developer Tools, AI&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-if-a-github-contribution-could-become-reusable-knowledge-&quot;&gt;What if a GitHub contribution could become reusable knowledge?&lt;/h3&gt;
&lt;p&gt;A pull request contains more than code.&lt;/p&gt;
&lt;p&gt;It can represent hours of debugging, architectural decisions, technical research, new testing methods, and practical experience. Yet much of that knowledge remains scattered across repositories, commits, discussions, and documentation.&lt;/p&gt;
&lt;p&gt;At MyZubster, we have been experimenting with a system that connects these different sources without losing their original context.&lt;/p&gt;
&lt;p&gt;Our idea is relatively simple:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Every documented contribution should have the opportunity to become part of a discoverable knowledge network.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Instead of automatically converting repository activity into claims about someone's abilities, we want to preserve the original evidence and give contributors the option to build their own knowledge records around it.&lt;/p&gt;
&lt;p&gt;This experiment started with our collaboration with Nicola and has progressively expanded to other open-source contributors.&lt;/p&gt;
&lt;p&gt;Our latest development milestone is now available through the public MyZubster Knowledge Graph.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Explore it here:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/knowledge?view=graph&quot;&gt;MyZubster Interactive Knowledge Graph&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In this article, we'll explain our implementation, the problems we encountered, and how other developers can apply similar principles to their own projects.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-starting-with-a-real-contributor-nicola&quot;&gt;1. Starting with a real contributor: Nicola&lt;/h3&gt;
&lt;p&gt;Our initial experiment involved working with Nicola, also known as N4K48.&lt;/p&gt;
&lt;p&gt;We wanted to explore how an independent contributor could document technical work, preserve ownership of their own development environment, and connect their knowledge to a broader ecosystem.&lt;/p&gt;
&lt;p&gt;Our collaboration has involved software development, Docker, GitHub workflows, and the documentation of technical experiments.&lt;/p&gt;
&lt;p&gt;An important part of this process was learning to distinguish between an experiment that works locally and a fully validated integration.&lt;/p&gt;
&lt;p&gt;For example, we prepared experimental infrastructure for a MyZubster Node Bridge, including HTTPS endpoints and Docker-related workflows.&lt;/p&gt;
&lt;p&gt;However, preparing an accessible server is not the same as proving that an independent contributor's machine has completed every stage of a distributed integration.&lt;/p&gt;
&lt;p&gt;This distinction influenced the entire knowledge system.&lt;/p&gt;
&lt;p&gt;We began asking: what exactly does each piece of evidence prove?&lt;/p&gt;
&lt;p&gt;A repository commit might document a code change. A test result can support a claim about specific behavior. A deployment record shows that a deployment occurred.&lt;/p&gt;
&lt;p&gt;None of these records automatically proves every broader claim about a project's maturity.&lt;/p&gt;
&lt;p&gt;We carried this principle into the design of our Knowledge Cards.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Technical references:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster&quot;&gt;MyZubster Main Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/zorgax-profile-builder&quot;&gt;Knowledge Card Builder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/commit/b21e64233e83f0edefda249fc161d863e8b0b64d&quot;&gt;Connecting Nicola's Repository to Knowledge Workflows&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-building-knowledge-cards-with-supporting-evidence&quot;&gt;2. Building Knowledge Cards with supporting evidence&lt;/h3&gt;
&lt;p&gt;A Knowledge Card is a structured record that describes a specific piece of knowledge or experience.&lt;/p&gt;
&lt;p&gt;Instead of storing a generic statement such as &quot;I know backend development,&quot; a card can describe a particular technical activity and link to supporting evidence.&lt;/p&gt;
&lt;p&gt;Consider this illustrative example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;title&quot;: &quot;API Integration Testing&quot;,
  &quot;domain&quot;: &quot;Backend Engineering&quot;,
  &quot;description&quot;: &quot;Developed tests for an API integration&quot;,
  &quot;evidence&quot;: [
    {
      &quot;label&quot;: &quot;Public GitHub Pull Request&quot;,
      &quot;url&quot;: &quot;https://github.com/example/project/pull/123&quot;
    }
  ],
  &quot;verificationNote&quot;: &quot;Public source linked; broader skill claims have not been independently verified.&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a simplified example rather than the complete MyZubster production schema.&lt;/p&gt;
&lt;p&gt;The important elements are the relationship between a claim, its source, and its verification status.&lt;/p&gt;
&lt;p&gt;We also wanted contributors to control publication.&lt;/p&gt;
&lt;p&gt;Our backend therefore distinguishes between private drafts and public Knowledge Cards.&lt;/p&gt;
&lt;p&gt;The public catalog is served through:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET /api/knowledge-evidence/public
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The relevant backend query is based on two explicit conditions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const cards = await KnowledgeDraft.find({
  status: 'published',
  visibility: 'public'
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This design avoids exposing private drafts simply because they exist in the database.&lt;/p&gt;
&lt;p&gt;It also means that creating a draft and publishing a card are separate operations.&lt;/p&gt;
&lt;p&gt;The public catalog is not a complete inventory of every contributor's private knowledge. It contains only records that satisfy the publication requirements.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-turning-individual-records-into-an-interactive-graph&quot;&gt;3. Turning individual records into an interactive graph&lt;/h3&gt;
&lt;p&gt;Once we had a public knowledge catalog, another problem became apparent.&lt;/p&gt;
&lt;p&gt;A list of cards provides useful information, but it does not immediately show how everything connects.&lt;/p&gt;
&lt;p&gt;One contributor might work across several domains. Multiple cards might reference related public evidence. Different contributors might work on separate aspects of the same technical problem.&lt;/p&gt;
&lt;p&gt;We introduced an interactive graph to make those relationships easier to explore.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/1438&quot;&gt;Pull Request #1438&lt;/a&gt; implemented the initial public graph.&lt;/p&gt;
&lt;p&gt;It connects four principal node types:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Knowledge Cards&lt;/li&gt;
&lt;li&gt;Contributors&lt;/li&gt;
&lt;li&gt;Knowledge domains&lt;/li&gt;
&lt;li&gt;Public evidence sources&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Conceptually, the initial graph can be represented as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;              Knowledge Domain
                     |
                     |
Contributor ---- Knowledge Card
                     |
                     |
               Public Source
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The frontend retrieves public cards, builds the node and relationship structures, and renders them as an interactive visualization.&lt;/p&gt;
&lt;p&gt;Users can search, filter, select nodes, and inspect the related information.&lt;/p&gt;
&lt;p&gt;The implementation also preserves verification notes, which help communicate the limitations of the evidence displayed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/knowledge?view=graph&quot;&gt;Interactive Knowledge Graph&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-the-problem-we-discovered-our-contributors-were-missing&quot;&gt;4. The problem we discovered: our contributors were missing&lt;/h3&gt;
&lt;p&gt;After introducing the graph, we discovered an important limitation.&lt;/p&gt;
&lt;p&gt;We already had several contributors with documented public GitHub activity. But many of them did not appear in the Knowledge Graph.&lt;/p&gt;
&lt;p&gt;The reason was straightforward: the graph was built exclusively from personally published Knowledge Cards.&lt;/p&gt;
&lt;p&gt;A public pull request was not enough to create a graph node.&lt;/p&gt;
&lt;p&gt;We had two possible approaches.&lt;/p&gt;
&lt;p&gt;The first was to automatically generate personal Knowledge Cards for contributors based on their repository activity.&lt;/p&gt;
&lt;p&gt;We decided against that approach.&lt;/p&gt;
&lt;p&gt;A public pull request does not necessarily authorize another organization to create a personal knowledge profile, publish private information, or assign qualifications.&lt;/p&gt;
&lt;p&gt;Instead, we implemented a second evidence layer.&lt;/p&gt;
&lt;h4 id=&quot;our-solution-separate-github-contributions-from-personal-knowledge-cards&quot;&gt;Our solution: separate GitHub contributions from personal Knowledge Cards&lt;/h4&gt;
&lt;p&gt;On October 2, 2026, we integrated &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/1453&quot;&gt;Pull Request #1453&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The update adds selected, publicly documented GitHub contributions to the Knowledge Graph.&lt;/p&gt;
&lt;p&gt;These contributions are represented separately from owner-published Knowledge Cards.&lt;/p&gt;
&lt;p&gt;The expanded model now looks conceptually like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                    Knowledge Domain
                      /        \
                     /          \
           Knowledge Card    Public GitHub PR
                 |                |
                 |                |
            Contributor       GitHub Account
                 |
                 |
           Public Evidence
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This separation matters because the two types of records have different meanings.&lt;/p&gt;
&lt;p&gt;A Knowledge Card is a record explicitly published by its owner.&lt;/p&gt;
&lt;p&gt;A GitHub contribution record documents public activity associated with a repository.&lt;/p&gt;
&lt;p&gt;The second record does not automatically become the first.&lt;/p&gt;
&lt;h4 id=&quot;preserving-pull-request-status&quot;&gt;Preserving pull request status&lt;/h4&gt;
&lt;p&gt;We also distinguish between different contribution states.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const workStatus = {
  merged: 'PR integrated',
  submitted: 'PR submitted / not integrated',
  closed_without_merge: 'PR closed without integration'
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This helps prevent a common documentation mistake: presenting submitted or abandoned work as a completed production feature.&lt;/p&gt;
&lt;p&gt;Our initial implementation uses a curated selection of public GitHub contributions. It is not an unrestricted or continuously synchronized GitHub crawler.&lt;/p&gt;
&lt;p&gt;Records must be reviewed and updated as their underlying status changes.&lt;/p&gt;
&lt;p&gt;The change has been merged, and the corresponding Vercel production deployment reached the ready state.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/1453&quot;&gt;View the Implementation — PR #1453&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-real-examples-from-our-open-source-community&quot;&gt;5. Real examples from our open-source community&lt;/h3&gt;
&lt;p&gt;We began the new contribution layer with documented activities from several MyZubster contributors.&lt;/p&gt;
&lt;p&gt;These examples demonstrate how a knowledge graph can connect completely different technical domains.&lt;/p&gt;
&lt;h4 id=&quot;nfc-development-and-documentation&quot;&gt;NFC development and documentation&lt;/h4&gt;
&lt;p&gt;Contributor &lt;strong&gt;@jdjioe5-cpu&lt;/strong&gt; has public contributions related to our Animal Registry.&lt;/p&gt;
&lt;p&gt;The associated work covers browser-based NFC simulation, physical tag design, and API documentation.&lt;/p&gt;
&lt;p&gt;Rather than combining these into one generic skill label, the individual contributions can be represented separately.&lt;/p&gt;
&lt;p&gt;Sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster-animal-registry/pull/9&quot;&gt;Web-Based NFC Tag Simulator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster-animal-registry/pull/10&quot;&gt;NFC Tag Physical Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster-animal-registry/pull/11&quot;&gt;NFC API Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;geospatial-development&quot;&gt;Geospatial development&lt;/h4&gt;
&lt;p&gt;Contributor &lt;strong&gt;@leanworld7-netizen&lt;/strong&gt; has a merged contribution addressing garden geolocation and area-based search.&lt;/p&gt;
&lt;p&gt;This connects backend engineering with geographic data and community-garden applications.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/57&quot;&gt;Garden Geolocation — PR #57&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;frontend-and-telemetry&quot;&gt;Frontend and telemetry&lt;/h4&gt;
&lt;p&gt;Contributor &lt;strong&gt;@laurentketterle-hub&lt;/strong&gt; has a merged contribution involving a Space Station telemetry dashboard.&lt;/p&gt;
&lt;p&gt;It provides an example of how frontend development and monitoring can be represented within a larger knowledge network.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/399&quot;&gt;Telemetry Dashboard — PR #399&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;automated-testing&quot;&gt;Automated testing&lt;/h4&gt;
&lt;p&gt;Contributor &lt;strong&gt;@foxxx009&lt;/strong&gt; has a merged contribution involving automated tests for GitHubMonitor.&lt;/p&gt;
&lt;p&gt;Test automation is an interesting knowledge domain because the associated evidence can include both the implemented tests and their documented execution.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/259&quot;&gt;GitHubMonitor Tests — PR #259&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Other selected records represent work involving security, documentation, and metaverse integration.&lt;/p&gt;
&lt;p&gt;Their individual states are preserved rather than automatically presenting every submission as an accepted result.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-from-knowledge-cards-to-contributor-passports&quot;&gt;6. From Knowledge Cards to Contributor Passports&lt;/h3&gt;
&lt;p&gt;Our next question was how to connect multiple contributions made by the same person.&lt;/p&gt;
&lt;p&gt;That led us to introduce the &lt;strong&gt;Contributor Passport&lt;/strong&gt; framework.&lt;/p&gt;
&lt;p&gt;The idea is to organize evidence-backed knowledge records into a coherent contributor history.&lt;/p&gt;
&lt;p&gt;The conceptual sequence is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Contribution
     |
     v
Public Evidence
     |
     v
Knowledge Card
     |
     v
Contributor Passport
     |
     v
Optional Independent Pilot Node
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We integrated the framework through &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/1445&quot;&gt;PR #1445&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Contributor Passports are not automatic professional certifications.&lt;/p&gt;
&lt;p&gt;They are intended to organize documented contributions, declared knowledge, and associated evidence.&lt;/p&gt;
&lt;p&gt;Participation is also optional.&lt;/p&gt;
&lt;p&gt;A contributor should not be required to publish private personal information or enroll in a separate program merely to participate in open-source development.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/blob/main/docs/CONTRIBUTOR-PILOT-NODES.md&quot;&gt;Read the Contributor Pilot Node Framework&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-independent-nodes-and-the-future-of-distributed-collaboration&quot;&gt;7. Independent nodes and the future of distributed collaboration&lt;/h3&gt;
&lt;p&gt;An interesting extension of the Contributor Passport is the Independent Contributor Pilot Node.&lt;/p&gt;
&lt;p&gt;The concept explores whether contributors can maintain their own development scope and technical identity while connecting relevant knowledge to MyZubster.&lt;/p&gt;
&lt;p&gt;Potential applications include independent software projects, robotics, documentation, testing, and environmental monitoring.&lt;/p&gt;
&lt;p&gt;However, publishing the framework is only one step.&lt;/p&gt;
&lt;p&gt;Actually deploying independent nodes requires reproducible onboarding, clearly defined technical interfaces, and end-to-end validation.&lt;/p&gt;
&lt;p&gt;We are treating these as separate milestones.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-paid-bounties-and-transparent-contributor-agreements&quot;&gt;8. Paid Bounties and transparent contributor agreements&lt;/h3&gt;
&lt;p&gt;We have also introduced a public Paid Bounties page.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/paid-bounties&quot;&gt;Explore MyZubster Paid Bounties&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The system distinguishes proposed tasks from explicitly reserved work.&lt;/p&gt;
&lt;p&gt;This is another example of why precise status information matters.&lt;/p&gt;
&lt;p&gt;An interesting technical proposal should not be confused with a funded contract.&lt;/p&gt;
&lt;p&gt;Likewise, completing a Knowledge Card should not automatically authorize a contributor's enrollment in a separate development program.&lt;/p&gt;
&lt;p&gt;We are experimenting with connecting accepted contributions, documented evidence, optional knowledge records, and separately agreed rewards.&lt;/p&gt;
&lt;p&gt;Our implementation is documented in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/1444&quot;&gt;PR #1444&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;9-what-other-developers-can-learn-from-this-experiment&quot;&gt;9. What other developers can learn from this experiment&lt;/h3&gt;
&lt;p&gt;Our development process has highlighted several principles that are useful beyond MyZubster.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First, keep the original source accessible.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A knowledge claim becomes easier to examine when people can follow its supporting evidence back to an original repository, commit, pull request, or document.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second, model different evidence states explicitly.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Private drafts, published cards, submitted pull requests, merged contributions, and reviewed scientific results should not share a single generic verification label.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Third, separate evidence from interpretation.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A merged pull request provides evidence of integration. Interpreting the skills required to produce it may require additional context.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fourth, respect contributor consent.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Public development activity does not automatically authorize the creation of a personal profile or the disclosure of unrelated personal information.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fifth, test the relationships, not just individual features.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A knowledge system depends on the integrity of the connections between contributors, claims, sources, and publication states.&lt;/p&gt;
&lt;p&gt;Our public contributor graph update introduced regression tests designed to preserve the distinction between GitHub evidence and personally published Knowledge Cards.&lt;/p&gt;
&lt;p&gt;The change passed our CI, security, and evidence-gate workflows before integration.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;10-explore-the-project-and-contribute&quot;&gt;10. Explore the project and contribute&lt;/h3&gt;
&lt;p&gt;We are developing MyZubster openly because we believe the knowledge generated during development should be useful to other people.&lt;/p&gt;
&lt;p&gt;Our next challenges include expanding contributor participation, improving the accessibility of the Knowledge Graph, maintaining accurate public contribution records, and further validating independent development environments.&lt;/p&gt;
&lt;p&gt;We welcome technical reviews, reproducible bug reports, documentation improvements, and contributions that help make the system more transparent.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project links:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/&quot;&gt;MyZubster Official Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/knowledge?view=graph&quot;&gt;Interactive Knowledge Graph&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/zorgax-profile-builder&quot;&gt;Create a Knowledge Card&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem&quot;&gt;MyZubster GitHub Organization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster&quot;&gt;Main Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/paid-bounties&quot;&gt;Public Paid Bounties&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/blob/main/docs/CONTRIBUTOR-PILOT-NODES.md&quot;&gt;Contributor Pilot Node Framework&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you maintain an open-source project, we'd also be interested in discussing how you document the knowledge created by your contributors.&lt;/p&gt;
&lt;p&gt;Do you currently rely on README files, documentation websites, GitHub discussions, contributor profiles, or something else?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Our experiment is built around one idea: preserve the evidence, document the knowledge, and make the connections visible.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;MyZubster is an evolving open-source project. Public GitHub evidence, contributor-declared knowledge, independently verified results, and experimental infrastructure are represented as distinct concepts.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29561/from-github-contributions-to-a-public-knowledge-graph-what-were-building-with-myzubster</guid>
<pubDate>Fri, 02 Oct 2026 17:17:25 +0000</pubDate>
</item>
<item>
<title># Building a Trustworthy Developer Portfolio with GitHub, Node.js, and Structured Evidence</title>
<link>https://coderlegion.com/29488/building-trustworthy-developer-portfolio-with-github-node-js-and-structured-evidence</link>
<description>&lt;h2 id=&quot;building-a-trustworthy-developer-portfolio-with-github-node-js-and-structured-evidence&quot;&gt;Building a Trustworthy Developer Portfolio with GitHub, Node.js, and Structured Evidence&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;How we built Knowledge Cards in MyZubster, integrated GitHub evidence, protected profile README updates, and learned from a real open-source documentation proposal.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;By Daniel Ioni — Founder &amp;amp; Product Builder, MyZubster&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;What does a GitHub profile actually tell us about a developer?&lt;/p&gt;
&lt;p&gt;We can examine repositories, commits, pull requests, documentation, and contribution activity. However, those artifacts don't always explain what problems a developer investigated, what knowledge they acquired, or how their work relates to specific technical subjects.&lt;/p&gt;
&lt;p&gt;At the same time, a traditional portfolio might contain a long list of declared skills without providing any supporting references.&lt;/p&gt;
&lt;p&gt;While developing &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com&quot;&gt;MyZubster&lt;/a&gt;, we started experimenting with a different model.&lt;/p&gt;
&lt;p&gt;Our idea was to connect structured descriptions of technical activities with supporting GitHub references while preserving an important distinction:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The existence of a technical artifact is not the same thing as independent verification of someone's expertise.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This article explains how we implemented that idea using Knowledge Cards, our Zorgax integration, GitHub APIs, and a controlled README synchronization workflow.&lt;/p&gt;
&lt;p&gt;I'll also describe the bugs we encountered and how an unmerged Monero Docs pull request helped us improve our approach to documenting open-source activities.&lt;/p&gt;
&lt;h3 id=&quot;1-designing-the-knowledge-card-model&quot;&gt;1. Designing the Knowledge Card model&lt;/h3&gt;
&lt;p&gt;We wanted to move beyond traditional skill lists.&lt;/p&gt;
&lt;p&gt;Instead of displaying a skill such as &lt;em&gt;Node.js: Expert&lt;/em&gt;, we wanted developers to explain their activities and attach relevant technical references.&lt;/p&gt;
&lt;p&gt;We created Knowledge Cards containing five main elements:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Title:&lt;/strong&gt; What activity or area of knowledge does the card describe?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Domain:&lt;/strong&gt; Which technologies or technical subjects are involved?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Description:&lt;/strong&gt; What does the owner declare they worked on?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evidence:&lt;/strong&gt; Which external sources support or document that description?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verification note:&lt;/strong&gt; What can readers actually establish from the available evidence?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example, one of our published cards describes the development of digital identity and privacy-related modules for MyZubster Space Station.&lt;/p&gt;
&lt;p&gt;It references a GitHub commit containing implementation work, technical documentation, tests, and GitHub Actions configuration.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/knowledge-card?id=6abf35c2ae779390fec8ecfd&quot;&gt;View the Space Station Knowledge Card&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster-space-station/commit/3eb0c7d050428eb63077c8926dcb3ab03d9204d5&quot;&gt;Inspect the associated GitHub commit&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The card distinguishes the owner's description from the public source reference.&lt;/p&gt;
&lt;p&gt;This design also helps readers investigate an activity without interpreting every statement as an independently certified qualification.&lt;/p&gt;
&lt;h3 id=&quot;2-github-url-validation-syntax-is-not-verification&quot;&gt;2. GitHub URL validation: syntax is not verification&lt;/h3&gt;
&lt;p&gt;One of our first practical problems involved a malformed GitHub commit URL.&lt;/p&gt;
&lt;p&gt;We initially added a commit reference manually. It looked correct but was missing the final character of its SHA identifier.&lt;/p&gt;
&lt;p&gt;Consequently, our README importer excluded it.&lt;/p&gt;
&lt;p&gt;We subsequently added the complete reference using the evidence attachment workflow.&lt;/p&gt;
&lt;p&gt;This debugging experience highlighted two different responsibilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Validate that a GitHub URL has an acceptable structure.&lt;/li&gt;
&lt;li&gt;Independently check whether the resource is publicly accessible.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For README import, we use a validation function similar to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const publicGithubSourceUrl = value =&amp;gt; {
  const url = String(value || '').trim();

  return /^https:\/\/github\.com\/[a-z\d_.-]+\/[a-z\d_.-]+\/(?:pull\/[1-9]\d*|commit\/[a-f\d]{40})\/?$/i.test(url)
    ? url
    : '';
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The regular expression accepts GitHub commit URLs with complete 40-character hexadecimal SHA identifiers and GitHub pull request URLs.&lt;/p&gt;
&lt;p&gt;However, this validation does not establish that the commit or pull request exists.&lt;/p&gt;
&lt;p&gt;For that, our backend performs a separate GitHub API request.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GET https://api.github.com/repos/OWNER/REPOSITORY/commits/SHA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If GitHub does not return an accessible resource, our application must not label it as publicly verified.&lt;/p&gt;
&lt;p&gt;This distinction is especially important when working with private repositories, inaccessible forks, and authenticated API sessions.&lt;/p&gt;
&lt;p&gt;A resource visible to one authenticated account might not be publicly accessible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Engineering lesson:&lt;/strong&gt; never confuse format validation with external evidence verification.&lt;/p&gt;
&lt;h3 id=&quot;3-linking-evidence-without-automatically-certifying-expertise&quot;&gt;3. Linking evidence without automatically certifying expertise&lt;/h3&gt;
&lt;p&gt;We implemented a workflow that allows developers to attach public GitHub evidence to existing Knowledge Cards.&lt;/p&gt;
&lt;p&gt;The owner selects a card, provides a supported GitHub URL, and explicitly confirms the operation.&lt;/p&gt;
&lt;p&gt;Our backend checks whether the referenced artifact is publicly accessible before attaching it.&lt;/p&gt;
&lt;p&gt;When the check succeeds, the reference can be described as an accessible public GitHub source.&lt;/p&gt;
&lt;p&gt;However, we deliberately avoid describing the associated activity as independently certified.&lt;/p&gt;
&lt;p&gt;A GitHub commit contains useful information, including its changes, timestamps, commit metadata, and potentially an associated GitHub account.&lt;/p&gt;
&lt;p&gt;That information can support an investigation.&lt;/p&gt;
&lt;p&gt;It does not independently prove every claim a developer makes about their expertise or individual responsibility.&lt;/p&gt;
&lt;p&gt;Our verification notes communicate those limitations directly.&lt;/p&gt;
&lt;p&gt;This approach allows structured portfolios to become more transparent without pretending that an automated system can accurately measure developer competence from GitHub metadata alone.&lt;/p&gt;
&lt;h3 id=&quot;4-documenting-an-unmerged-monero-docs-proposal&quot;&gt;4. Documenting an unmerged Monero Docs proposal&lt;/h3&gt;
&lt;p&gt;The most interesting test of our evidence model involved Monero Docs.&lt;/p&gt;
&lt;p&gt;Through my GitHub account &lt;code&gt;DanielIoni-creator&lt;/code&gt;, I prepared a documentation clarification in a fork of the Monero Docs repository.&lt;/p&gt;
&lt;p&gt;The proposed change involved the &lt;code&gt;pending&lt;/code&gt; parameter of the Wallet RPC method &lt;code&gt;get_transfers&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The original documentation stated:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Include pending transfers.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our proposed clarification was:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Include pending, unconfirmed outgoing transfers.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The objective was to clarify that the parameter concerns pending outgoing transfers that have not yet been confirmed.&lt;/p&gt;
&lt;p&gt;The proposal was submitted to the upstream project through pull request #389.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The pull request was closed without being merged.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Another complication emerged during verification: the original fork and pull request were not accessible without authentication.&lt;/p&gt;
&lt;p&gt;Although our connected GitHub account could retrieve information about the activity, we could not treat that access as equivalent to independent public verification.&lt;/p&gt;
&lt;p&gt;We had two options.&lt;/p&gt;
&lt;p&gt;We could omit the activity entirely because the original evidence wasn't publicly accessible.&lt;/p&gt;
&lt;p&gt;Or we could document it transparently, explicitly explaining the limitations.&lt;/p&gt;
&lt;p&gt;We chose the second approach.&lt;/p&gt;
&lt;p&gt;We prepared a publicly accessible technical document containing the original wording, the proposed modification, references to the original commit and pull request, and a clear statement about the outcome.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/blob/main/docs/contributions/daniel-ioni-monero-docs-wallet-rpc.md&quot;&gt;Read our Monero Docs contribution dossier&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We then created a dedicated Knowledge Card.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/knowledge-card?id=6abf3a8c3258ab27d03c57b4&quot;&gt;View the Monero Knowledge Card&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Crucially, we label the supporting document as &lt;strong&gt;self-declared documentation that has not been independently verified&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We don't describe the proposal as an accepted upstream contribution, an official Monero collaboration, or an independently certified achievement.&lt;/p&gt;
&lt;h4 id=&quot;what-other-developers-can-learn-from-this&quot;&gt;What other developers can learn from this&lt;/h4&gt;
&lt;p&gt;Not every meaningful engineering activity results in merged code.&lt;/p&gt;
&lt;p&gt;Developers investigate problems, prepare proposals, explore unfamiliar codebases, and sometimes submit changes that are not accepted.&lt;/p&gt;
&lt;p&gt;These experiences can be documented and discussed.&lt;/p&gt;
&lt;p&gt;But the outcome is part of the engineering record.&lt;/p&gt;
&lt;p&gt;A transparent portfolio should distinguish a proposed change from a merged change rather than treating both as identical achievements.&lt;/p&gt;
&lt;h3 id=&quot;5-using-separate-evidence-categories&quot;&gt;5. Using separate evidence categories&lt;/h3&gt;
&lt;p&gt;The Monero example revealed a limitation in systems that treat every external URL as equivalent evidence.&lt;/p&gt;
&lt;p&gt;We needed to distinguish between publicly accessible GitHub commits and public documents authored by the card owner.&lt;/p&gt;
&lt;p&gt;These are different categories.&lt;/p&gt;
&lt;p&gt;A publicly accessible commit allows readers to inspect a specific repository change.&lt;/p&gt;
&lt;p&gt;An owner-authored document allows readers to inspect the owner's account of an activity.&lt;/p&gt;
&lt;p&gt;Both can be useful, but they support different conclusions.&lt;/p&gt;
&lt;p&gt;We updated our README importer to track them separately.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let githubSourceCount = 0;
let declaredDocumentCount = 0;

for (const source of evidenceItems) {
  if (source.isDocument) {
    declaredDocumentCount++;
  } else {
    githubSourceCount++;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This simplified illustration reflects the separation implemented in our importer.&lt;/p&gt;
&lt;p&gt;Instead of making every reference look like a verified GitHub contribution, the generated README displays distinct labels.&lt;/p&gt;
&lt;p&gt;This makes the resulting portfolio more informative and reduces the risk of accidentally overstating the evidence.&lt;/p&gt;
&lt;h3 id=&quot;6-safely-synchronizing-a-github-profile-readme&quot;&gt;6. Safely synchronizing a GitHub profile README&lt;/h3&gt;
&lt;p&gt;Once the Knowledge Cards were publicly available, we wanted users to include them in their GitHub profiles.&lt;/p&gt;
&lt;p&gt;However, automatic README modification introduces a serious risk.&lt;/p&gt;
&lt;p&gt;Developers often maintain their GitHub profile descriptions manually. An automated system should not erase existing biographies, project references, or other user-authored content.&lt;/p&gt;
&lt;p&gt;We introduced an explicitly managed section:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- MYZUBSTER-KNOWLEDGE-CARDS:START --&amp;gt;

## Knowledge Cards

&amp;lt;!-- Reviewed Knowledge Card content --&amp;gt;

&amp;lt;!-- MYZUBSTER-KNOWLEDGE-CARDS:END --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;During synchronization, Zorgax prepares the updated Knowledge Card content inside these boundaries while preserving unrelated README sections.&lt;/p&gt;
&lt;p&gt;If the managed block already exists, it is replaced instead of duplicated.&lt;/p&gt;
&lt;p&gt;This makes the operation repeatable.&lt;/p&gt;
&lt;p&gt;But we added another important restriction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If Zorgax cannot retrieve the current GitHub README, the update stops.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;During our testing, we encountered this condition when the existing README was temporarily unavailable to the application.&lt;/p&gt;
&lt;p&gt;The workflow refused to prepare a potentially unsafe update.&lt;/p&gt;
&lt;p&gt;This is preferable to guessing the previous state of an external document.&lt;/p&gt;
&lt;h4 id=&quot;user-controlled-publication&quot;&gt;User-controlled publication&lt;/h4&gt;
&lt;p&gt;We also separated content generation from publication.&lt;/p&gt;
&lt;p&gt;The user must load the existing profile, generate the updated content, inspect the preview, approve it, and authorize the GitHub update.&lt;/p&gt;
&lt;p&gt;Knowledge Card publication does not automatically rewrite the user's GitHub profile.&lt;/p&gt;
&lt;p&gt;This explicit approval process provides an additional layer of control over public identity information.&lt;/p&gt;
&lt;h3 id=&quot;7-idempotence-matters-for-generated-text-too&quot;&gt;7. Idempotence matters for generated text, too&lt;/h3&gt;
&lt;p&gt;Idempotence is usually discussed in relation to APIs, infrastructure, and database operations.&lt;/p&gt;
&lt;p&gt;We discovered that it matters just as much when generating public descriptions.&lt;/p&gt;
&lt;p&gt;Our backend automatically appends a standard verification statement when public GitHub evidence is attached to a card.&lt;/p&gt;
&lt;p&gt;Initially, a normalization function could remove the final punctuation from an existing custom note.&lt;/p&gt;
&lt;p&gt;The result was grammatically incorrect:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;... does not independently certify expertise At least
one public GitHub source has been linked ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We corrected the text-normalization logic so it preserves or restores punctuation.&lt;/p&gt;
&lt;p&gt;However, fixing new updates wasn't enough.&lt;/p&gt;
&lt;p&gt;We also needed to repair notes that had already been published.&lt;/p&gt;
&lt;p&gt;We therefore supported an explicit refresh operation for existing cards.&lt;/p&gt;
&lt;p&gt;One requirement was especially important: refreshing an already corrected note should not append the same verification statement again.&lt;/p&gt;
&lt;p&gt;Conceptually, our regression test checks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const first = refreshVerificationNote(existingNote);

const second = refreshVerificationNote(first);

expect(second).toBe(first);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example illustrates the behavior rather than reproducing the complete backend test suite.&lt;/p&gt;
&lt;p&gt;We deployed the correction, refreshed the affected Knowledge Card, synchronized the GitHub profile again, and verified the resulting README.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Engineering lesson:&lt;/strong&gt; generated text deserves the same attention to repeatability and regression testing as other application data.&lt;/p&gt;
&lt;h3 id=&quot;8-what-we-actually-published&quot;&gt;8. What we actually published&lt;/h3&gt;
&lt;p&gt;After completing the workflow, we verified the public GitHub profile README.&lt;/p&gt;
&lt;p&gt;It contained three published Knowledge Cards, three public GitHub commit references, and one separately labeled public document relating to the Monero Docs proposal.&lt;/p&gt;
&lt;p&gt;It also contained exactly one managed Knowledge Card section.&lt;/p&gt;
&lt;p&gt;Our published cards cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MyZubster ecosystem development and supporting repository activity.&lt;/li&gt;
&lt;li&gt;Space Station security, digital identity, and privacy-related modules.&lt;/li&gt;
&lt;li&gt;A Monero Wallet RPC documentation proposal that was not merged upstream.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/myzubster&quot;&gt;Explore the MyZubster GitHub profile&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The resulting README is not a certificate of technical expertise.&lt;/p&gt;
&lt;p&gt;Instead, it provides a structured overview of activities and references that readers can inspect and evaluate for themselves.&lt;/p&gt;
&lt;h3 id=&quot;9-lessons-for-developers-building-similar-systems&quot;&gt;9. Lessons for developers building similar systems&lt;/h3&gt;
&lt;p&gt;Several principles emerged from this work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep claims and evidence separate.&lt;/strong&gt; A user description and its supporting source have different roles. Your data model should preserve that distinction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Validate both structure and availability.&lt;/strong&gt; A correctly formatted GitHub URL doesn't guarantee that the resource exists or is public.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Don't inflate contribution status.&lt;/strong&gt; A proposed documentation improvement can be worth discussing even when the upstream pull request is closed without a merge.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Preserve existing user content.&lt;/strong&gt; When updating external documents, define the exact area your application controls and stop when the original document cannot be retrieved.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Make publication an explicit decision.&lt;/strong&gt; AI-assisted generation and external publication should be separate operations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test repeated operations.&lt;/strong&gt; Re-running synchronization or refreshing verification text should not introduce duplicate sections or statements.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Treat documentation quality as engineering quality.&lt;/strong&gt; Small punctuation errors can undermine the clarity of public verification disclosures.&lt;/p&gt;
&lt;h3 id=&quot;10-what-s-next-for-myzubster-&quot;&gt;10. What's next for MyZubster?&lt;/h3&gt;
&lt;p&gt;Our Knowledge Card implementation is an evolving foundation.&lt;/p&gt;
&lt;p&gt;We're continuing to explore ways to improve source management, clean up outdated evidence, clarify verification states, and connect structured technical activities with other parts of the MyZubster ecosystem.&lt;/p&gt;
&lt;p&gt;The broader goal is to make documented knowledge easier to discover without introducing misleading automated expertise scores or unsupported claims.&lt;/p&gt;
&lt;p&gt;We believe developers should be able to explain not only what they successfully delivered, but also the investigations, proposals, experiments, and technical decisions behind their work.&lt;/p&gt;
&lt;p&gt;That history becomes especially valuable when the supporting evidence and its limitations are clearly presented.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;This project started as an attempt to improve our GitHub profile.&lt;/p&gt;
&lt;p&gt;It evolved into an exploration of evidence modeling, API validation, safe synchronization, idempotent operations, and transparent open-source documentation.&lt;/p&gt;
&lt;p&gt;The most valuable result isn't simply having three Knowledge Cards displayed on GitHub.&lt;/p&gt;
&lt;p&gt;It's having a workflow that encourages developers to document their activities while preserving the distinction between personal declarations, accessible technical artifacts, and independently established facts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A useful developer portfolio should make technical work discoverable, supporting evidence inspectable, and verification limitations visible.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That's what we're working toward with MyZubster and Zorgax.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;project-references&quot;&gt;Project references&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com&quot;&gt;MyZubster&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster&quot;&gt;MyZubster open-source repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/zorgax&quot;&gt;Zorgax&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/myzubster&quot;&gt;GitHub profile&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster-space-station/commit/3eb0c7d050428eb63077c8926dcb3ab03d9204d5&quot;&gt;Space Station security commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/blob/main/docs/contributions/daniel-ioni-monero-docs-wallet-rpc.md&quot;&gt;Monero Docs contribution dossier&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Author:&lt;/strong&gt; Daniel Ioni&lt;br&gt;
&lt;strong&gt;Project:&lt;/strong&gt; MyZubster&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The Monero Docs proposal discussed in this article was not merged into the upstream project. The linked public dossier records the proposal and explicitly discloses its verification limitations.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29488/building-trustworthy-developer-portfolio-with-github-node-js-and-structured-evidence</guid>
<pubDate>Fri, 02 Oct 2026 05:25:24 +0000</pubDate>
</item>
<item>
<title>Engineering an Evidence-First Knowledge Graph: From GitHub Artifacts to On-Chain Proofs in MyZubster</title>
<link>https://coderlegion.com/28984/engineering-evidence-first-knowledge-graph-from-github-artifacts-chain-proofs-myzubster</link>
<description>&lt;p&gt;Certo. Per CoderLegion lo farei più tecnico del post DEV, con più attenzione ad architettura, data provenance, canonicalizzazione, hashing e al motivo per cui dopo il test con Nicola stiamo valutando standard esistenti invece di continuare ad aggiungere smart contract custom.&lt;br&gt;
Engineering an Evidence-First Knowledge Graph: From GitHub Artifacts to On-Chain Proofs in MyZubster&lt;br&gt;
Building and testing a complete provenance chain with a real contributor, canonical payloads, SHA-256, Ethereum Sepolia, RAG, and a Knowledge Graph&lt;br&gt;
Tonight we ran one of the most complete end-to-end experiments so far on MyZubster.&lt;br&gt;
The test involved a real contributor, Nicola / N4K48, rather than synthetic fixtures.&lt;br&gt;
What started as a relatively simple test of the MyZubster Knowledge Profile evolved into a much larger architecture experiment involving:&lt;br&gt;
Authentication&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Knowledge Profile&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Private Knowledge Cards&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Publication&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Real software contribution&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GitHub evidence&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RAG / Qdrant&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Knowledge Graph&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Canonical payload&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SHA-256&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ethereum Sepolia&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Publicly inspectable provenance&lt;/p&gt;
&lt;p&gt;The most important result was not the blockchain transaction.&lt;br&gt;
It was discovering where the boundaries of the MyZubster architecture should probably be.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The problem we are trying to solve&lt;br&gt;
Most professional profiles fundamentally contain claims.&lt;br&gt;
For example:&lt;br&gt;
{&lt;br&gt;
  &quot;person&quot;: &quot;N4K48&quot;,&lt;br&gt;
  &quot;skill&quot;: &quot;Docker&quot;&lt;br&gt;
}&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This tells us what somebody says about themselves.&lt;br&gt;
It tells us very little about why another person should trust that statement.&lt;br&gt;
We wanted to experiment with a different model:&lt;br&gt;
Person&lt;br&gt;
 ↓&lt;br&gt;
Claim&lt;br&gt;
 ↓&lt;br&gt;
Activity&lt;br&gt;
 ↓&lt;br&gt;
Artifact&lt;br&gt;
 ↓&lt;br&gt;
Evidence&lt;br&gt;
 ↓&lt;br&gt;
Cryptographic commitment&lt;br&gt;
 ↓&lt;br&gt;
Attestation&lt;/p&gt;
&lt;p&gt;The objective is not to magically transform a claim into truth.&lt;br&gt;
The objective is to make its provenance inspectable.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Testing with a real contributor&lt;br&gt;
Nicola created a MyZubster Knowledge Profile representing different parts of his real activity.&lt;br&gt;
We used three Knowledge Cards during the experiment:&lt;br&gt;
N4K48&lt;br&gt;
 │&lt;br&gt;
 ├── Truck driving and transport organization&lt;br&gt;
 │&lt;br&gt;
 ├── Docker / AI experiments on myzubster-mvp&lt;br&gt;
 │&lt;br&gt;
 └── Learning and collaboration in MyZubster&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This immediately forced us to distinguish two concepts that are easy to accidentally merge:&lt;br&gt;
CLAIM&lt;/p&gt;
&lt;p&gt;and:&lt;br&gt;
EVIDENCE&lt;/p&gt;
&lt;p&gt;A professional statement is not automatically evidence.&lt;br&gt;
A GitHub commit is not automatically proof of every professional statement either.&lt;br&gt;
Therefore MyZubster needs to preserve the semantic boundary between them.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Authentication failed before cryptography did&lt;br&gt;
The first failures had nothing to do with Ethereum.&lt;br&gt;
Nicola encountered an expired JWT during the onboarding flow.&lt;br&gt;
The UI indicated an authenticated state, but a subsequent request produced:&lt;br&gt;
jwt expired&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Another issue appeared in the conversational onboarding flow: information already supplied by the user was not always being reused correctly.&lt;br&gt;
We also discovered a routing issue where the Knowledge Profile Builder URL displayed the MyZubster homepage instead of the Builder.&lt;br&gt;
These problems were fixed before continuing.&lt;br&gt;
The authentication behavior was changed so that an expired session could trigger re-authentication while preserving the locally prepared onboarding information.&lt;br&gt;
This matters because provenance starts before hashing.&lt;br&gt;
If users lose their work because authentication expires, the cryptographic layer does not save the product.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Private-first Knowledge Cards&lt;br&gt;
The next test covered persistence.&lt;br&gt;
Nicola created three cards and saved them privately.&lt;br&gt;
The tested state transition was approximately:&lt;br&gt;
NEW&lt;br&gt;
 ↓&lt;br&gt;
PRIVATE DRAFT&lt;br&gt;
 ↓&lt;br&gt;
RELOAD&lt;br&gt;
 ↓&lt;br&gt;
EDIT&lt;br&gt;
 ↓&lt;br&gt;
PREVIEW&lt;br&gt;
 ↓&lt;br&gt;
PUBLISH&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The important property here is that creation does not imply publication.&lt;br&gt;
A Knowledge Card can exist privately while the owner reviews:&lt;br&gt;
claims&lt;br&gt;
activities&lt;br&gt;
sources&lt;br&gt;
evidence&lt;br&gt;
verification state&lt;/p&gt;
&lt;p&gt;Only an explicit publication action makes the card public.&lt;br&gt;
Published cards can also be withdrawn before being modified.&lt;br&gt;
That gives us a lifecycle closer to:&lt;br&gt;
DRAFT&lt;br&gt;
  │&lt;br&gt;
  ▼&lt;br&gt;
PUBLIC&lt;br&gt;
  │&lt;br&gt;
  ▼&lt;br&gt;
WITHDRAWN / DRAFT&lt;/p&gt;
&lt;p&gt;rather than treating the knowledge database as an append-only public feed.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Turning software work into evidence&lt;br&gt;
The next step was intentionally practical.&lt;br&gt;
Instead of adding another manually entered skill, Nicola performed a real change in his myzubster-mvp repository.&lt;br&gt;
The work involved:&lt;br&gt;
scripts/ingest_knowledge.py&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;and improvements to the chunking/knowledge-ingestion process.&lt;br&gt;
The resulting test reported:&lt;br&gt;
46 knowledge chunks loaded into Qdrant&lt;br&gt;
1 empty document skipped&lt;/p&gt;
&lt;p&gt;The Git commit associated with that work could then become a source attached to the relevant Knowledge Card.&lt;br&gt;
We now had something structurally different from:&lt;br&gt;
N4K48&lt;br&gt;
 ↓&lt;br&gt;
&quot;knows software development&quot;&lt;/p&gt;
&lt;p&gt;We had:&lt;br&gt;
N4K48&lt;br&gt;
 ↓&lt;br&gt;
Knowledge Card&lt;br&gt;
 ↓&lt;br&gt;
documented software activity&lt;br&gt;
 ↓&lt;br&gt;
GitHub repository&lt;br&gt;
 ↓&lt;br&gt;
specific commit&lt;br&gt;
 ↓&lt;br&gt;
inspectable code change&lt;/p&gt;
&lt;p&gt;This is an important distinction.&lt;br&gt;
MyZubster does not need to decide that the commit proves a particular competence.&lt;br&gt;
It can state something narrower:&lt;br&gt;
This artifact is evidence associated with this activity and this Knowledge Card.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;RAG became part of the evidence trail&lt;br&gt;
The test also exercised the knowledge retrieval stack.&lt;br&gt;
The local project involved:&lt;br&gt;
Docker Compose&lt;br&gt;
   │&lt;br&gt;
   ├── Ollama&lt;br&gt;
   │&lt;br&gt;
   ├── Qdrant&lt;br&gt;
   │&lt;br&gt;
   ├── API&lt;br&gt;
   │&lt;br&gt;
   └── Open WebUI&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;During retrieval testing, the context being returned was insufficient.&lt;br&gt;
The experiment therefore changed:&lt;br&gt;
AI_CONTEXT_LIMIT=1&lt;/p&gt;
&lt;p&gt;to:&lt;br&gt;
AI_CONTEXT_LIMIT=5&lt;/p&gt;
&lt;p&gt;and repeated the retrieval tests.&lt;br&gt;
That work itself became documentable evidence.&lt;br&gt;
This creates a useful recursive property.&lt;br&gt;
The system can represent knowledge about the process used to improve the system itself:&lt;br&gt;
engineering work&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Git artifact&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Knowledge Card&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Knowledge Graph&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Building the Knowledge Graph&lt;br&gt;
Once the cards were public, we started connecting them into a graph.&lt;br&gt;
Conceptually:&lt;pre&gt;&lt;code&gt;            N4K48
              │
 ┌────────────┼────────────┐
 │            │            │
 ▼            ▼            ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Transport      Learning     Docker / AI&lt;br&gt;
   Card           Card          Card&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                             │
                             ▼
                         GitHub repo
                             │
                             ▼
                           commit
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Sources become traversable relationships instead of text buried inside a profile.&lt;br&gt;
During this phase we found another real-world infrastructure issue.&lt;br&gt;
The visual Knowledge Graph deployment was protected by Vercel, preventing Nicola from accessing it.&lt;br&gt;
After adjusting deployment accessibility, testing continued, including from mobile.&lt;br&gt;
We later corrected another graph/catalog connection that produced a &quot;Catalog unavailable&quot; result.&lt;br&gt;
These may look like small frontend/infrastructure bugs, but they matter for an evidence system.&lt;br&gt;
Evidence that technically exists but cannot be navigated is significantly less useful.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Proof v1: hashing the wrong thing&lt;br&gt;
Then we introduced Ethereum Sepolia.&lt;br&gt;
The first MyZubsterProof experiment anchored a SHA-256 digest related to the Knowledge Card.&lt;br&gt;
However, the digest represented the Knowledge Card URL.&lt;br&gt;
Conceptually:&lt;br&gt;
Knowledge Card URL&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; SHA-256&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; bytes32&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Ethereum Sepolia&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This works cryptographically.&lt;br&gt;
But semantically it proves much less than we wanted.&lt;br&gt;
Suppose:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://example.com/card/123&quot;&gt;https://example.com/card/123&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;continues to exist while the content served at that URL changes.&lt;br&gt;
Hashing the URL does not detect the content change.&lt;br&gt;
The first experiment therefore became useful precisely because it exposed this weakness.&lt;br&gt;
We explicitly documented it rather than treating an on-chain hash as stronger evidence than it actually was.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Proof v2: canonicalize before hashing&lt;br&gt;
Proof v2 changed the architecture.&lt;br&gt;
Instead of hashing a location, we created a deterministic representation of the Knowledge Card itself.&lt;br&gt;
Conceptually:&lt;br&gt;
Knowledge Card&lt;br&gt;
   ↓&lt;br&gt;
canonical representation&lt;br&gt;
   ↓&lt;br&gt;
exact bytes&lt;br&gt;
   ↓&lt;br&gt;
SHA-256&lt;br&gt;
   ↓&lt;br&gt;
bytes32&lt;br&gt;
   ↓&lt;br&gt;
Ethereum Sepolia&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For the tested card, the resulting digest was:&lt;br&gt;
6097e05866bafceec24663d2638cb1dae5742ac78284abbfd45cc9c3b0bfb845&lt;/p&gt;
&lt;p&gt;The corresponding on-chain value was:&lt;br&gt;
0x6097e05866bafceec24663d2638cb1dae5742ac78284abbfd45cc9c3b0bfb845&lt;/p&gt;
&lt;p&gt;Reading knowledgeHash() from the deployed MyZubsterProof contract returned that same value.&lt;br&gt;
The canonical payload and the verification procedure were also committed to the repository.&lt;br&gt;
That gives us a reproducible verification path:&lt;br&gt;
canonical payload&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   │
   ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SHA-256(payload)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   │
   ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;expected digest&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   │
   ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;on-chain knowledgeHash()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   │
   ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;comparison&lt;/p&gt;
&lt;p&gt;If all values match, we can establish that the committed canonical representation corresponds to the digest stored by the contract.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Why canonicalization matters&lt;br&gt;
This is a subtle but critical engineering detail.&lt;br&gt;
Consider two JSON objects:&lt;br&gt;
{&lt;br&gt;
  &quot;name&quot;: &quot;N4K48&quot;,&lt;br&gt;
  &quot;activity&quot;: &quot;Docker&quot;&lt;br&gt;
}&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;and:&lt;br&gt;
{&lt;br&gt;
  &quot;activity&quot;: &quot;Docker&quot;,&lt;br&gt;
  &quot;name&quot;: &quot;N4K48&quot;&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;Semantically, an application may consider them equivalent.&lt;br&gt;
At the byte level, they are different.&lt;br&gt;
And therefore:&lt;br&gt;
SHA256(payloadA) != SHA256(payloadB)&lt;/p&gt;
&lt;p&gt;can occur even when both payloads represent the same logical information.&lt;br&gt;
The same issue appears with:&lt;br&gt;
whitespace&lt;br&gt;
field ordering&lt;br&gt;
Unicode normalization&lt;br&gt;
line endings&lt;br&gt;
optional values&lt;br&gt;
number serialization&lt;br&gt;
dates&lt;/p&gt;
&lt;p&gt;So an evidence system cannot simply say:&lt;br&gt;
hash(JSON.stringify(object))&lt;/p&gt;
&lt;p&gt;and assume the problem is solved forever.&lt;br&gt;
The canonicalization algorithm effectively becomes part of the protocol.&lt;br&gt;
A durable proof therefore needs something like:&lt;br&gt;
payload format&lt;br&gt;
canonicalization version&lt;br&gt;
hash algorithm&lt;br&gt;
digest&lt;br&gt;
attestation reference&lt;/p&gt;
&lt;p&gt;Proof v2 pushed us toward that realization.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Integrity is not truth&lt;br&gt;
This is probably the most important architectural rule from the experiment.&lt;br&gt;
Assume the canonical payload contains:&lt;br&gt;
{&lt;br&gt;
  &quot;claim&quot;: &quot;N4K48 is an expert in Docker&quot;&lt;br&gt;
}&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;and suppose we perfectly canonicalize it, hash it and anchor the digest on Ethereum.&lt;br&gt;
We have demonstrated something about the integrity of the statement.&lt;br&gt;
We have not demonstrated that the statement itself is true.&lt;br&gt;
Formally:&lt;br&gt;
integrity(claim) ≠ truth(claim)&lt;/p&gt;
&lt;p&gt;Likewise:&lt;br&gt;
existence(GitHub commit) ≠ proof(skill)&lt;/p&gt;
&lt;p&gt;and:&lt;br&gt;
blockchain timestamp ≠ professional certification&lt;/p&gt;
&lt;p&gt;Different evidence supports different propositions.&lt;br&gt;
MyZubster therefore needs to model not only evidence, but also what each piece of evidence is capable of establishing.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Provenance as a graph&lt;br&gt;
This leads to a more interesting representation.&lt;br&gt;
Instead of:&lt;br&gt;
Person → Skill&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;we can represent:&lt;br&gt;
Person&lt;br&gt;
  │&lt;br&gt;
  ▼&lt;br&gt;
Claim&lt;br&gt;
  │&lt;br&gt;
  ▼&lt;br&gt;
Activity&lt;br&gt;
  │&lt;br&gt;
  ▼&lt;br&gt;
Knowledge Card&lt;br&gt;
  │&lt;br&gt;
  ├──────────────► GitHub artifact&lt;br&gt;
  │&lt;br&gt;
  ├──────────────► external source&lt;br&gt;
  │&lt;br&gt;
  ▼&lt;br&gt;
Canonical Payload&lt;br&gt;
  │&lt;br&gt;
  ▼&lt;br&gt;
Digest&lt;br&gt;
  │&lt;br&gt;
  ▼&lt;br&gt;
Attestation&lt;/p&gt;
&lt;p&gt;Each edge has semantics.&lt;br&gt;
For example:&lt;br&gt;
Person --DECLARES--&amp;gt; Claim&lt;/p&gt;
&lt;p&gt;Claim --SUPPORTED_BY--&amp;gt; Evidence&lt;/p&gt;
&lt;p&gt;Activity --PRODUCED--&amp;gt; Artifact&lt;/p&gt;
&lt;p&gt;KnowledgeCard --CANONICALIZED_AS--&amp;gt; Payload&lt;/p&gt;
&lt;p&gt;Payload --HASHED_AS--&amp;gt; Digest&lt;/p&gt;
&lt;p&gt;Digest --ATTESTED_BY--&amp;gt; Attestation&lt;/p&gt;
&lt;p&gt;This is much more expressive than simply attaching a blockchain transaction ID to a user profile.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The architectural question that emerged&lt;br&gt;
After completing Proof v2, Nicola compared the experiment with existing systems and standards.&lt;br&gt;
The comparison included:&lt;ul&gt;
&lt;li&gt;Talent Protocol / Builder Score;&lt;/li&gt;
&lt;li&gt;Ethereum Attestation Service;&lt;/li&gt;
&lt;li&gt;cheqd;&lt;/li&gt;
&lt;li&gt;Open Badges 3.0 and Verifiable Credentials.&lt;br&gt;
That comparison raised a very useful question.&lt;br&gt;
Why should MyZubster implement every cryptographic primitive itself?&lt;br&gt;
The ecosystem already contains mature approaches for:&lt;br&gt;
identity&lt;br&gt;
credentials&lt;br&gt;
attestations&lt;br&gt;
reputation&lt;br&gt;
issuers&lt;br&gt;
verification&lt;br&gt;
revocation&lt;br&gt;
cryptographic proofs&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Creating:&lt;br&gt;
MyZubsterProofV3.sol&lt;br&gt;
MyZubsterProofV4.sol&lt;br&gt;
MyZubsterProofV5.sol&lt;/p&gt;
&lt;p&gt;may therefore be less valuable than making MyZubster interoperable with existing primitives.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Separating the layers&lt;br&gt;
The experiment suggests that the architecture could eventually be separated into several layers.&lt;br&gt;
Identity layer&lt;br&gt;
Person&lt;br&gt;
Wallet&lt;br&gt;
GitHub identity&lt;br&gt;
MyZubster account&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Knowledge layer&lt;br&gt;
Knowledge Card&lt;br&gt;
Claim&lt;br&gt;
Activity&lt;br&gt;
Skill&lt;br&gt;
Project&lt;/p&gt;
&lt;p&gt;Evidence layer&lt;br&gt;
GitHub commit&lt;br&gt;
Pull request&lt;br&gt;
Repository&lt;br&gt;
Document&lt;br&gt;
External source&lt;br&gt;
Artifact&lt;/p&gt;
&lt;p&gt;Integrity layer&lt;br&gt;
canonical payload&lt;br&gt;
hash algorithm&lt;br&gt;
digest&lt;br&gt;
version&lt;/p&gt;
&lt;p&gt;Attestation layer&lt;br&gt;
Potentially:&lt;br&gt;
Ethereum Attestation Service&lt;/p&gt;
&lt;p&gt;rather than a growing family of MyZubster-specific contracts.&lt;br&gt;
Credential layer&lt;br&gt;
Potential interoperability with:&lt;br&gt;
W3C Verifiable Credentials&lt;br&gt;
Open Badges&lt;/p&gt;
&lt;p&gt;Relationship layer&lt;br&gt;
This is where MyZubster's Knowledge Graph becomes particularly important.&lt;br&gt;
Person&lt;br&gt;
 ↓&lt;br&gt;
Claim&lt;br&gt;
 ↓&lt;br&gt;
Activity&lt;br&gt;
 ↓&lt;br&gt;
Evidence&lt;br&gt;
 ↓&lt;br&gt;
Artifact&lt;br&gt;
 ↓&lt;br&gt;
Attestation&lt;br&gt;
 ↓&lt;br&gt;
Credential&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;MyZubster may be the graph, not the protocol&lt;br&gt;
This was probably the biggest architectural insight of the evening.&lt;br&gt;
Originally it is tempting to think:&lt;br&gt;
MyZubster&lt;br&gt;
   =&lt;br&gt;
our smart contract&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;But after this experiment, another model looks more interesting:&lt;br&gt;
MyZubster&lt;br&gt;
   =&lt;br&gt;
evidence/provenance graph&lt;br&gt;
+&lt;br&gt;
workflow&lt;br&gt;
+&lt;br&gt;
verification semantics&lt;br&gt;
+&lt;br&gt;
human-readable navigation&lt;/p&gt;
&lt;p&gt;with standards handling lower-level primitives where appropriate.&lt;br&gt;
That means MyZubster could potentially consume:&lt;br&gt;
GitHub evidence&lt;br&gt;
EAS attestations&lt;br&gt;
Verifiable Credentials&lt;br&gt;
Open Badges&lt;br&gt;
blockchain transactions&lt;br&gt;
traditional documents&lt;br&gt;
other public evidence&lt;/p&gt;
&lt;p&gt;without pretending that all evidence has the same meaning.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A possible future architecture&lt;br&gt;
The architecture we are now exploring looks closer to this:&lt;pre&gt;&lt;code&gt;               PERSON
                 │
                 ▼
               CLAIM
                 │
                 ▼
              ACTIVITY
                 │
                 ▼
          KNOWLEDGE CARD
                 │
    ┌────────────┴────────────┐
    │                         │
    ▼                         ▼
EVIDENCE                   ARTIFACT
    │                         │
    └────────────┬────────────┘
                 │
                 ▼
        CANONICAL PAYLOAD
                 │
                 ▼
              SHA-256
                 │
     ┌───────────┴───────────┐
     │                       │
     ▼                       ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;   ATTESTATION              CREDENTIAL&lt;br&gt;
   e.g. EAS                VC / Badge&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     │                       │
     └───────────┬───────────┘
                 ▼
         MYZUBSTER GRAPH
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notice that the blockchain is no longer the center of the system.&lt;br&gt;
The relationships are.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Why this differs from a reputation score&lt;br&gt;
Consider a reputation system that produces:&lt;br&gt;
Score = 87&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That number may be useful for ranking or filtering.&lt;br&gt;
But a provenance graph asks a different question:&lt;br&gt;
Why?&lt;/p&gt;
&lt;p&gt;A MyZubster-style path could eventually allow someone to inspect:&lt;br&gt;
N4K48&lt;br&gt;
 ↓&lt;br&gt;
Docker-related Knowledge Card&lt;br&gt;
 ↓&lt;br&gt;
specific development activity&lt;br&gt;
 ↓&lt;br&gt;
GitHub commit&lt;br&gt;
 ↓&lt;br&gt;
source code&lt;br&gt;
 ↓&lt;br&gt;
canonical representation&lt;br&gt;
 ↓&lt;br&gt;
cryptographic digest&lt;br&gt;
 ↓&lt;br&gt;
attestation&lt;/p&gt;
&lt;p&gt;The user is not forced to trust only the final number.&lt;br&gt;
They can inspect the path that produced the evidence.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Evidence types should not collapse into one trust score&lt;br&gt;
This is another consequence of the experiment.&lt;br&gt;
Imagine three pieces of evidence:&lt;br&gt;
A: self-declared professional experience&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;B: public GitHub commit&lt;/p&gt;
&lt;p&gt;C: credential issued by a university&lt;/p&gt;
&lt;p&gt;All three may belong in the graph.&lt;br&gt;
But they should not automatically become:&lt;br&gt;
A = B = C&lt;/p&gt;
&lt;p&gt;Their semantics are different.&lt;br&gt;
A useful graph therefore needs metadata such as:&lt;br&gt;
evidence type&lt;br&gt;
issuer&lt;br&gt;
subject&lt;br&gt;
source&lt;br&gt;
verification method&lt;br&gt;
verification status&lt;br&gt;
created timestamp&lt;br&gt;
attestation reference&lt;br&gt;
revocation state&lt;/p&gt;
&lt;p&gt;The graph should preserve those differences rather than hide them behind one universal score.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What the experiment actually demonstrated&lt;br&gt;
By the end of the session we had exercised a chain approximately like this:&lt;br&gt;
Real contributor&lt;br&gt;
  ↓&lt;br&gt;
authenticated MyZubster account&lt;br&gt;
  ↓&lt;br&gt;
private Knowledge Cards&lt;br&gt;
  ↓&lt;br&gt;
public Knowledge Cards&lt;br&gt;
  ↓&lt;br&gt;
real software activity&lt;br&gt;
  ↓&lt;br&gt;
GitHub evidence&lt;br&gt;
  ↓&lt;br&gt;
RAG / Qdrant test&lt;br&gt;
  ↓&lt;br&gt;
Knowledge Graph&lt;br&gt;
  ↓&lt;br&gt;
canonical card payload&lt;br&gt;
  ↓&lt;br&gt;
SHA-256&lt;br&gt;
  ↓&lt;br&gt;
Sepolia Proof v2&lt;br&gt;
  ↓&lt;br&gt;
public verification information&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is more useful to us than an isolated smart-contract demo because every layer exposed different failure modes.&lt;br&gt;
We encountered:&lt;br&gt;
expired authentication&lt;br&gt;
routing problems&lt;br&gt;
deployment access restrictions&lt;br&gt;
catalog/navigation problems&lt;br&gt;
retrieval-context limitations&lt;br&gt;
evidence-modeling questions&lt;br&gt;
URL-vs-content hashing semantics&lt;br&gt;
canonicalization requirements&lt;br&gt;
truth-vs-integrity boundaries&lt;/p&gt;
&lt;p&gt;That is exactly what an end-to-end proof of concept should reveal.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What we deliberately did not prove&lt;br&gt;
It is equally important to document what the experiment does not establish.&lt;br&gt;
Proof v2 does not prove that every statement in a Knowledge Card is true.&lt;br&gt;
It does not automatically prove professional competence.&lt;br&gt;
It does not establish that GitHub activity is equivalent to mastery of a skill.&lt;br&gt;
It does not turn blockchain data into a trusted credential issuer.&lt;br&gt;
What it does establish is a mechanism for connecting an exact representation of information with a reproducible digest and a public cryptographic reference.&lt;br&gt;
That is an integrity primitive.&lt;br&gt;
Trust requires additional layers.&lt;/li&gt;
&lt;li&gt;The next engineering experiment&lt;br&gt;
Instead of immediately creating another custom proof contract, the next useful experiment is likely to take the exact Proof v2 data model and map it onto existing standards.&lt;br&gt;
Something like:&lt;br&gt;
Knowledge Card&lt;br&gt;
  ↓&lt;br&gt;
canonical payload&lt;br&gt;
  ↓&lt;br&gt;
current Proof v2&lt;br&gt;
  │&lt;br&gt;
  ├────► EAS schema mapping&lt;br&gt;
  │&lt;br&gt;
  └────► Open Badges / VC mapping&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Then we can compare:&lt;br&gt;
custom MyZubster contract&lt;br&gt;
vs.&lt;br&gt;
standardized attestation&lt;/p&gt;
&lt;p&gt;across:&lt;br&gt;
schema flexibility&lt;br&gt;
revocation&lt;br&gt;
references&lt;br&gt;
issuer semantics&lt;br&gt;
verification&lt;br&gt;
interoperability&lt;br&gt;
developer complexity&lt;br&gt;
gas cost&lt;br&gt;
long-term maintainability&lt;/p&gt;
&lt;p&gt;Only after that comparison should we decide what belongs inside MyZubster itself.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The broader engineering lesson&lt;br&gt;
Tonight's work started with a very ordinary product task:&lt;br&gt;
Can a contributor create a knowledge profile?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It progressed into:&lt;br&gt;
Can the profile contain inspectable evidence?&lt;/p&gt;
&lt;p&gt;Then:&lt;br&gt;
Can that evidence point to real engineering work?&lt;/p&gt;
&lt;p&gt;Then:&lt;br&gt;
Can the exact knowledge representation be cryptographically committed?&lt;/p&gt;
&lt;p&gt;And eventually:&lt;br&gt;
Should MyZubster even own the attestation protocol?&lt;/p&gt;
&lt;p&gt;That progression matters.&lt;br&gt;
A proof of concept should not merely prove that the original architecture works.&lt;br&gt;
Sometimes its most valuable outcome is discovering that part of the original architecture should not exist.&lt;br&gt;
For MyZubster, that may mean allowing established standards to handle credentials and attestations while concentrating our engineering effort on the part we actually care about:&lt;br&gt;
human claim&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;real activity&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;inspectable evidence&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;artifact&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;verification&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;provenance&lt;/p&gt;
&lt;p&gt;and making that entire chain navigable.&lt;br&gt;
That's the direction we're exploring next.&lt;br&gt;
CoderLegion title&lt;br&gt;
Engineering an Evidence-First Knowledge Graph: GitHub, SHA-256, Ethereum and MyZubster&lt;br&gt;
Subtitle&lt;br&gt;
How a real contributor test evolved from Knowledge Cards and RAG into canonical payloads, on-chain proofs, provenance graphs, and a rethink of our attestation architecture.&lt;br&gt;
Short description / excerpt&lt;br&gt;
We tested MyZubster end-to-end with a real contributor: authentication, private and public Knowledge Cards, GitHub evidence, Qdrant/RAG, a Knowledge Graph, canonical payloads, SHA-256 and Ethereum Sepolia. The most important result wasn't the smart contract—it was realizing that MyZubster may be better positioned as an evidence and provenance graph built on interoperable attestation standards.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28984/engineering-evidence-first-knowledge-graph-from-github-artifacts-chain-proofs-myzubster</guid>
<pubDate>Tue, 29 Sep 2026 02:03:45 +0000</pubDate>
</item>
<item>
<title>Building Verifiable Bounty Payments and Revocable Account Sessions at MyZubster Over the latest MyZu</title>
<link>https://coderlegion.com/28935/building-verifiable-bounty-payments-revocable-account-sessions-myzubster-over-latest</link>
<description>&lt;h2 id=&quot;engineering-myzubster-verifiable-crypto-settlements-refresh-token-rotation-revocable-sessions-and-cross-mongoose-runtime-hardening&quot;&gt;Engineering MyZubster: Verifiable Crypto Settlements, Refresh-Token Rotation, Revocable Sessions, and Cross-Mongoose Runtime Hardening&lt;/h2&gt;
&lt;p&gt;MyZubster recently completed a substantial infrastructure iteration spanning two systems that initially appear unrelated: contributor bounty settlement and authentication/session security.&lt;/p&gt;
&lt;p&gt;They share an important engineering requirement:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;external or client-side state must not be treated as authoritative without server-side verification and persistence.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For bounty payments, a blockchain transaction needs to become a verified, replay-safe treasury record.&lt;/p&gt;
&lt;p&gt;For authentication, possession of a cryptographically valid JWT should not permanently imply that a session is still authorized.&lt;/p&gt;
&lt;p&gt;This development cycle introduced infrastructure addressing both problems.&lt;/p&gt;
&lt;p&gt;This article goes through the implementation, security properties, failure modes we discovered during integration testing, and the validation performed before pushing the final changes.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-systems-we-changed&quot;&gt;1. The Systems We Changed&lt;/h3&gt;
&lt;p&gt;The work covered two major areas.&lt;/p&gt;
&lt;h4 id=&quot;treasury-and-bounty-settlement&quot;&gt;Treasury and bounty settlement&lt;/h4&gt;
&lt;p&gt;We implemented infrastructure for recording verified cryptocurrency conversions associated with bounty settlement.&lt;/p&gt;
&lt;p&gt;The flow now supports:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;external transaction
        │
        ▼
blockchain reference
        │
        ▼
conversion verification
        │
        ▼
normalized treasury object
        │
        ▼
dry-run validation
        │
        ▼
idempotency check
        │
        ▼
persistent settlement record
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;authentication-and-account-security&quot;&gt;Authentication and account security&lt;/h4&gt;
&lt;p&gt;MYZ-71 introduced a more substantial authentication lifecycle:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;authentication
      │
      ▼
persistent server session
      │
      ├── short-lived access token
      │
      └── rotating opaque refresh token
                    │
                    ▼
             replay detection
                    │
                    ▼
              session revocation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The frontend then gained automatic renewal, account/device controls, terminal-expiry recovery, and CSRF protection for cookie-authenticated mutations.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-i-verifiable-cryptocurrency-bounty-settlement&quot;&gt;Part I — Verifiable Cryptocurrency Bounty Settlement&lt;/h2&gt;
&lt;h3 id=&quot;2-moving-beyond-a-transaction-was-sent-&quot;&gt;2. Moving Beyond “A Transaction Was Sent”&lt;/h3&gt;
&lt;p&gt;Sending cryptocurrency is only one part of a bounty system.&lt;/p&gt;
&lt;p&gt;From an engineering perspective, the application needs to answer several additional questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why was the transaction made?&lt;/li&gt;
&lt;li&gt;Which asset left the treasury?&lt;/li&gt;
&lt;li&gt;Which asset arrived?&lt;/li&gt;
&lt;li&gt;Which network was used?&lt;/li&gt;
&lt;li&gt;What external provider participated?&lt;/li&gt;
&lt;li&gt;Which blockchain transaction proves the operation?&lt;/li&gt;
&lt;li&gt;Has the conversion been independently verified?&lt;/li&gt;
&lt;li&gt;Has this exact operation already been recorded?&lt;/li&gt;
&lt;li&gt;Can the dashboard consume the result without directly coupling itself to persistence?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the settlement flow implemented during this cycle, the normalized record contains information such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;purpose&quot;: &quot;BOUNTY_SETTLEMENT&quot;,
  &quot;provider&quot;: &quot;SimpleSwap&quot;,
  &quot;source&quot;: {
    &quot;asset&quot;: &quot;BTC&quot;,
    &quot;amount&quot;: &quot;0.000464&quot;,
    &quot;txId&quot;: &quot;2ea0bc209a2d5bcfbf520f830f1a39dee0691e47c461afc316e5aeb2e83cdd2d&quot;
  },
  &quot;target&quot;: {
    &quot;asset&quot;: &quot;ETH&quot;,
    &quot;network&quot;: &quot;ethereum-mainnet&quot;,
    &quot;amount&quot;: &quot;0.014355678390291923&quot;,
    &quot;txId&quot;: &quot;0x62b58e69931ef6046d8f68df39c118fe0b1fc7234b88c780d26a8b53fdcddcc6&quot;
  },
  &quot;state&quot;: &quot;CONVERSION_COMPLETED&quot;,
  &quot;verification&quot;: {
    &quot;status&quot;: &quot;VERIFIED&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This represents the BTC → ETH treasury conversion as application state rather than leaving the operation as an external exchange receipt or manually interpreted transaction history.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-separating-verification-from-persistence&quot;&gt;3. Separating Verification From Persistence&lt;/h3&gt;
&lt;p&gt;A financial script should not immediately mutate production data simply because it successfully parsed its input.&lt;/p&gt;
&lt;p&gt;We therefore implemented a dry-run path.&lt;/p&gt;
&lt;p&gt;The sequence becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;construct record
     │
     ▼
validate input
     │
     ▼
verify external reference
     │
     ▼
print normalized result
     │
     ▼
STOP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The operator sees:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;DRY-RUN OK — no database write performed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only explicit commit mode proceeds to persistence.&lt;/p&gt;
&lt;p&gt;This creates a useful operational boundary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;verification != mutation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That distinction matters for treasury operations because malformed metadata can be caught before it becomes part of the persistent accounting history.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-idempotent-conversion-recording&quot;&gt;4. Idempotent Conversion Recording&lt;/h3&gt;
&lt;p&gt;Financial scripts are frequently rerun.&lt;/p&gt;
&lt;p&gt;A terminal disconnect, uncertain command result, deployment retry, or operator mistake should not produce a second logical settlement record.&lt;/p&gt;
&lt;p&gt;The first committed execution recorded the conversion:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;RECORDED
{
  &quot;id&quot;: &quot;6ab96bcb4c201256912e837a&quot;,
  &quot;purpose&quot;: &quot;BOUNTY_SETTLEMENT&quot;,
  &quot;state&quot;: &quot;CONVERSION_COMPLETED&quot;,
  &quot;verificationStatus&quot;: &quot;VERIFIED&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Repeating the operation returned:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;IDEMPOTENT REPLAY — conversion already recorded
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;instead of creating a duplicate.&lt;/p&gt;
&lt;p&gt;The desired invariant is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;same external conversion
        +
same transaction identity
        =
same internal logical record
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is much safer than relying on the operator to remember whether a previous command completed.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-provider-boundary-for-settlement-observability&quot;&gt;5. Provider Boundary for Settlement Observability&lt;/h3&gt;
&lt;p&gt;The settlement dashboard should not need to understand database internals.&lt;/p&gt;
&lt;p&gt;We introduced a conversion-provider boundary so the dashboard can receive preloaded conversion information.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;buildDashboard({
  conversionProvider: () =&amp;gt; conversion
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This provides two useful properties.&lt;/p&gt;
&lt;p&gt;First, the dashboard can consume treasury state without performing hidden database I/O.&lt;/p&gt;
&lt;p&gt;Second, provider failure does not need to crash the entire dashboard.&lt;/p&gt;
&lt;p&gt;If the repository fails, the conversion layer can instead return:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;configured: false
items: []
reason: conversion repository unavailable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and emit an explicit warning.&lt;/p&gt;
&lt;p&gt;That is deliberate degraded behavior.&lt;/p&gt;
&lt;p&gt;For operational dashboards, an unavailable subsystem should be visible as unavailable — not silently represented as empty valid state and not allowed to crash unrelated observability.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-ii-revocable-authentication-sessions&quot;&gt;Part II — Revocable Authentication Sessions&lt;/h2&gt;
&lt;h3 id=&quot;6-why-jwt-validation-alone-was-not-enough&quot;&gt;6. Why JWT Validation Alone Was Not Enough&lt;/h3&gt;
&lt;p&gt;A signed JWT answers an important question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Was this token issued by a trusted signer and has its cryptographic validity expired?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It does not necessarily answer:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Does the server still want this browser session to exist?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Those are different questions.&lt;/p&gt;
&lt;p&gt;A user may need to revoke a stolen device.&lt;/p&gt;
&lt;p&gt;A refresh token may have been replayed.&lt;/p&gt;
&lt;p&gt;An administrator or security flow may invalidate a session.&lt;/p&gt;
&lt;p&gt;A user may explicitly log out.&lt;/p&gt;
&lt;p&gt;For these cases, authentication needs authoritative server-side state.&lt;/p&gt;
&lt;p&gt;We introduced persistent &lt;code&gt;AuthSession&lt;/code&gt; records.&lt;/p&gt;
&lt;p&gt;The resulting model is conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;User
 │
 ├── AuthSession A
 │      ├── sessionId
 │      ├── expiry
 │      ├── device metadata
 │      ├── lastSeenAt
 │      ├── refreshTokenHash
 │      └── revocation state
 │
 ├── AuthSession B
 │
 └── AuthSession C
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Access JWTs contain a session identifier.&lt;/p&gt;
&lt;p&gt;JWT verification can therefore be followed by server-session validation.&lt;/p&gt;
&lt;p&gt;A valid signature no longer necessarily means an active session.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-refresh-tokens-became-opaque-persistent-and-rotating&quot;&gt;7. Refresh Tokens Became Opaque, Persistent, and Rotating&lt;/h3&gt;
&lt;p&gt;The new refresh credential is an opaque token.&lt;/p&gt;
&lt;p&gt;Its raw value is not persisted.&lt;/p&gt;
&lt;p&gt;Instead, the server stores an HMAC-SHA256 representation.&lt;/p&gt;
&lt;p&gt;The lifecycle looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;refresh token R1
      │
      ▼
hash / identify
      │
      ▼
validate session
      │
      ▼
atomically consume R1
      │
      ▼
issue R2
      │
      ▼
store hash(R2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;R1 is now consumed.&lt;/p&gt;
&lt;p&gt;The server maintains bounded history for previously consumed refresh-token hashes.&lt;/p&gt;
&lt;p&gt;The bounded part matters: security metadata must not create an indefinitely growing session document.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-atomic-rotation&quot;&gt;8. Atomic Rotation&lt;/h3&gt;
&lt;p&gt;Refresh rotation introduces a concurrency problem.&lt;/p&gt;
&lt;p&gt;Imagine two requests attempting to use R1 at nearly the same time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Request A ── R1 ──┐
                  ├── session
Request B ── R1 ──┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A naive implementation could allow both requests to observe R1 as valid before either update becomes visible.&lt;/p&gt;
&lt;p&gt;Rotation therefore needs an atomic state transition.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;UPDATE AuthSession
WHERE
    session = S
AND currentRefreshHash = hash(R1)

SET
    currentRefreshHash = hash(R2)
    consumed += hash(R1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only the request that successfully matches the expected current state should rotate the token.&lt;/p&gt;
&lt;p&gt;This is effectively compare-and-swap behavior applied to authentication state.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;9-refresh-replay-detection&quot;&gt;9. Refresh Replay Detection&lt;/h3&gt;
&lt;p&gt;Single-use refresh tokens provide another security signal.&lt;/p&gt;
&lt;p&gt;If R1 was already consumed and later appears again, something unusual happened.&lt;/p&gt;
&lt;p&gt;Possibilities include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;credential theft;&lt;/li&gt;
&lt;li&gt;duplicated stale browser state;&lt;/li&gt;
&lt;li&gt;client bugs;&lt;/li&gt;
&lt;li&gt;copied authentication material;&lt;/li&gt;
&lt;li&gt;an actual replay attempt.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Our implementation treats reuse as a security event.&lt;/p&gt;
&lt;p&gt;The tested behavior is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;consumed refresh token reused
            │
            ▼
AUTH_REFRESH_REPLAY
            │
            ▼
revoke complete session
            │
            ▼
revokedReason = refresh-token-replay
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The response is therefore stronger than simply rejecting the stale token.&lt;/p&gt;
&lt;p&gt;The associated session itself becomes invalid.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;10-frontend-refresh-deduplication&quot;&gt;10. Frontend Refresh Deduplication&lt;/h3&gt;
&lt;p&gt;Server-side single-use credentials require corresponding client-side concurrency control.&lt;/p&gt;
&lt;p&gt;Suppose a page launches five API requests and the access token expires.&lt;/p&gt;
&lt;p&gt;All five may receive &lt;code&gt;401&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Without coordination:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;401 A → refresh
401 B → refresh
401 C → refresh
401 D → refresh
401 E → refresh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only one of those refresh attempts can legitimately consume the current refresh token.&lt;/p&gt;
&lt;p&gt;So the browser authentication client shares an in-flight refresh promise:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;401 A ─┐
401 B ─┤
401 C ─┼──► one refresh operation
401 D ─┤
401 E ─┘
              │
              ▼
          new access state
              │
              ▼
         retry requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This aligns frontend concurrency semantics with backend single-use token semantics.&lt;/p&gt;
&lt;p&gt;Requests are retried only once.&lt;/p&gt;
&lt;p&gt;The client does not enter an infinite &lt;code&gt;401 → refresh → retry&lt;/code&gt; loop.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-iii-account-security&quot;&gt;Part III — Account Security&lt;/h2&gt;
&lt;h3 id=&quot;11-session-management-api&quot;&gt;11. Session Management API&lt;/h3&gt;
&lt;p&gt;The session architecture is exposed through account-security endpoints:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;POST   /api/auth/refresh
POST   /api/auth/logout
GET    /api/auth/me
GET    /api/auth/me/sessions
DELETE /api/auth/me/sessions/:sessionId
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These endpoints provide the foundation for user-controlled session management.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;12-active-device-management&quot;&gt;12. Active Device Management&lt;/h3&gt;
&lt;p&gt;The frontend now contains an account-security surface.&lt;/p&gt;
&lt;p&gt;Users can inspect session information including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;whether the session is the current one;&lt;/li&gt;
&lt;li&gt;creation time;&lt;/li&gt;
&lt;li&gt;last activity;&lt;/li&gt;
&lt;li&gt;expiry;&lt;/li&gt;
&lt;li&gt;available revocation actions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A remote session can be revoked without destroying every other login.&lt;/p&gt;
&lt;p&gt;The current session can also be terminated.&lt;/p&gt;
&lt;p&gt;When that happens, server-side state is revoked and browser authentication state is cleared.&lt;/p&gt;
&lt;p&gt;This turns logout from a mostly client-side token deletion into an actual server-side authorization transition.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;13-session-metadata-and-privacy&quot;&gt;13. Session Metadata and Privacy&lt;/h3&gt;
&lt;p&gt;Session observability is useful, but it should not require retaining unnecessary sensitive network data.&lt;/p&gt;
&lt;p&gt;The session implementation therefore does not store raw client IP addresses as plain values.&lt;/p&gt;
&lt;p&gt;Instead, IP information can be represented through an HMAC-SHA256 digest.&lt;/p&gt;
&lt;p&gt;This allows certain correlation/security use cases without turning the session collection into a direct historical database of raw IP addresses.&lt;/p&gt;
&lt;p&gt;Session lifetime is also bounded.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-iv-authentication-recovery-in-the-metaverse&quot;&gt;Part IV — Authentication Recovery in the Metaverse&lt;/h2&gt;
&lt;h3 id=&quot;14-a-renewable-authenticated-fetch-layer&quot;&gt;14. A Renewable Authenticated Fetch Layer&lt;/h3&gt;
&lt;p&gt;The Metaverse API was moved onto a reusable authenticated request layer.&lt;/p&gt;
&lt;p&gt;The high-level flow is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;API request
    │
    ▼
access accepted?
 ┌──┴──┐
yes    no
 │      │
return  ▼
     renewable auth failure?
          │
       ┌──┴──┐
       no    yes
       │      │
      fail    ▼
           refresh
              │
              ▼
          retry once
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not every &lt;code&gt;401&lt;/code&gt; is renewable.&lt;/p&gt;
&lt;p&gt;An explicitly revoked session, for example, should not cause endless attempts to resurrect authentication.&lt;/p&gt;
&lt;p&gt;The client distinguishes terminal authentication state from renewable access-token failure.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;15-terminal-expiry-events&quot;&gt;15. Terminal Expiry Events&lt;/h3&gt;
&lt;p&gt;If refresh fails terminally, the browser:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;clears migration authentication state;&lt;/li&gt;
&lt;li&gt;emits an authentication-expired event;&lt;/li&gt;
&lt;li&gt;redirects through the login flow.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This behavior was connected to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Neon Plaza;&lt;/li&gt;
&lt;li&gt;Metaverse room pages;&lt;/li&gt;
&lt;li&gt;room creation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Multiple concurrent failures are deduplicated so the application does not perform several competing redirects.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;16-preserving-the-intended-destination&quot;&gt;16. Preserving the Intended Destination&lt;/h3&gt;
&lt;p&gt;Authentication expiry should not destroy user navigation context.&lt;/p&gt;
&lt;p&gt;The system preserves a local return path so a user can authenticate and return to the intended Metaverse location.&lt;/p&gt;
&lt;p&gt;That can include:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/metaverse/rooms/example?invite=...#section
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But return paths are also security-sensitive.&lt;/p&gt;
&lt;p&gt;Protocol-relative destinations are rejected.&lt;/p&gt;
&lt;p&gt;The application falls back to a known local Metaverse path when the return target is unsafe.&lt;/p&gt;
&lt;p&gt;This prevents the convenience mechanism from becoming an open-redirect primitive.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-v-csrf-protection&quot;&gt;Part V — CSRF Protection&lt;/h2&gt;
&lt;h3 id=&quot;17-cookies-changed-the-threat-model&quot;&gt;17. Cookies Changed the Threat Model&lt;/h3&gt;
&lt;p&gt;Once authentication relies on HttpOnly cookies, the browser automatically attaches those credentials to matching requests.&lt;/p&gt;
&lt;p&gt;That is useful for protecting credentials from direct JavaScript access, but it also means CSRF must be addressed explicitly.&lt;/p&gt;
&lt;p&gt;The server now distinguishes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Authorization: Bearer ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;from ambient cookie authentication.&lt;/p&gt;
&lt;p&gt;Unsafe cookie-authenticated browser mutations require trusted request-origin information.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;18-origin-referer-and-fetch-metadata&quot;&gt;18. Origin, Referer, and Fetch Metadata&lt;/h3&gt;
&lt;p&gt;For applicable unsafe methods, the server evaluates browser request context.&lt;/p&gt;
&lt;p&gt;Cross-site Fetch Metadata can be rejected before session lookup:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Sec-Fetch-Site: cross-site
        │
        ▼
      reject
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trusted &lt;code&gt;Origin&lt;/code&gt; or &lt;code&gt;Referer&lt;/code&gt; information is checked for cookie-authenticated mutations.&lt;/p&gt;
&lt;p&gt;Rejected requests return a structured error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;AUTH_CSRF_REJECTED
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trusted production origins can be configured rather than hardcoded.&lt;/p&gt;
&lt;p&gt;Bearer clients remain supported during the migration because explicit bearer credentials have different ambient-credential behavior from browser cookies.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-vi-structured-authentication-errors&quot;&gt;Part VI — Structured Authentication Errors&lt;/h2&gt;
&lt;h3 id=&quot;19-error-codes-instead-of-raw-cryptographic-errors&quot;&gt;19. Error Codes Instead of Raw Cryptographic Errors&lt;/h3&gt;
&lt;p&gt;Raw JWT/library exceptions are not a good public API.&lt;/p&gt;
&lt;p&gt;The authentication layer now uses structured application errors and request IDs.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;error&quot;: {
    &quot;code&quot;: &quot;AUTH_...&quot;,
    &quot;request_id&quot;: &quot;...&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a stable contract between server and frontend.&lt;/p&gt;
&lt;p&gt;The UI can distinguish:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;access expiry;&lt;/li&gt;
&lt;li&gt;revoked sessions;&lt;/li&gt;
&lt;li&gt;refresh replay;&lt;/li&gt;
&lt;li&gt;terminal refresh failure;&lt;/li&gt;
&lt;li&gt;CSRF rejection;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;without parsing arbitrary exception strings.&lt;/p&gt;
&lt;p&gt;Request IDs also improve operational debugging.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-vii-the-mongoose-bson-failure-we-found-during-testing&quot;&gt;Part VII — The Mongoose/BSON Failure We Found During Testing&lt;/h2&gt;
&lt;h3 id=&quot;20-the-initial-symptom&quot;&gt;20. The Initial Symptom&lt;/h3&gt;
&lt;p&gt;One of the most useful results of the final integration test was not a passing test.&lt;/p&gt;
&lt;p&gt;It was a hang.&lt;/p&gt;
&lt;p&gt;The social-identity suite stopped at:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;RUNS tests/socialIdentityService.test.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Eventually it failed with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;MongooseError:
Operation `metaversecharacters.findOne()` buffering timed out after 10000ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same thing happened during test cleanup:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;metaversecharacters.deleteMany()
buffering timed out
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first glance, this looked like a MongoMemoryServer or asynchronous-test problem.&lt;/p&gt;
&lt;p&gt;It wasn't.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;21-two-mongoose-runtimes-in-one-process&quot;&gt;21. Two Mongoose Runtimes in One Process&lt;/h3&gt;
&lt;p&gt;The root application used:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Mongoose 7.8.12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;while the nested backend used:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Mongoose 8.24.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;User&lt;/code&gt; and &lt;code&gt;AuthSession&lt;/code&gt; came from the root runtime.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MetaverseCharacter&lt;/code&gt; came from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;backend/src/models/MetaverseCharacter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and that model resolved its own:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;require('mongoose')
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;against the backend installation.&lt;/p&gt;
&lt;p&gt;So the test had effectively created:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;MongoMemoryServer
       │
       ▼
root Mongoose 7
       │
       ├── User
       └── AuthSession


backend Mongoose 8
       │
       └── MetaverseCharacter
            X
       no connected test DB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The timeout suddenly made sense.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;22-connecting-both-runtimes-revealed-the-deeper-problem&quot;&gt;22. Connecting Both Runtimes Revealed the Deeper Problem&lt;/h3&gt;
&lt;p&gt;As a diagnostic step, both Mongoose instances were connected to the same in-memory MongoDB.&lt;/p&gt;
&lt;p&gt;The timeout disappeared.&lt;/p&gt;
&lt;p&gt;But the next error was more informative:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;BSONVersionError:
Unsupported BSON version,
bson types must be from bson 6.x.x
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An ObjectId produced by one Mongoose/BSON ecosystem was being passed into a model using another BSON implementation.&lt;/p&gt;
&lt;p&gt;So simply connecting both instances was not a sound fix.&lt;/p&gt;
&lt;p&gt;It exposed the deeper boundary problem.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;23-model-factory-as-the-boundary-repair&quot;&gt;23. Model Factory as the Boundary Repair&lt;/h3&gt;
&lt;p&gt;We did not solve this by increasing Jest timeouts.&lt;/p&gt;
&lt;p&gt;We did not keep &lt;code&gt;--forceExit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We did not normalize every ObjectId manually and hope no other BSON type crossed the boundary.&lt;/p&gt;
&lt;p&gt;Instead, &lt;code&gt;MetaverseCharacter&lt;/code&gt; was changed to expose a model factory.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function createMetaverseCharacterModel(mongoose) {
  const schema = new mongoose.Schema({
    // ...
  });

  return mongoose.models.MetaverseCharacter ||
    mongoose.model('MetaverseCharacter', schema);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Backend consumers can continue using the backend Mongoose runtime.&lt;/p&gt;
&lt;p&gt;Root consumers can instantiate the same schema using the root runtime.&lt;/p&gt;
&lt;p&gt;That changes the topology to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Root application
       │
       ▼
root Mongoose
       │
       ├── User
       ├── AuthSession
       └── MetaverseCharacter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Backend application
       │
       ▼
backend Mongoose
       │
       └── MetaverseCharacter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The schema definition can be shared without forcing two incompatible Mongoose runtime objects into the same database operation.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;24-why-this-matters-beyond-tests&quot;&gt;24. Why This Matters Beyond Tests&lt;/h3&gt;
&lt;p&gt;It would have been easy to classify the failure as “just a Jest problem.”&lt;/p&gt;
&lt;p&gt;That would have been incorrect.&lt;/p&gt;
&lt;p&gt;The test exposed an actual architectural boundary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;shared model source
≠
shared Mongoose runtime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Node's module resolution can load different versions of the same dependency from different package trees.&lt;/p&gt;
&lt;p&gt;ODM objects such as ObjectIds, models, connections, and schemas are not automatically interchangeable simply because they ultimately represent MongoDB concepts.&lt;/p&gt;
&lt;p&gt;The lesson is broader:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;When a Node.js process contains multiple ODM/library runtimes, pass primitives or explicitly bind shared schemas to the owning runtime. Do not assume runtime-specific objects are portable across package boundaries.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-viii-repairing-source-corruption&quot;&gt;Part VIII — Repairing Source Corruption&lt;/h2&gt;
&lt;h3 id=&quot;25-utf-8-and-control-character-validation&quot;&gt;25. UTF-8 and Control-Character Validation&lt;/h3&gt;
&lt;p&gt;During the MYZ-71 repair we also discovered corrupted frontend source content.&lt;/p&gt;
&lt;p&gt;The affected surfaces included authentication/security and Metaverse pages.&lt;/p&gt;
&lt;p&gt;We explicitly checked for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Unicode replacement characters;&lt;/li&gt;
&lt;li&gt;unexpected control characters;&lt;/li&gt;
&lt;li&gt;malformed JSX;&lt;/li&gt;
&lt;li&gt;corrupted localized strings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The affected source was repaired and then checked again before running the frontend suite.&lt;/p&gt;
&lt;p&gt;This was important because some apparent React/parser failures were not behavioral regressions at all — they were malformed source bytes or broken string literals.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;26-javascript-string-parsing-failure&quot;&gt;26. JavaScript String Parsing Failure&lt;/h3&gt;
&lt;p&gt;Another backend gate exposed a malformed JavaScript string containing an apostrophe:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Autorizzazione GitHub non associata all'account ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The single quote terminated the surrounding single-quoted JavaScript string.&lt;/p&gt;
&lt;p&gt;This prevented Babel/Jest from parsing the controller.&lt;/p&gt;
&lt;p&gt;The string was repaired and syntax validation was performed before rerunning the backend suites.&lt;/p&gt;
&lt;p&gt;This is a simple bug, but it reinforces why syntax/build gates belong in the final validation pipeline even after behavioral tests have been added.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-ix-validation&quot;&gt;Part IX — Validation&lt;/h2&gt;
&lt;h3 id=&quot;27-backend-final-gate&quot;&gt;27. Backend Final Gate&lt;/h3&gt;
&lt;p&gt;After repairing the Mongoose boundary and JavaScript syntax, the targeted backend gate completed successfully:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Test Suites: 3 passed, 3 total
Tests:       18 passed, 18 total
Time:        4.478 s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The suites covered the relevant session controller, social authentication callback, and social identity behavior.&lt;/p&gt;
&lt;p&gt;The previously hanging social identity tests now completed normally.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;28-frontend-final-gate&quot;&gt;28. Frontend Final Gate&lt;/h3&gt;
&lt;p&gt;The targeted frontend gate also completed successfully:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Test Suites: 4 passed, 4 total
Tests:       13 passed, 13 total
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Coverage included the authentication-expiry redirect, account-security page, auth-session API, and Metaverse API behavior.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;29-production-build&quot;&gt;29. Production Build&lt;/h3&gt;
&lt;p&gt;We then generated an optimized React production build:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Creating an optimized production build...
Compiled with warnings.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The build completed successfully.&lt;/p&gt;
&lt;p&gt;The remaining ESLint warnings were non-blocking and outside the scope of the repair.&lt;/p&gt;
&lt;p&gt;They were deliberately not mixed into the security fix simply to produce an artificially warning-free diff.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;30-git-integrity-gate&quot;&gt;30. Git Integrity Gate&lt;/h3&gt;
&lt;p&gt;Before committing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git diff --cached --check
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;completed cleanly.&lt;/p&gt;
&lt;p&gt;Generated build artifacts were removed from the commit.&lt;/p&gt;
&lt;p&gt;An unrelated untracked directory was also deliberately kept outside the MYZ-71 commit.&lt;/p&gt;
&lt;p&gt;The final repair was committed as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1878e02a
fix: harden revocable account sessions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and pushed to the MYZ-71 branch.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-x-what-is-operational-now&quot;&gt;Part X — What Is Operational Now&lt;/h2&gt;
&lt;h3 id=&quot;31-treasury&quot;&gt;31. Treasury&lt;/h3&gt;
&lt;p&gt;The implemented treasury infrastructure now supports:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;bounty settlement purpose
        │
        ▼
crypto source transaction
        │
        ▼
asset conversion
        │
        ▼
target-chain transaction
        │
        ▼
verification
        │
        ▼
dry-run
        │
        ▼
idempotent persistence
        │
        ▼
dashboard provider
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The recorded BTC → ETH operation reached:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;CONVERSION_COMPLETED
VERIFIED
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and replaying the recorder did not create a duplicate conversion.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;32-authentication&quot;&gt;32. Authentication&lt;/h3&gt;
&lt;p&gt;The implemented authentication/session infrastructure now provides:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;login
 │
 ▼
persistent AuthSession
 │
 ├── access JWT bound to session
 │
 └── opaque rotating refresh credential
          │
          ├── atomic rotation
          ├── bounded consumed-token history
          └── replay detection
                   │
                   ▼
              session revocation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The frontend adds:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;authenticated request
       │
       ▼
renew if allowed
       │
       ▼
retry once
       │
       ▼
terminal failure?
       │
       ▼
clear state + auth-expired event
       │
       ▼
login
       │
       ▼
restore safe local destination
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-xi-security-properties-achieved&quot;&gt;Part XI — Security Properties Achieved&lt;/h2&gt;
&lt;p&gt;The resulting implementation has several concrete security properties.&lt;/p&gt;
&lt;h4 id=&quot;server-authority-over-sessions&quot;&gt;Server authority over sessions&lt;/h4&gt;
&lt;p&gt;A cryptographically valid JWT can still be rejected when its server session has been revoked or removed.&lt;/p&gt;
&lt;h4 id=&quot;single-use-refresh-credentials&quot;&gt;Single-use refresh credentials&lt;/h4&gt;
&lt;p&gt;Refresh credentials rotate instead of remaining indefinitely reusable.&lt;/p&gt;
&lt;h4 id=&quot;replay-detection&quot;&gt;Replay detection&lt;/h4&gt;
&lt;p&gt;Reuse of consumed refresh state causes session revocation.&lt;/p&gt;
&lt;h4 id=&quot;no-raw-refresh-token-persistence&quot;&gt;No raw refresh-token persistence&lt;/h4&gt;
&lt;p&gt;Refresh credentials are represented server-side through cryptographic hashes.&lt;/p&gt;
&lt;h4 id=&quot;client-server-concurrency-alignment&quot;&gt;Client/server concurrency alignment&lt;/h4&gt;
&lt;p&gt;The frontend shares concurrent refresh work while the backend performs atomic token rotation.&lt;/p&gt;
&lt;h4 id=&quot;remote-session-revocation&quot;&gt;Remote session revocation&lt;/h4&gt;
&lt;p&gt;A user can invalidate another authenticated device without necessarily destroying every active session.&lt;/p&gt;
&lt;h4 id=&quot;cookie-mutation-protection&quot;&gt;Cookie mutation protection&lt;/h4&gt;
&lt;p&gt;Unsafe cookie-authenticated browser operations are protected by trusted-origin/Fetch Metadata checks.&lt;/p&gt;
&lt;h4 id=&quot;structured-auth-failures&quot;&gt;Structured auth failures&lt;/h4&gt;
&lt;p&gt;Clients receive stable authentication semantics rather than raw JWT/library errors.&lt;/p&gt;
&lt;h4 id=&quot;safe-return-navigation&quot;&gt;Safe return navigation&lt;/h4&gt;
&lt;p&gt;Authentication recovery preserves useful local navigation without accepting arbitrary protocol-relative redirect destinations.&lt;/p&gt;
&lt;h4 id=&quot;explicit-odm-runtime-ownership&quot;&gt;Explicit ODM runtime ownership&lt;/h4&gt;
&lt;p&gt;The same Metaverse schema can be bound to the Mongoose runtime that actually owns the surrounding connection and models.&lt;/p&gt;
&lt;h4 id=&quot;replay-safe-treasury-persistence&quot;&gt;Replay-safe treasury persistence&lt;/h4&gt;
&lt;p&gt;Reexecuting the same verified conversion recorder does not produce duplicate settlement state.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-xii-what-we-deliberately-did-not-claim&quot;&gt;Part XII — What We Deliberately Did Not Claim&lt;/h2&gt;
&lt;p&gt;Engineering reports are more useful when they clearly distinguish completed infrastructure from future work.&lt;/p&gt;
&lt;p&gt;This iteration does &lt;strong&gt;not&lt;/strong&gt; mean the entire MYZ-71 roadmap is finished.&lt;/p&gt;
&lt;p&gt;Remaining work includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;passkeys and/or magic-link authentication;&lt;/li&gt;
&lt;li&gt;step-up authentication for sensitive operations;&lt;/li&gt;
&lt;li&gt;extending automatic renewal to authenticated clients outside the current Metaverse integration;&lt;/li&gt;
&lt;li&gt;completing migration away from legacy localStorage/bearer compatibility;&lt;/li&gt;
&lt;li&gt;strict production server-session enforcement after the migration window;&lt;/li&gt;
&lt;li&gt;production trusted-origin verification;&lt;/li&gt;
&lt;li&gt;broader OAuth production verification;&lt;/li&gt;
&lt;li&gt;production MongoDB stability validation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the treasury side, a verified conversion record is also not the final form of a contributor payment platform.&lt;/p&gt;
&lt;p&gt;Future settlement work can connect:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;bounty
  ↓
contributor
  ↓
approved amount
  ↓
payment instruction
  ↓
source transaction
  ↓
optional conversion
  ↓
destination transaction
  ↓
verification
  ↓
notification
  ↓
settled bounty state
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;into one auditable lifecycle.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The most valuable part of this development cycle was not the number of endpoints or React components added.&lt;/p&gt;
&lt;p&gt;It was the strengthening of system boundaries.&lt;/p&gt;
&lt;p&gt;For payments:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;blockchain activity
      ↓
verified application state
      ↓
idempotent treasury record
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For authentication:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;client credential
      ↓
server-authoritative session
      ↓
rotating credentials
      ↓
replay detection
      ↓
revocation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And for the Node/MongoDB architecture:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;shared schema
      ↓
explicit runtime ownership
      ↓
consistent Mongoose connection
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The final repair was validated with 18/18 targeted backend tests, 13/13 targeted frontend tests, a successful optimized frontend build, and a clean Git diff check.&lt;/p&gt;
&lt;p&gt;More importantly, integration testing exposed a real Mongoose 7/Mongoose 8 BSON boundary that would have been easy to hide with larger timeouts. Fixing the ownership boundary instead of suppressing the symptom left the architecture in a better state.&lt;/p&gt;
&lt;p&gt;MyZubster now has stronger primitives for two areas where correctness matters disproportionately: &lt;strong&gt;moving value and maintaining identity&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That gives us a much safer base for the next layer of automation.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28935/building-verifiable-bounty-payments-revocable-account-sessions-myzubster-over-latest</guid>
<pubDate>Mon, 28 Sep 2026 14:14:58 +0000</pubDate>
</item>
<item>
<title>Securing the MyZubster Metaverse: From Stateless JWTs to Revocable Sessions</title>
<link>https://coderlegion.com/28926/securing-the-myzubster-metaverse-from-stateless-jwts-to-revocable-sessions</link>
<description>&lt;p&gt;Securing the MyZubster Metaverse: From Stateless JWTs to Revocable Sessions&lt;br&gt;
We have completed another major security milestone for the MyZubster Metaverse.&lt;br&gt;
The original authentication system depended mainly on browser-stored JWT access tokens. That solution was simple, but it could not provide the session lifecycle controls required by a platform with persistent identities, virtual characters, private rooms, realtime interactions and future Marketplace or payment operations.&lt;br&gt;
Under MYZ-71 — Authentication, Sessions &amp;amp; Account Security, we have now implemented:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;persistent server-side sessions;&lt;/li&gt;
&lt;li&gt;session-bound JWT access tokens;&lt;/li&gt;
&lt;li&gt;real logout;&lt;/li&gt;
&lt;li&gt;active-device management;&lt;/li&gt;
&lt;li&gt;remote session revocation;&lt;/li&gt;
&lt;li&gt;rotating opaque refresh tokens;&lt;/li&gt;
&lt;li&gt;refresh-token replay detection;&lt;/li&gt;
&lt;li&gt;automatic access-token renewal for the Metaverse;&lt;/li&gt;
&lt;li&gt;concurrent refresh deduplication;&lt;/li&gt;
&lt;li&gt;origin-based CSRF protection;&lt;/li&gt;
&lt;li&gt;structured authentication errors.&lt;br&gt;
The implementation is currently available in a Draft pull request:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/danieldirimini-myzubster/myzubster/pull/10&quot;&gt;https://github.com/danieldirimini-myzubster/myzubster/pull/10&lt;/a&gt;&lt;br&gt;
It has not been merged or declared production-ready yet.&lt;br&gt;
Why stateless JWTs were no longer enough&lt;br&gt;
A server can validate a JWT without loading a database session, but a completely stateless design has significant limitations.&lt;br&gt;
A token may remain valid even after:&lt;/li&gt;
&lt;li&gt;the user logs out;&lt;/li&gt;
&lt;li&gt;the device is no longer trusted;&lt;/li&gt;
&lt;li&gt;the password changes;&lt;/li&gt;
&lt;li&gt;the account owner wants to disconnect another device;&lt;/li&gt;
&lt;li&gt;a security event requires immediate revocation.&lt;br&gt;
Long-lived tokens also increase the impact of credential theft.&lt;br&gt;
If they are stored in localStorage, they are accessible to frontend JavaScript. This makes an XSS vulnerability significantly more dangerous.&lt;br&gt;
We moved toward a hybrid architecture:&lt;br&gt;
User authentication&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Persistent server-side session&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Short-lived JWT access token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rotating opaque refresh token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Automatic renewal&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replay detection and revocation&lt;br&gt;
The JWT is still useful, but the persistent session is now the final source of authority.&lt;br&gt;
Persistent server-side sessions&lt;br&gt;
Each authenticated device receives its own MongoDB AuthSession.&lt;br&gt;
A session records:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;the session identifier;&lt;/li&gt;
&lt;li&gt;the owning user;&lt;/li&gt;
&lt;li&gt;creation and expiration timestamps;&lt;/li&gt;
&lt;li&gt;last activity;&lt;/li&gt;
&lt;li&gt;limited device metadata;&lt;/li&gt;
&lt;li&gt;a privacy-preserving network digest;&lt;/li&gt;
&lt;li&gt;revocation time and reason;&lt;/li&gt;
&lt;li&gt;the current refresh-token hash;&lt;/li&gt;
&lt;li&gt;previously consumed refresh-token hashes;&lt;/li&gt;
&lt;li&gt;refresh-token rotation metadata.&lt;br&gt;
MongoDB TTL indexing is used to remove expired sessions automatically.&lt;br&gt;
A valid JWT is rejected when its corresponding session is missing, expired or revoked.&lt;br&gt;
Binding access tokens to sessions&lt;br&gt;
New access tokens include a server-generated sid claim.&lt;br&gt;
A simplified token looks like this:&lt;br&gt;
{&lt;br&gt;
&quot;sub&quot;: &quot;user-id&quot;,&lt;br&gt;
&quot;sid&quot;: &quot;session-uuid&quot;,&lt;br&gt;
&quot;jti&quot;: &quot;access-token-uuid&quot;,&lt;br&gt;
&quot;iat&quot;: 1790540000,&lt;br&gt;
&quot;exp&quot;: 1790540900&lt;br&gt;
}&lt;br&gt;
Request authentication now consists of two steps:&lt;br&gt;
Verify the JWT&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Validate the server-side session&lt;br&gt;
This provides both efficient request authentication and immediate revocation.&lt;br&gt;
If a device is removed from the account-security interface, its JWT stops working even if it has not reached its expiration time.&lt;br&gt;
Real logout and active-device management&lt;br&gt;
The React frontend now includes:&lt;br&gt;
/account/security&lt;br&gt;
Users can inspect:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;active sessions;&lt;/li&gt;
&lt;li&gt;the current device;&lt;/li&gt;
&lt;li&gt;creation time;&lt;/li&gt;
&lt;li&gt;last activity;&lt;/li&gt;
&lt;li&gt;expiration time;&lt;/li&gt;
&lt;li&gt;limited device information.&lt;br&gt;
They can disconnect another device or terminate their current session.&lt;br&gt;
The supporting endpoints are:&lt;br&gt;
POST /api/auth/logout&lt;br&gt;
GET /api/auth/me&lt;br&gt;
GET /api/auth/me/sessions&lt;br&gt;
DELETE /api/auth/me/sessions/:sessionId&lt;br&gt;
Logout is idempotent.&lt;br&gt;
Even when the access token is expired or malformed, the backend clears the authentication cookies. When possible, the refresh token is used to identify and revoke the persistent session.&lt;br&gt;
Opaque refresh tokens&lt;br&gt;
The refresh token is deliberately not a JWT.&lt;br&gt;
Its conceptual format is:&lt;br&gt;
myzr.&amp;lt;session-id&amp;gt;.&amp;lt;cryptographically-random-secret&amp;gt;&lt;br&gt;
The raw token is never stored in MongoDB.&lt;br&gt;
The backend calculates:&lt;br&gt;
HMAC-SHA256(server secret, raw refresh token)&lt;br&gt;
Only the resulting digest is persisted.&lt;br&gt;
If the session database is exposed, an attacker cannot directly use the stored digest to renew a session.&lt;br&gt;
Atomic refresh-token rotation&lt;br&gt;
Refresh tokens are single-use.&lt;br&gt;
When the browser calls:&lt;br&gt;
POST /api/auth/refresh&lt;br&gt;
the server:&lt;ol&gt;
&lt;li&gt;reads the HttpOnly refresh cookie;&lt;/li&gt;
&lt;li&gt;extracts the session identifier;&lt;/li&gt;
&lt;li&gt;hashes the submitted token;&lt;/li&gt;
&lt;li&gt;creates a new random refresh token;&lt;/li&gt;
&lt;li&gt;atomically compares the submitted hash with the current stored hash;&lt;/li&gt;
&lt;li&gt;replaces the current hash;&lt;/li&gt;
&lt;li&gt;stores the consumed hash in a bounded history;&lt;/li&gt;
&lt;li&gt;issues a new access token;&lt;/li&gt;
&lt;li&gt;sends new authentication cookies.&lt;br&gt;
Only one request can successfully consume the current token.&lt;br&gt;
This prevents ordinary concurrent requests from rotating the same credential more than once.&lt;br&gt;
Refresh-token replay detection&lt;br&gt;
The server maintains a bounded history of consumed refresh-token hashes.&lt;br&gt;
If a previously consumed token appears again, the session is treated as potentially compromised:&lt;br&gt;
Old refresh token submitted&lt;br&gt;
   ↓&lt;br&gt;
Replay detected&lt;br&gt;
   ↓&lt;br&gt;
Entire session revoked&lt;br&gt;
   ↓&lt;br&gt;
Cookies cleared&lt;br&gt;
   ↓&lt;br&gt;
New login required&lt;br&gt;
The revocation reason is recorded as:&lt;br&gt;
refresh-token-replay&lt;br&gt;
The history is limited so the MongoDB session document cannot grow indefinitely.&lt;br&gt;
Automatic renewal in the Metaverse&lt;br&gt;
We introduced a reusable frontend client:&lt;br&gt;
authenticatedFetch(input, options)&lt;br&gt;
The client handles the access-token lifecycle:&lt;br&gt;
Protected request&lt;br&gt;
   ↓&lt;br&gt;
401 access-token response&lt;br&gt;
   ↓&lt;br&gt;
POST /api/auth/refresh&lt;br&gt;
   ↓&lt;br&gt;
Receive rotated access token&lt;br&gt;
   ↓&lt;br&gt;
Retry original request once&lt;br&gt;
The complete Metaverse API module now uses this client.&lt;br&gt;
It covers operations involving:&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;profiles;&lt;/li&gt;
&lt;li&gt;rooms;&lt;/li&gt;
&lt;li&gt;invitations;&lt;/li&gt;
&lt;li&gt;sessions;&lt;/li&gt;
&lt;li&gt;participants;&lt;/li&gt;
&lt;li&gt;stage access;&lt;/li&gt;
&lt;li&gt;messages;&lt;/li&gt;
&lt;li&gt;moderation;&lt;/li&gt;
&lt;li&gt;reports;&lt;/li&gt;
&lt;li&gt;landmarks;&lt;/li&gt;
&lt;li&gt;realtime world actions.&lt;br&gt;
The client retries only recognized access-token errors and never enters an infinite retry loop.&lt;br&gt;
Deduplicating concurrent refresh operations&lt;br&gt;
A Metaverse page can perform several API requests simultaneously.&lt;br&gt;
When an access token expires, multiple requests may receive a 401 at approximately the same time.&lt;br&gt;
Without coordination, every request could call /api/auth/refresh using the same single-use refresh token. This could produce an accidental replay event.&lt;br&gt;
The client therefore shares one in-flight refresh operation:&lt;br&gt;
Request A → 401 ┐&lt;br&gt;
Request B → 401 ├─→ One refresh&lt;br&gt;
Request C → 401 ┘&lt;pre&gt;&lt;code&gt;               ↓
         New access token
               ↓
        Retry A, B and C
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tests confirm that two simultaneous protected requests produce exactly one refresh operation and two successful retries.&lt;br&gt;
Handling terminal authentication failures&lt;br&gt;
The client does not silently renew an explicitly revoked session.&lt;br&gt;
For example:&lt;br&gt;
AUTH_SESSION_REVOKED&lt;br&gt;
is returned directly to the application.&lt;br&gt;
If the refresh token is no longer valid, the client:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;clears the temporary browser credentials;&lt;/li&gt;
&lt;li&gt;emits:&lt;br&gt;
myzubster:auth-expired&lt;br&gt;
The UI can use this event to return the user to authentication without every page implementing its own renewal logic.&lt;br&gt;
CSRF protection&lt;br&gt;
Cookie authentication introduces a different security risk: browsers attach cookies automatically.&lt;br&gt;
State-changing requests must therefore be protected against Cross-Site Request Forgery.&lt;br&gt;
We added an origin-based CSRF guard for unsafe cookie-authenticated methods:&lt;br&gt;
POST&lt;br&gt;
PUT&lt;br&gt;
PATCH&lt;br&gt;
DELETE&lt;br&gt;
The guard evaluates:&lt;/li&gt;
&lt;li&gt;Origin;&lt;/li&gt;
&lt;li&gt;Referer;&lt;/li&gt;
&lt;li&gt;Sec-Fetch-Site;&lt;/li&gt;
&lt;li&gt;configured application origins;&lt;/li&gt;
&lt;li&gt;the actual request target.&lt;br&gt;
A browser request marked as cross-site is rejected before the backend performs a session lookup:&lt;br&gt;
Sec-Fetch-Site: cross-site&lt;br&gt;
The API returns a structured response:&lt;br&gt;
{&lt;br&gt;
&quot;success&quot;: false,&lt;br&gt;
&quot;request_id&quot;: &quot;request-uuid&quot;,&lt;br&gt;
&quot;error&quot;: {&lt;br&gt;
  &quot;code&quot;: &quot;AUTH_CSRF_REJECTED&quot;,&lt;br&gt;
  &quot;message&quot;: &quot;Origine della richiesta non autorizzata&quot;&lt;br&gt;
}&lt;br&gt;
}&lt;br&gt;
Which routes are protected?&lt;br&gt;
The trusted-origin policy is applied to:&lt;/li&gt;
&lt;li&gt;password registration;&lt;/li&gt;
&lt;li&gt;password login;&lt;/li&gt;
&lt;li&gt;OAuth ticket verification;&lt;/li&gt;
&lt;li&gt;social-login ticket exchange;&lt;/li&gt;
&lt;li&gt;refresh-token rotation;&lt;/li&gt;
&lt;li&gt;logout;&lt;/li&gt;
&lt;li&gt;Gmail verification exchange;&lt;/li&gt;
&lt;li&gt;protected mutations authenticated through cookies.&lt;br&gt;
Bearer-token clients remain compatible during the migration because they do not depend on automatically attached cookies.&lt;br&gt;
CLI and server-to-server clients that do not send browser origin metadata also remain supported.&lt;br&gt;
Configuring trusted origins&lt;br&gt;
Production and preview origins can be configured with:&lt;br&gt;
AUTH_TRUSTED_ORIGINS=&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com,https://preview.myzubster.com&quot;&gt;https://www.myzubster.com,https://preview.myzubster.com&lt;/a&gt;&lt;br&gt;
The middleware also recognizes:&lt;br&gt;
FRONTEND_URL&lt;br&gt;
PUBLIC_APP_URL&lt;br&gt;
GATEWAY_PUBLIC_URL&lt;br&gt;
Local development accepts:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;http://localhost:3000&quot;&gt;http://localhost:3000&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;http://127.0.0.1:3000&quot;&gt;http://127.0.0.1:3000&lt;/a&gt;&lt;br&gt;
These values still need to be verified in the real deployment before the platform switches to cookie-only authentication.&lt;br&gt;
Cookie protections&lt;br&gt;
The authentication cookies use:&lt;br&gt;
HttpOnly&lt;br&gt;
SameSite=Lax&lt;br&gt;
Secure in production&lt;br&gt;
The refresh cookie is restricted to:&lt;br&gt;
Path=/api/auth&lt;br&gt;
This prevents it from being attached to unrelated application requests.&lt;br&gt;
SameSite=Lax provides another security layer, but it is not treated as the only CSRF defence.&lt;br&gt;
Privacy considerations&lt;br&gt;
Raw client IP addresses are not stored.&lt;br&gt;
The server stores an HMAC-derived digest instead. This allows limited correlation for security analysis without retaining the original address as ordinary application data.&lt;br&gt;
Device metadata is also intentionally limited.&lt;br&gt;
A session-security feature should help users recognize their devices without becoming an invasive tracking mechanism.&lt;br&gt;
Validation results&lt;br&gt;
The focused backend security scope passes:&lt;br&gt;
30/30 tests&lt;br&gt;
The focused frontend and Metaverse scope passes:&lt;br&gt;
19/19 tests&lt;br&gt;
The test coverage includes:&lt;/li&gt;
&lt;li&gt;persistent session creation;&lt;/li&gt;
&lt;li&gt;session-bound JWT validation;&lt;/li&gt;
&lt;li&gt;real logout;&lt;/li&gt;
&lt;li&gt;remote revocation;&lt;/li&gt;
&lt;li&gt;refresh-token hashing;&lt;/li&gt;
&lt;li&gt;atomic token rotation;&lt;/li&gt;
&lt;li&gt;refresh replay detection;&lt;/li&gt;
&lt;li&gt;replay-driven session revocation;&lt;/li&gt;
&lt;li&gt;automatic access-token renewal;&lt;/li&gt;
&lt;li&gt;exactly-once retry;&lt;/li&gt;
&lt;li&gt;concurrent refresh deduplication;&lt;/li&gt;
&lt;li&gt;structured authentication failures;&lt;/li&gt;
&lt;li&gt;same-origin CSRF acceptance;&lt;/li&gt;
&lt;li&gt;configured frontend-origin acceptance;&lt;/li&gt;
&lt;li&gt;cross-site CSRF rejection;&lt;/li&gt;
&lt;li&gt;rejection before database lookup;&lt;/li&gt;
&lt;li&gt;bearer-token compatibility;&lt;/li&gt;
&lt;li&gt;non-browser compatibility.&lt;br&gt;
The React production build also completes successfully.&lt;br&gt;
GitHub Actions status&lt;br&gt;
The affected backend suites are green:&lt;br&gt;
tests/authSessionService.test.js&lt;br&gt;
tests/authSessionController.test.js&lt;br&gt;
tests/authMiddlewareSessions.test.js&lt;br&gt;
tests/socialIdentityService.test.js&lt;br&gt;
tests/csrfProtection.test.js&lt;br&gt;
The complete repository currently reports:&lt;br&gt;
142 test suites passed&lt;br&gt;
42 test suites failed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;746 tests passed&lt;br&gt;
44 tests failed&lt;br&gt;
The remaining failures belong to existing repository-wide problems involving Zorgax, Marketplace, seller policies and legacy Metaverse expectations.&lt;br&gt;
The new authentication implementation is green in its affected scope, but the pull request remains a Draft because the repository as a whole is not yet CI-clean.&lt;br&gt;
What is completed?&lt;br&gt;
Persistent server-side sessions           DONE&lt;br&gt;
Session-bound access tokens                DONE&lt;br&gt;
Real logout                                DONE&lt;br&gt;
Active-device management                   DONE&lt;br&gt;
Remote session revocation                  DONE&lt;br&gt;
Rotating opaque refresh tokens             DONE&lt;br&gt;
Refresh-token replay detection             DONE&lt;br&gt;
Password and social authentication         DONE&lt;br&gt;
Account-security interface                 DONE&lt;br&gt;
Metaverse automatic renewal                DONE&lt;br&gt;
Concurrent refresh deduplication           DONE&lt;br&gt;
Origin-based CSRF protection               DONE&lt;br&gt;
Structured authentication errors           DONE&lt;br&gt;
Focused automated testing                  DONE&lt;br&gt;
What is still missing?&lt;br&gt;
Renewal outside the Metaverse&lt;br&gt;
Some Marketplace, Zorgax, profile and administrative pages still use direct fetch calls and manually constructed bearer headers.&lt;br&gt;
They need to be migrated to authenticatedFetch.&lt;br&gt;
Removal of localStorage authentication&lt;br&gt;
Browser storage remains as a temporary migration bridge.&lt;br&gt;
The final architecture should avoid exposing reusable credentials to frontend JavaScript.&lt;br&gt;
Before removing it, we must migrate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;all authenticated API clients;&lt;/li&gt;
&lt;li&gt;protected React pages;&lt;/li&gt;
&lt;li&gt;realtime connections;&lt;/li&gt;
&lt;li&gt;embedded or mobile clients;&lt;/li&gt;
&lt;li&gt;legacy user sessions.&lt;br&gt;
Production trusted-origin validation&lt;br&gt;
The public deployment must verify:&lt;/li&gt;
&lt;li&gt;exact frontend origins;&lt;/li&gt;
&lt;li&gt;preview domains;&lt;/li&gt;
&lt;li&gt;reverse-proxy host and protocol forwarding;&lt;/li&gt;
&lt;li&gt;cookie paths;&lt;/li&gt;
&lt;li&gt;cross-site rejection;&lt;/li&gt;
&lt;li&gt;same-origin refresh;&lt;/li&gt;
&lt;li&gt;session revocation;&lt;/li&gt;
&lt;li&gt;deliberate replay detection.&lt;br&gt;
Strict session enforcement&lt;br&gt;
After legacy tokens have expired, we can enable:&lt;br&gt;
REQUIRE_SERVER_SESSION=true&lt;br&gt;
JWTs without a valid persistent session will then be rejected.&lt;br&gt;
Passkeys and magic links&lt;br&gt;
MYZ-71 still includes passwordless authentication.&lt;br&gt;
Passkeys require complete WebAuthn support for:&lt;/li&gt;
&lt;li&gt;challenge generation;&lt;/li&gt;
&lt;li&gt;relying-party configuration;&lt;/li&gt;
&lt;li&gt;allowed origins;&lt;/li&gt;
&lt;li&gt;credential counters;&lt;/li&gt;
&lt;li&gt;credential revocation;&lt;/li&gt;
&lt;li&gt;recovery.&lt;br&gt;
Magic links require short-lived, single-use verification tokens.&lt;br&gt;
Step-up authentication&lt;br&gt;
Sensitive operations should require recent stronger authentication.&lt;br&gt;
Possible examples include:&lt;/li&gt;
&lt;li&gt;changing an email address;&lt;/li&gt;
&lt;li&gt;changing a password;&lt;/li&gt;
&lt;li&gt;registering or deleting passkeys;&lt;/li&gt;
&lt;li&gt;deleting an account;&lt;/li&gt;
&lt;li&gt;wallet operations;&lt;/li&gt;
&lt;li&gt;payment approval;&lt;/li&gt;
&lt;li&gt;sensitive Marketplace changes.&lt;br&gt;
Production OAuth and database validation&lt;br&gt;
Google, GitHub and Facebook authentication must still be tested using the production callback URLs and secrets.&lt;br&gt;
MongoDB Atlas must be checked for:&lt;/li&gt;
&lt;li&gt;TTL-index creation;&lt;/li&gt;
&lt;li&gt;refresh-token indexes;&lt;/li&gt;
&lt;li&gt;document growth;&lt;/li&gt;
&lt;li&gt;expired-session cleanup;&lt;/li&gt;
&lt;li&gt;concurrent rotation;&lt;/li&gt;
&lt;li&gt;rolling-deployment compatibility.&lt;br&gt;
Recommended next steps&lt;ol&gt;
&lt;li&gt;Review the Draft pull request&lt;/li&gt;
&lt;li&gt;Configure production trusted origins&lt;/li&gt;
&lt;li&gt;Deploy with migration compatibility enabled&lt;/li&gt;
&lt;li&gt;Verify cookies and database indexes&lt;/li&gt;
&lt;li&gt;Test automatic Metaverse renewal&lt;/li&gt;
&lt;li&gt;Test concurrent refresh requests&lt;/li&gt;
&lt;li&gt;Test cross-site rejection&lt;/li&gt;
&lt;li&gt;Test deliberate refresh-token replay&lt;/li&gt;
&lt;li&gt;Migrate the remaining frontend API clients&lt;/li&gt;
&lt;li&gt;Allow legacy tokens to expire&lt;/li&gt;
&lt;li&gt;Remove tokens from localStorage&lt;/li&gt;
&lt;li&gt;Enable strict session enforcement&lt;/li&gt;
&lt;li&gt;Remove bearer migration support&lt;/li&gt;
&lt;li&gt;Add passkeys, magic links and step-up authentication&lt;br&gt;
Final thoughts&lt;br&gt;
Authentication security is not just about issuing a JWT.&lt;br&gt;
A complete system must manage the entire lifecycle:&lt;br&gt;
Authenticate&lt;br&gt;
Create&lt;br&gt;
Validate&lt;br&gt;
Renew&lt;br&gt;
Observe&lt;br&gt;
Revoke&lt;br&gt;
Recover&lt;br&gt;
Monitor&lt;br&gt;
MyZubster now has the foundations for secure multi-device sessions, refresh-token rotation, replay detection, automatic Metaverse renewal and browser-origin protection.&lt;br&gt;
The next challenge is deployment and migration: validate the public environment, move the remaining clients to the shared authentication layer and eliminate browser-accessible credentials.&lt;br&gt;
Draft pull request:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/danieldirimini-myzubster/myzubster/pull/10&quot;&gt;https://github.com/danieldirimini-myzubster/myzubster/pull/10&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;myzubster-metaverse-cybersecurity-authentication-nodejs-reactjs-mongodb-jwt-csrf-websecurity-softwarearchitecture-opensource&quot;&gt;MyZubster #Metaverse #CyberSecurity #Authentication #NodeJS #ReactJS #MongoDB #JWT #CSRF #WebSecurity #SoftwareArchitecture #OpenSource&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28926/securing-the-myzubster-metaverse-from-stateless-jwts-to-revocable-sessions</guid>
<pubDate>Mon, 28 Sep 2026 12:31:55 +0000</pubDate>
</item>
<item>
<title>How We Built Revocable Sessions and Refresh-Token Replay Protection for the MyZubster Metaverse</title>
<link>https://coderlegion.com/28847/how-built-revocable-sessions-and-refresh-token-replay-protection-for-myzubster-metaverse</link>
<description>&lt;p&gt;Ecco la versione in inglese pronta per CoderLegion.&lt;br&gt;
How We Built Revocable Sessions and Refresh-Token Replay Protection for the MyZubster Metaverse&lt;br&gt;
Authentication in a Metaverse platform is more than a login form.&lt;br&gt;
An authenticated identity may control characters, social relationships, private spaces, Marketplace activity and, eventually, payment-related operations. For MyZubster, relying only on long-lived JSON Web Tokens was no longer a sufficient security model.&lt;br&gt;
Under MYZ-71 — Authentication, Sessions &amp;amp; Account Security, we started replacing stateless browser authentication with persistent, revocable and observable server-side sessions.&lt;br&gt;
This article describes what we implemented, how refresh-token rotation works, how we detect token replay, what we tested and what still needs to be completed before production deployment.&lt;br&gt;
The implementation is currently in a Draft pull request. It has not been merged or declared production-ready.&lt;/p&gt;
&lt;p&gt;Why stateless JWT authentication was not enough&lt;br&gt;
JWTs are useful because a server can validate them without loading a database record for every request.&lt;br&gt;
However, a completely stateless model creates important limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;logout cannot reliably invalidate an existing token;&lt;/li&gt;
&lt;li&gt;users cannot see which devices are authenticated;&lt;/li&gt;
&lt;li&gt;administrators cannot revoke one compromised device;&lt;/li&gt;
&lt;li&gt;a stolen token remains valid until expiration;&lt;/li&gt;
&lt;li&gt;password changes do not automatically terminate sessions;&lt;/li&gt;
&lt;li&gt;long-lived tokens increase the potential impact of credential theft;&lt;/li&gt;
&lt;li&gt;tokens stored in localStorage can be accessed by malicious JavaScript after an XSS compromise.&lt;br&gt;
We therefore introduced a hybrid architecture:&lt;br&gt;
Authentication&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Persistent server-side session&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Short-lived JWT access token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rotating opaque refresh token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replay detection and session revocation&lt;br&gt;
JWTs are still used for request authentication, but the server-side session is now the final source of authority.&lt;br&gt;
Persistent server-side sessions&lt;br&gt;
We added a MongoDB AuthSession model.&lt;br&gt;
A separate session is created for each authenticated device. The record contains:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;a unique session identifier;&lt;/li&gt;
&lt;li&gt;the user who owns the session;&lt;/li&gt;
&lt;li&gt;creation and expiration timestamps;&lt;/li&gt;
&lt;li&gt;the last activity timestamp;&lt;/li&gt;
&lt;li&gt;revocation state and reason;&lt;/li&gt;
&lt;li&gt;limited device metadata;&lt;/li&gt;
&lt;li&gt;a privacy-preserving client-IP digest;&lt;/li&gt;
&lt;li&gt;the current refresh-token hash;&lt;/li&gt;
&lt;li&gt;a bounded history of consumed refresh-token hashes;&lt;/li&gt;
&lt;li&gt;refresh-token rotation metadata.&lt;br&gt;
MongoDB TTL indexing is used to remove expired session records automatically.&lt;br&gt;
This enables real session lifecycle management. A valid JWT is rejected whenever its associated session is expired, missing or revoked.&lt;br&gt;
Session-bound access tokens&lt;br&gt;
New JWT access tokens include a server-generated sid claim.&lt;br&gt;
A simplified payload looks like this:&lt;br&gt;
{&lt;br&gt;
&quot;sub&quot;: &quot;user-id&quot;,&lt;br&gt;
&quot;sid&quot;: &quot;session-uuid&quot;,&lt;br&gt;
&quot;jti&quot;: &quot;access-token-uuid&quot;,&lt;br&gt;
&quot;iat&quot;: 1790540000,&lt;br&gt;
&quot;exp&quot;: 1790540900&lt;br&gt;
}&lt;br&gt;
Authentication now involves two checks:&lt;ol&gt;
&lt;li&gt;Verify the JWT signature and expiration&lt;/li&gt;
&lt;li&gt;Validate the persistent session referenced by sid&lt;br&gt;
This architecture preserves the advantages of JWTs while restoring the ability to revoke access immediately.&lt;br&gt;
If the user removes a device from the account-security page, the session becomes invalid even if its most recent JWT has not expired.&lt;br&gt;
Opaque refresh tokens&lt;br&gt;
The refresh token is not a JWT and does not contain trusted user data.&lt;br&gt;
Its conceptual format is:&lt;br&gt;
myzr.&amp;lt;session-id&amp;gt;.&amp;lt;cryptographically-random-secret&amp;gt;&lt;br&gt;
The random section is produced using a cryptographically secure random-number generator.&lt;br&gt;
The complete refresh token is sent to the authenticated browser, but it is never stored directly in MongoDB.&lt;br&gt;
Instead, the backend calculates:&lt;br&gt;
HMAC-SHA256(server secret, raw refresh token)&lt;br&gt;
Only the resulting digest is saved.&lt;br&gt;
This prevents a database leak from immediately exposing usable refresh credentials.&lt;br&gt;
Protected authentication cookies&lt;br&gt;
Refresh tokens are stored in cookies configured with:&lt;br&gt;
HttpOnly&lt;br&gt;
SameSite=Lax&lt;br&gt;
Secure in production&lt;br&gt;
Path=/api/auth&lt;br&gt;
HttpOnly prevents frontend JavaScript from reading the credential.&lt;br&gt;
The restricted path reduces the number of requests carrying the refresh cookie.&lt;br&gt;
Access tokens can also be received through an HttpOnly cookie. Bearer-token authentication remains temporarily supported to avoid breaking clients during the migration.&lt;br&gt;
Atomic refresh-token rotation&lt;br&gt;
A refresh token is single-use.&lt;br&gt;
When the browser calls:&lt;br&gt;
POST /api/auth/refresh&lt;br&gt;
the backend:&lt;/li&gt;
&lt;li&gt;reads the refresh cookie;&lt;/li&gt;
&lt;li&gt;extracts the session identifier;&lt;/li&gt;
&lt;li&gt;calculates the HMAC digest of the submitted token;&lt;/li&gt;
&lt;li&gt;generates a new opaque token;&lt;/li&gt;
&lt;li&gt;atomically compares the submitted hash with the current stored hash;&lt;/li&gt;
&lt;li&gt;replaces the stored hash;&lt;/li&gt;
&lt;li&gt;records the consumed hash;&lt;/li&gt;
&lt;li&gt;issues a new access token;&lt;/li&gt;
&lt;li&gt;sends new access and refresh cookies.&lt;br&gt;
The database update only succeeds if:&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;the session exists;&lt;/li&gt;
&lt;li&gt;the session has not expired;&lt;/li&gt;
&lt;li&gt;the session has not been revoked;&lt;/li&gt;
&lt;li&gt;the submitted hash is still the current refresh-token hash.&lt;br&gt;
This is effectively a compare-and-swap operation.&lt;br&gt;
If two requests try to use the same refresh token simultaneously, only one can rotate it successfully.&lt;br&gt;
Refresh-token replay detection&lt;br&gt;
Rotation prevents normal reuse, but the system must also detect when an old token appears again.&lt;br&gt;
After every successful rotation, the consumed token hash is placed in a bounded history.&lt;br&gt;
If a token from that history is submitted again, the server treats it as a possible credential compromise.&lt;br&gt;
Previously consumed token submitted&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replay detected&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Entire server-side session revoked&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Authentication cookies cleared&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A new login is required&lt;br&gt;
The session receives the revocation reason:&lt;br&gt;
refresh-token-replay&lt;br&gt;
The history is limited to the latest 50 hashes so the MongoDB document cannot grow indefinitely.&lt;br&gt;
Replay does not necessarily prove malicious activity. It could also indicate an outdated client or an unusual concurrency condition. Revoking the session is nevertheless the safest response.&lt;br&gt;
New authentication endpoints&lt;br&gt;
The current implementation provides the following endpoints.&lt;br&gt;
Refresh the session&lt;br&gt;
POST /api/auth/refresh&lt;br&gt;
This endpoint rotates the refresh token and issues a new access token.&lt;br&gt;
The raw refresh token is never included in the JSON response.&lt;br&gt;
Log out&lt;br&gt;
POST /api/auth/logout&lt;br&gt;
Logout is idempotent.&lt;br&gt;
Authentication cookies are cleared even when the access token is missing, malformed, expired or already revoked.&lt;br&gt;
When possible, the refresh token is used to find and revoke the persistent session.&lt;br&gt;
Retrieve the authenticated account&lt;br&gt;
GET /api/auth/me&lt;br&gt;
Returns the authenticated user and safe session metadata.&lt;br&gt;
List active devices&lt;br&gt;
GET /api/auth/me/sessions&lt;br&gt;
Returns the user’s active sessions and indicates the current device.&lt;br&gt;
Revoke a device&lt;br&gt;
DELETE /api/auth/me/sessions/:sessionId&lt;br&gt;
Allows users to terminate another session remotely.&lt;br&gt;
Revoking the current session also clears local authentication state and returns the user to the login flow.&lt;br&gt;
Password and social authentication&lt;br&gt;
Password registration and login now create persistent sessions and issue both access and refresh cookies.&lt;br&gt;
Verified social-login flows use the same architecture.&lt;br&gt;
The raw refresh token is not placed inside:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;redirect URLs;&lt;/li&gt;
&lt;li&gt;OAuth query parameters;&lt;/li&gt;
&lt;li&gt;signed callback tickets;&lt;/li&gt;
&lt;li&gt;public API responses.&lt;br&gt;
This reduces the risk of credentials leaking through browser history, analytics systems, proxy logs or referrer headers.&lt;br&gt;
Account-security interface&lt;br&gt;
The React frontend now includes:&lt;br&gt;
/account/security&lt;br&gt;
The page displays:&lt;/li&gt;
&lt;li&gt;active sessions;&lt;/li&gt;
&lt;li&gt;the current device;&lt;/li&gt;
&lt;li&gt;session creation time;&lt;/li&gt;
&lt;li&gt;last activity;&lt;/li&gt;
&lt;li&gt;expiration time;&lt;/li&gt;
&lt;li&gt;limited device information.&lt;br&gt;
Users can remotely revoke another device or terminate the current session.&lt;br&gt;
The page is linked from the authenticated account panel and Neon Plaza.&lt;br&gt;
The deployment configuration also supports the route as a single-page application path and applies:&lt;br&gt;
X-Robots-Tag: noindex, nofollow&lt;br&gt;
Account-security pages should not be indexed by search engines.&lt;br&gt;
Privacy-preserving session metadata&lt;br&gt;
The system does not store raw client IP addresses.&lt;br&gt;
Instead, it saves an HMAC-derived digest. This provides a limited correlation signal for security purposes without retaining the original IP address as normal application data.&lt;br&gt;
Device metadata is also intentionally limited.&lt;br&gt;
Session security should help users recognize their devices without becoming an invasive tracking mechanism.&lt;br&gt;
Legacy migration support&lt;br&gt;
The existing frontend still contains API clients that expect a token in localStorage or use:&lt;br&gt;
Authorization: Bearer &lt;br&gt;
Removing this immediately would break existing routes and clients.&lt;br&gt;
The migration is therefore divided into stages:&lt;br&gt;
Persistent sessions and cookie support&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rotating refresh tokens&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Centralized access-token renewal&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CSRF protection&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cookie-only authentication&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expiration of legacy browser tokens&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Strict server-session enforcement&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Removal of localStorage and bearer migration&lt;br&gt;
An environment option is available for strict enforcement:&lt;br&gt;
REQUIRE_SERVER_SESSION=true&lt;br&gt;
It should only be activated after existing clients and legacy credentials have been migrated.&lt;br&gt;
Testing the implementation&lt;br&gt;
The focused authentication implementation currently passes:&lt;br&gt;
Backend:  30/30 tests&lt;br&gt;
Frontend: 12/12 tests&lt;br&gt;
The React production build also completes successfully.&lt;br&gt;
Real MongoDB integration coverage verifies the complete replay lifecycle:&lt;br&gt;
Create persistent session&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use current refresh token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rotate successfully&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Submit the old token again&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reject the replay&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Revoke the server-side session&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Persist refresh-token-replay as the reason&lt;br&gt;
The following authentication suites also pass in GitHub Actions:&lt;br&gt;
tests/authSessionService.test.js&lt;br&gt;
tests/authSessionController.test.js&lt;br&gt;
tests/socialIdentityService.test.js&lt;br&gt;
Repository-wide CI status&lt;br&gt;
The complete repository pipeline is not yet green.&lt;br&gt;
The latest full backend execution reported:&lt;br&gt;
141 test suites passed&lt;br&gt;
42 test suites failed&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;738 tests passed&lt;br&gt;
44 tests failed&lt;br&gt;
The failing tests mainly concern pre-existing behavior in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zorgax payments and payment intents;&lt;/li&gt;
&lt;li&gt;Marketplace flows;&lt;/li&gt;
&lt;li&gt;seller-membership policies;&lt;/li&gt;
&lt;li&gt;older Metaverse authentication expectations;&lt;/li&gt;
&lt;li&gt;Metaverse observability;&lt;/li&gt;
&lt;li&gt;legacy source-code assertion tests.&lt;br&gt;
The Security Audit and MYZ-164 policy workflows also remain red because of previously identified repository-wide conditions.&lt;br&gt;
The new MYZ-71 session and refresh-token suites are green, but the pull request must remain a Draft until the wider baseline and the remaining authentication work are addressed.&lt;br&gt;
What still needs to be implemented&lt;br&gt;
Centralized access-token renewal&lt;br&gt;
The frontend needs a shared API layer that can:&lt;ol&gt;
&lt;li&gt;identify an expired access token;&lt;/li&gt;
&lt;li&gt;call /api/auth/refresh;&lt;/li&gt;
&lt;li&gt;retry the original request;&lt;/li&gt;
&lt;li&gt;prevent simultaneous refresh calls;&lt;/li&gt;
&lt;li&gt;redirect to login when renewal fails.&lt;br&gt;
Without a centralized implementation, individual pages may respond differently when the access token expires.&lt;br&gt;
CSRF protection&lt;br&gt;
Cookie-based authentication requires explicit protection for state-changing operations.&lt;br&gt;
The final design should combine appropriate controls such as:&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;origin validation;&lt;/li&gt;
&lt;li&gt;CSRF tokens;&lt;/li&gt;
&lt;li&gt;double-submit cookies;&lt;/li&gt;
&lt;li&gt;carefully selected SameSite policies;&lt;/li&gt;
&lt;li&gt;rejection of unsafe cross-origin requests.&lt;br&gt;
SameSite=Lax should not be considered the only defence.&lt;br&gt;
Removal of browser-stored tokens&lt;br&gt;
The current frontend still updates the legacy localStorage token as a temporary compatibility bridge.&lt;br&gt;
The final architecture should not expose reusable authentication credentials to frontend JavaScript.&lt;br&gt;
Before removing localStorage, we must migrate:&lt;/li&gt;
&lt;li&gt;every authenticated API client;&lt;/li&gt;
&lt;li&gt;protected React routes;&lt;/li&gt;
&lt;li&gt;realtime and websocket authentication;&lt;/li&gt;
&lt;li&gt;mobile or embedded clients;&lt;/li&gt;
&lt;li&gt;old sessions and legacy JWTs.&lt;br&gt;
Passkeys and magic links&lt;br&gt;
The MYZ-71 roadmap includes passwordless authentication based on flows such as:&lt;br&gt;
/auth/start&lt;br&gt;
/auth/verify&lt;br&gt;
Passkey support will require correct WebAuthn management for:&lt;/li&gt;
&lt;li&gt;relying-party configuration;&lt;/li&gt;
&lt;li&gt;allowed origins;&lt;/li&gt;
&lt;li&gt;challenge creation and expiration;&lt;/li&gt;
&lt;li&gt;credential counters;&lt;/li&gt;
&lt;li&gt;credential revocation;&lt;/li&gt;
&lt;li&gt;account recovery.&lt;br&gt;
Magic links will require short-lived, single-use verification tokens and replay protection.&lt;br&gt;
Step-up authentication&lt;br&gt;
Sensitive operations should require recent stronger verification.&lt;br&gt;
Examples include:&lt;/li&gt;
&lt;li&gt;changing the primary email;&lt;/li&gt;
&lt;li&gt;changing a password;&lt;/li&gt;
&lt;li&gt;registering or deleting passkeys;&lt;/li&gt;
&lt;li&gt;deleting the account;&lt;/li&gt;
&lt;li&gt;accessing wallet or payment functionality;&lt;/li&gt;
&lt;li&gt;modifying high-impact Marketplace settings.&lt;br&gt;
A valid session should not automatically authorize every sensitive action indefinitely.&lt;br&gt;
Password-change session policy&lt;br&gt;
The application must define what happens after a password change:&lt;/li&gt;
&lt;li&gt;revoke every session;&lt;/li&gt;
&lt;li&gt;keep only the current session;&lt;/li&gt;
&lt;li&gt;allow the user to choose;&lt;/li&gt;
&lt;li&gt;require step-up authentication.&lt;br&gt;
This policy must be enforced on the server and tested with real session records.&lt;br&gt;
Production OAuth validation&lt;br&gt;
Google, GitHub and Facebook login still need production verification for:&lt;/li&gt;
&lt;li&gt;exact callback URLs;&lt;/li&gt;
&lt;li&gt;public frontend and API origins;&lt;/li&gt;
&lt;li&gt;provider secrets;&lt;/li&gt;
&lt;li&gt;redirect allowlists;&lt;/li&gt;
&lt;li&gt;account-linking behavior;&lt;/li&gt;
&lt;li&gt;missing-email cases;&lt;/li&gt;
&lt;li&gt;cookie behavior across domains.&lt;br&gt;
MongoDB Atlas validation&lt;br&gt;
The production deployment must verify:&lt;/li&gt;
&lt;li&gt;TTL-index creation;&lt;/li&gt;
&lt;li&gt;refresh-token indexes;&lt;/li&gt;
&lt;li&gt;cleanup of expired sessions;&lt;/li&gt;
&lt;li&gt;document growth;&lt;/li&gt;
&lt;li&gt;rolling-deployment compatibility;&lt;/li&gt;
&lt;li&gt;session behavior under concurrent refresh requests.&lt;br&gt;
Rate limiting&lt;br&gt;
Authentication endpoints need dedicated abuse protection.&lt;br&gt;
This includes limits for:&lt;/li&gt;
&lt;li&gt;password login;&lt;/li&gt;
&lt;li&gt;refresh requests;&lt;/li&gt;
&lt;li&gt;password recovery;&lt;/li&gt;
&lt;li&gt;magic-link generation;&lt;/li&gt;
&lt;li&gt;WebAuthn challenge creation;&lt;/li&gt;
&lt;li&gt;social-auth callbacks.&lt;br&gt;
Security monitoring&lt;br&gt;
Structured security events should be produced for:&lt;/li&gt;
&lt;li&gt;login success and failure;&lt;/li&gt;
&lt;li&gt;session creation;&lt;/li&gt;
&lt;li&gt;session revocation;&lt;/li&gt;
&lt;li&gt;remote device removal;&lt;/li&gt;
&lt;li&gt;refresh-token replay;&lt;/li&gt;
&lt;li&gt;password changes;&lt;/li&gt;
&lt;li&gt;passkey registration;&lt;/li&gt;
&lt;li&gt;suspicious authentication activity.&lt;br&gt;
Logs must never contain passwords, raw access tokens, raw refresh tokens, OAuth authorization codes or provider secrets.&lt;br&gt;
Recommended deployment sequence&lt;br&gt;
The safest rollout order is:&lt;ol&gt;
&lt;li&gt;review the Draft pull request;&lt;/li&gt;
&lt;li&gt;deploy with legacy compatibility enabled;&lt;/li&gt;
&lt;li&gt;verify MongoDB indexes;&lt;/li&gt;
&lt;li&gt;validate cookies using the real public domains;&lt;/li&gt;
&lt;li&gt;test login, refresh, logout and remote revocation;&lt;/li&gt;
&lt;li&gt;execute a controlled refresh-token replay test;&lt;/li&gt;
&lt;li&gt;implement centralized frontend renewal;&lt;/li&gt;
&lt;li&gt;add and validate CSRF protection;&lt;/li&gt;
&lt;li&gt;monitor session and replay events;&lt;/li&gt;
&lt;li&gt;allow legacy credentials to expire;&lt;/li&gt;
&lt;li&gt;activate strict server-session enforcement;&lt;/li&gt;
&lt;li&gt;remove localStorage and legacy bearer authentication;&lt;/li&gt;
&lt;li&gt;introduce passkeys, magic links and step-up authentication.&lt;br&gt;
Current progress&lt;br&gt;
Persistent server-side sessions             DONE&lt;br&gt;
Session-bound JWT access tokens              DONE&lt;br&gt;
Real logout                                  DONE&lt;br&gt;
Active-device listing                        DONE&lt;br&gt;
Remote session revocation                    DONE&lt;br&gt;
Opaque rotating refresh tokens               DONE&lt;br&gt;
Refresh-token replay detection               DONE&lt;br&gt;
Password and social-login integration        DONE&lt;br&gt;
Account-security interface                   DONE&lt;br&gt;
Focused backend and frontend testing         DONE&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Centralized access-token renewal             PENDING&lt;br&gt;
CSRF hardening                               PENDING&lt;br&gt;
Cookie-only migration                        PENDING&lt;br&gt;
Removal of localStorage authentication       PENDING&lt;br&gt;
Passkeys and magic links                     PENDING&lt;br&gt;
Step-up authentication                       PENDING&lt;br&gt;
Production OAuth validation                  PENDING&lt;br&gt;
MongoDB Atlas production validation          PENDING&lt;br&gt;
Repository-wide green CI                     PENDING&lt;br&gt;
Final thoughts&lt;br&gt;
Secure authentication is not just about generating a token.&lt;br&gt;
It requires a complete lifecycle:&lt;br&gt;
Issue&lt;br&gt;
Verify&lt;br&gt;
Renew&lt;br&gt;
Observe&lt;br&gt;
Revoke&lt;br&gt;
Recover&lt;br&gt;
Monitor&lt;br&gt;
MyZubster now has the foundation for persistent multi-device sessions, secure refresh-token rotation and automatic replay response.&lt;br&gt;
The next major objective is completing the browser migration safely through centralized renewal, CSRF protection and the removal of JavaScript-accessible authentication credentials.&lt;br&gt;
Only after those controls are validated should strict session enforcement be activated in production.&lt;br&gt;
The current implementation can be reviewed in the Draft pull request:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/danieldirimini-myzubster/myzubster/pull/10&quot;&gt;https://github.com/danieldirimini-myzubster/myzubster/pull/10&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28847/how-built-revocable-sessions-and-refresh-token-replay-protection-for-myzubster-metaverse</guid>
<pubDate>Mon, 28 Sep 2026 01:35:52 +0000</pubDate>
</item>
<item>
<title>From Stateless JWTs to Revocable Sessions: Securing the MyZubster Metaverse</title>
<link>https://coderlegion.com/28825/from-stateless-jwts-to-revocable-sessions-securing-the-myzubster-metaverse</link>
<description>&lt;p&gt;From Stateless JWTs to Revocable Sessions: Securing the MyZubster Metaverse&lt;br&gt;
MyZubster is developing a persistent open-source digital ecosystem that connects verified identities, metaverse characters, community missions, knowledge, marketplaces, and contributor activity.&lt;br&gt;
As the platform grows, authentication can no longer mean simply issuing a JSON Web Token and trusting it until it expires.&lt;br&gt;
A persistent metaverse requires a stronger security model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;real server-side logout;&lt;/li&gt;
&lt;li&gt;session and device management;&lt;/li&gt;
&lt;li&gt;remote revocation;&lt;/li&gt;
&lt;li&gt;privacy-preserving activity metadata;&lt;/li&gt;
&lt;li&gt;secure browser cookies;&lt;/li&gt;
&lt;li&gt;structured authentication errors;&lt;/li&gt;
&lt;li&gt;compatibility with existing clients;&lt;/li&gt;
&lt;li&gt;a path toward passkeys and rotating refresh tokens.&lt;br&gt;
We have now implemented the first major security slice of MYZ-71 — Authentication, sessions and account security.&lt;br&gt;
The implementation is available in the following draft pull request:&lt;br&gt;
MYZ-71: Add revocable sessions and device security controls&lt;br&gt;
The pull request is still a draft. It has not been merged or deployed to production.&lt;br&gt;
Why stateless JWTs were no longer enough&lt;br&gt;
The previous authentication architecture issued a signed JWT containing information such as:&lt;br&gt;
{&lt;br&gt;
&quot;userId&quot;: &quot;USER_ID&quot;,&lt;br&gt;
&quot;username&quot;: &quot;daniel&quot;,&lt;br&gt;
&quot;role&quot;: &quot;user&quot;,&lt;br&gt;
&quot;iat&quot;: 1789970000,&lt;br&gt;
&quot;exp&quot;: 1790574800&lt;br&gt;
}&lt;br&gt;
The server could validate the signature and expiration time, but it could not immediately answer several important security questions:&lt;/li&gt;
&lt;li&gt;Did the user log out?&lt;/li&gt;
&lt;li&gt;Was the device remotely revoked?&lt;/li&gt;
&lt;li&gt;Is the token associated with a known active login?&lt;/li&gt;
&lt;li&gt;Was the session disabled after the token was created?&lt;/li&gt;
&lt;li&gt;Which browser created the session?&lt;/li&gt;
&lt;li&gt;Should this token be rejected before its cryptographic expiration?&lt;br&gt;
A purely stateless JWT remains valid until it expires. Logging out in the browser may remove a local token, but it does not necessarily invalidate a copied token.&lt;br&gt;
That limitation becomes especially important when an account controls:&lt;/li&gt;
&lt;li&gt;a persistent metaverse character;&lt;/li&gt;
&lt;li&gt;private rooms;&lt;/li&gt;
&lt;li&gt;creator permissions;&lt;/li&gt;
&lt;li&gt;marketplace activity;&lt;/li&gt;
&lt;li&gt;verified social identities;&lt;/li&gt;
&lt;li&gt;community moderation;&lt;/li&gt;
&lt;li&gt;contributor or reward-related actions.&lt;br&gt;
The new hybrid architecture&lt;br&gt;
We retained JWTs, but every newly issued JWT is now connected to a persistent server-side session.&lt;br&gt;
The new token includes two identifiers:&lt;br&gt;
{&lt;br&gt;
&quot;userId&quot;: &quot;USER_ID&quot;,&lt;br&gt;
&quot;username&quot;: &quot;daniel&quot;,&lt;br&gt;
&quot;role&quot;: &quot;user&quot;,&lt;br&gt;
&quot;sid&quot;: &quot;4cb9c3a1-8d39-4f2d-8a91-82c9556abc91&quot;,&lt;br&gt;
&quot;jti&quot;: &quot;48ab2bd5-b4fa-426a-af42-3e24758e0434&quot;,&lt;br&gt;
&quot;iat&quot;: 1789970000,&lt;br&gt;
&quot;exp&quot;: 1790574800&lt;br&gt;
}&lt;br&gt;
The sid claim identifies the persistent session. The jti uniquely identifies the issued JWT.&lt;br&gt;
Authentication now follows this sequence:&lt;br&gt;
Request&lt;br&gt;
↓&lt;br&gt;
Extract token from secure cookie or Authorization header&lt;br&gt;
↓&lt;br&gt;
Verify JWT signature and expiration&lt;br&gt;
↓&lt;br&gt;
Read the sid claim&lt;br&gt;
↓&lt;br&gt;
Find the corresponding server-side session&lt;br&gt;
↓&lt;br&gt;
Verify ownership, expiration and revocation state&lt;br&gt;
↓&lt;br&gt;
Allow or reject the request&lt;br&gt;
A valid JWT is no longer sufficient when its server-side session has been revoked or expired.&lt;br&gt;
Persistent AuthSession records&lt;br&gt;
A new MongoDB model stores the authentication state.&lt;br&gt;
Conceptually, the model contains:&lt;br&gt;
{&lt;br&gt;
sessionId: String,&lt;br&gt;
userId: ObjectId,&lt;br&gt;
userAgent: String,&lt;br&gt;
ipHash: String,&lt;br&gt;
createdAt: Date,&lt;br&gt;
lastSeenAt: Date,&lt;br&gt;
expiresAt: Date,&lt;br&gt;
revokedAt: Date,&lt;br&gt;
revokedReason: String&lt;br&gt;
}&lt;br&gt;
Session creation&lt;br&gt;
Every password or social login creates a cryptographically random session identifier:&lt;br&gt;
const sessionId = crypto.randomUUID();&lt;br&gt;
A JWT is then signed with that identifier:&lt;br&gt;
const token = jwt.sign(&lt;br&gt;
{&lt;br&gt;
  userId: String(user._id),&lt;br&gt;
  username: user.username,&lt;br&gt;
  role: user.role,&lt;br&gt;
  sid: sessionId&lt;br&gt;
},&lt;br&gt;
jwtSecret,&lt;br&gt;
{&lt;br&gt;
  expiresIn: sessionTtl,&lt;br&gt;
  jwtid: crypto.randomUUID()&lt;br&gt;
}&lt;br&gt;
);&lt;br&gt;
The session document and token are created as part of the same authentication flow.&lt;br&gt;
Expiration enforcement&lt;br&gt;
The session contains an explicit expiration timestamp and a MongoDB TTL index.&lt;br&gt;
However, MongoDB TTL cleanup is asynchronous. An expired document may remain in the collection temporarily.&lt;br&gt;
For this reason, authentication never relies only on physical deletion. The validation query requires:&lt;br&gt;
{&lt;br&gt;
sessionId: decoded.sid,&lt;br&gt;
userId: decoded.userId,&lt;br&gt;
revokedAt: null,&lt;br&gt;
expiresAt: { $gt: new Date() }&lt;br&gt;
}&lt;br&gt;
The session becomes unusable immediately after expiration, even if MongoDB has not deleted it yet.&lt;br&gt;
Revocation&lt;br&gt;
Revocation does not delete the session immediately. Instead, the server records when and why it was revoked:&lt;br&gt;
{&lt;br&gt;
revokedAt: new Date(),&lt;br&gt;
revokedReason: &quot;user-revoked&quot;&lt;br&gt;
}&lt;br&gt;
Keeping this state is useful for security analysis and makes the revocation operation auditable.&lt;br&gt;
Privacy-preserving IP metadata&lt;br&gt;
The server does not store raw IP addresses.&lt;br&gt;
When an address is available, it is converted into an HMAC-SHA256 digest:&lt;br&gt;
const ipHash = crypto&lt;br&gt;
.createHmac(&quot;sha256&quot;, SESSION_IP_HASH_SECRET)&lt;br&gt;
.update(ipAddress)&lt;br&gt;
.digest(&quot;hex&quot;);&lt;br&gt;
This allows the application to identify meaningful changes between login contexts without creating a database of clear-text IP addresses.&lt;br&gt;
The HMAC secret prevents the value from behaving like a simple unsalted hash that could be easily precomputed.&lt;br&gt;
Last-activity tracking without excessive writes&lt;br&gt;
The session records lastSeenAt, but it is not updated on every request.&lt;br&gt;
Updating the database for every authenticated API call would create unnecessary write volume.&lt;br&gt;
Instead, the timestamp is refreshed only when the previous update is older than five minutes:&lt;br&gt;
if (Date.now() - lastSeenAt &amp;gt; FIVE_MINUTES) {&lt;br&gt;
await AuthSession.updateOne(&lt;br&gt;
  {&lt;pre&gt;&lt;code&gt;sessionId: decoded.sid,
revokedAt: null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  },&lt;br&gt;
  {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$set: {
  lastSeenAt: new Date()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;br&gt;
);&lt;br&gt;
}&lt;br&gt;
This provides useful account activity information while reducing database pressure.&lt;br&gt;
Secure cookies and compatibility mode&lt;br&gt;
The authentication token can now be received from either:&lt;br&gt;
Authorization: Bearer TOKEN&lt;br&gt;
or the following browser cookie:&lt;br&gt;
myzubster_session&lt;br&gt;
The cookie uses these properties:&lt;br&gt;
{&lt;br&gt;
httpOnly: true,&lt;br&gt;
secure: production,&lt;br&gt;
sameSite: &quot;lax&quot;,&lt;br&gt;
path: &quot;/&quot;,&lt;br&gt;
maxAge: sessionTtl&lt;br&gt;
}&lt;br&gt;
Why use HttpOnly?&lt;br&gt;
Browser JavaScript cannot directly read an HttpOnly cookie.&lt;br&gt;
This limits simple token extraction through code such as:&lt;br&gt;
document.cookie&lt;br&gt;
It does not solve every XSS scenario, but it substantially reduces the exposure of authentication secrets.&lt;br&gt;
Why bearer tokens are still supported&lt;br&gt;
MyZubster already has clients that use tokens stored by older authentication flows.&lt;br&gt;
Removing bearer support immediately would invalidate existing sessions and potentially break multiple parts of the frontend.&lt;br&gt;
The system therefore supports a migration period:&lt;br&gt;
Legacy client&lt;br&gt;
→ Authorization: Bearer &lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;New browser session&lt;br&gt;
  → HttpOnly myzubster_session cookie&lt;br&gt;
Older JWTs may also lack an sid claim. They remain temporarily compatible unless strict mode is enabled:&lt;br&gt;
REQUIRE_SERVER_SESSION=true&lt;br&gt;
Once existing tokens have expired, strict mode can reject every JWT that is not connected to a server session.&lt;br&gt;
New authentication endpoints&lt;br&gt;
Four important endpoints were added.&lt;br&gt;
Current account&lt;br&gt;
GET /api/auth/me&lt;br&gt;
Returns the authenticated user and associated public character information.&lt;br&gt;
Active devices and sessions&lt;br&gt;
GET /api/auth/me/sessions&lt;br&gt;
Example response:&lt;br&gt;
{&lt;br&gt;
  &quot;success&quot;: true,&lt;br&gt;
  &quot;request_id&quot;: &quot;bf7a957e-1669-4432-a9a4-e686cb70cc07&quot;,&lt;br&gt;
  &quot;sessions&quot;: [&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;id&quot;: &quot;4cb9c3a1-8d39-4f2d-8a91-82c9556abc91&quot;,
  &quot;current&quot;: true,
  &quot;device&quot;: &quot;Mozilla/5.0 ...&quot;,
  &quot;createdAt&quot;: &quot;2026-09-27T08:00:00.000Z&quot;,
  &quot;lastSeenAt&quot;: &quot;2026-09-27T09:00:00.000Z&quot;,
  &quot;expiresAt&quot;: &quot;2026-10-04T08:00:00.000Z&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ]&lt;br&gt;
}&lt;br&gt;
Only sessions belonging to the authenticated user are returned.&lt;br&gt;
Revoke a session&lt;br&gt;
DELETE /api/auth/me/sessions/:sessionId&lt;br&gt;
The update is scoped by both the user and session identifier:&lt;br&gt;
{&lt;br&gt;
  userId: authenticatedUserId,&lt;br&gt;
  sessionId,&lt;br&gt;
  revokedAt: null&lt;br&gt;
}&lt;br&gt;
This prevents one account from revoking another user’s session.&lt;br&gt;
Logout&lt;br&gt;
POST /api/auth/logout&lt;br&gt;
Logout is deliberately idempotent.&lt;br&gt;
The cookie is cleared even when the submitted token is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;expired;&lt;/li&gt;
&lt;li&gt;malformed;&lt;/li&gt;
&lt;li&gt;already revoked;&lt;/li&gt;
&lt;li&gt;disconnected from a server session.&lt;br&gt;
The logout route is intentionally not protected by the standard authentication middleware. Otherwise, an expired token could be rejected before the handler had an opportunity to clear the invalid cookie.&lt;br&gt;
When a valid session is available, logout also revokes it server-side.&lt;br&gt;
Structured authentication errors&lt;br&gt;
Authentication errors now use stable public codes instead of exposing raw JWT library messages.&lt;br&gt;
Example:&lt;br&gt;
{&lt;br&gt;
&quot;success&quot;: false,&lt;br&gt;
&quot;request_id&quot;: &quot;5fe11522-c804-4907-a4d1-0f459f94ab2a&quot;,&lt;br&gt;
&quot;error&quot;: {&lt;br&gt;
  &quot;code&quot;: &quot;AUTH_SESSION_REVOKED&quot;,&lt;br&gt;
  &quot;message&quot;: &quot;Session expired or revoked&quot;&lt;br&gt;
}&lt;br&gt;
}&lt;br&gt;
Current error categories include:&lt;br&gt;
AUTH_TOKEN_MISSING&lt;br&gt;
AUTH_TOKEN_INVALID&lt;br&gt;
AUTH_TOKEN_EXPIRED&lt;br&gt;
AUTH_SESSION_REVOKED&lt;br&gt;
Each response contains a request ID that can be correlated with server logs.&lt;br&gt;
This is useful for debugging while avoiding the disclosure of stack traces, signing details, or internal JWT parsing errors.&lt;br&gt;
Integration with social authentication&lt;br&gt;
Persistent sessions are now created for:&lt;/li&gt;
&lt;li&gt;registration;&lt;/li&gt;
&lt;li&gt;password login;&lt;/li&gt;
&lt;li&gt;Google login;&lt;/li&gt;
&lt;li&gt;GitHub login;&lt;/li&gt;
&lt;li&gt;Facebook login.&lt;br&gt;
The social OAuth ticket exchange now creates the same server-side session used by password authentication and sets the secure cookie.&lt;br&gt;
During integration testing, we also corrected two existing issues.&lt;br&gt;
GitHub profile persistence&lt;br&gt;
An undefined nested GitHub public-profile snapshot could cause a Mongoose casting error.&lt;br&gt;
The new implementation updates nested data explicitly and writes the public snapshot only when a defined value is available.&lt;br&gt;
Facebook accounts without email&lt;br&gt;
Not every Facebook account exposes an email address.&lt;br&gt;
MyZubster already supported a deterministic private relay identity for this situation. The integration tests were aligned with that intended behavior instead of incorrectly requiring every account to provide an email.&lt;br&gt;
The relay value is treated as an internal identifier, not as a verified personal email.&lt;br&gt;
The account-security interface&lt;br&gt;
A new React page is available at:&lt;br&gt;
/account/security&lt;br&gt;
It displays:&lt;/li&gt;
&lt;li&gt;the authenticated account;&lt;/li&gt;
&lt;li&gt;all active devices;&lt;/li&gt;
&lt;li&gt;the current device;&lt;/li&gt;
&lt;li&gt;session creation time;&lt;/li&gt;
&lt;li&gt;last activity;&lt;/li&gt;
&lt;li&gt;session expiration;&lt;/li&gt;
&lt;li&gt;controls for remote revocation;&lt;/li&gt;
&lt;li&gt;current-session termination;&lt;/li&gt;
&lt;li&gt;logout.&lt;br&gt;
The page is linked from:&lt;/li&gt;
&lt;li&gt;the authenticated account panel on the MyZubster home interface;&lt;/li&gt;
&lt;li&gt;the Neon Plaza metaverse top bar.&lt;br&gt;
Frontend API module&lt;br&gt;
The React application uses a dedicated session API:&lt;br&gt;
getCurrentAccount();&lt;br&gt;
getAuthSessions();&lt;br&gt;
revokeAuthSession(sessionId);&lt;br&gt;
logoutCurrentSession();&lt;br&gt;
clearBrowserAuth();&lt;br&gt;
Requests include same-origin cookies:&lt;br&gt;
fetch(path, {&lt;br&gt;
credentials: &quot;same-origin&quot;,&lt;br&gt;
headers: {&lt;br&gt;
  ...bearerMigrationFallback&lt;br&gt;
}&lt;br&gt;
});&lt;br&gt;
Structured server errors are preserved so the UI can display a support request ID when an operation fails.&lt;br&gt;
Remote device revocation&lt;br&gt;
When the user revokes another device:&lt;ol&gt;
&lt;li&gt;the UI requests confirmation;&lt;/li&gt;
&lt;li&gt;a session-specific DELETE request is sent;&lt;/li&gt;
&lt;li&gt;the server verifies session ownership;&lt;/li&gt;
&lt;li&gt;the session receives a revocation timestamp;&lt;/li&gt;
&lt;li&gt;the frontend removes it from the active list;&lt;/li&gt;
&lt;li&gt;the revoked device is rejected on its next request.&lt;br&gt;
Current-device termination&lt;br&gt;
Terminating the current session:&lt;/li&gt;
&lt;li&gt;revokes the server-side session;&lt;/li&gt;
&lt;li&gt;clears the secure cookie;&lt;/li&gt;
&lt;li&gt;removes legacy browser credentials;&lt;/li&gt;
&lt;li&gt;redirects the user through authentication again.&lt;br&gt;
Legacy browser keys removed during logout include:&lt;br&gt;
myzubster-token&lt;br&gt;
token&lt;br&gt;
accessToken&lt;br&gt;
myzubster-user&lt;br&gt;
myzubster-identity-provider&lt;br&gt;
myzubster-metaverse-character-id&lt;br&gt;
Protected production routing&lt;br&gt;
The new account page is configured as an explicit Vercel SPA route.&lt;br&gt;
It includes:&lt;br&gt;
X-Robots-Tag: noindex, nofollow&lt;br&gt;
Account-security pages should not be indexed, cached as search snippets, or treated as public content.&lt;br&gt;
Both the root and standalone frontend Vercel configurations were updated and validated as JSON.&lt;br&gt;
Testing and verification&lt;br&gt;
Backend&lt;br&gt;
Five focused backend suites pass:&lt;br&gt;
authSessionService&lt;br&gt;
authMiddlewareSessions&lt;br&gt;
authSessionController&lt;br&gt;
socialAuthCallback&lt;br&gt;
socialIdentityService&lt;br&gt;
Results:&lt;br&gt;
5 suites passed&lt;br&gt;
24 tests passed&lt;br&gt;
Coverage includes:&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;persistent session creation;&lt;/li&gt;
&lt;li&gt;bearer and cookie extraction;&lt;/li&gt;
&lt;li&gt;active-session validation;&lt;/li&gt;
&lt;li&gt;revoked-session rejection;&lt;/li&gt;
&lt;li&gt;token-expiration handling;&lt;/li&gt;
&lt;li&gt;structured errors;&lt;/li&gt;
&lt;li&gt;session listing;&lt;/li&gt;
&lt;li&gt;remote revocation;&lt;/li&gt;
&lt;li&gt;valid logout;&lt;/li&gt;
&lt;li&gt;malformed-token logout;&lt;/li&gt;
&lt;li&gt;Google authentication;&lt;/li&gt;
&lt;li&gt;GitHub authentication;&lt;/li&gt;
&lt;li&gt;Facebook authentication;&lt;/li&gt;
&lt;li&gt;persistent metaverse characters.&lt;br&gt;
The social identity tests use MongoDB Memory Server so the persistence behavior is exercised rather than completely mocked.&lt;br&gt;
Frontend&lt;br&gt;
Three focused frontend suites pass:&lt;br&gt;
authSessions API&lt;br&gt;
AccountSecurityPage&lt;br&gt;
MetaversePage&lt;br&gt;
Results:&lt;br&gt;
3 suites passed&lt;br&gt;
11 tests passed&lt;br&gt;
The tests cover:&lt;/li&gt;
&lt;li&gt;same-origin credentials;&lt;/li&gt;
&lt;li&gt;bearer-token migration;&lt;/li&gt;
&lt;li&gt;safe session-ID encoding;&lt;/li&gt;
&lt;li&gt;structured server errors;&lt;/li&gt;
&lt;li&gt;local credential cleanup;&lt;/li&gt;
&lt;li&gt;account and session rendering;&lt;/li&gt;
&lt;li&gt;current-device labeling;&lt;/li&gt;
&lt;li&gt;remote revocation;&lt;/li&gt;
&lt;li&gt;removal of revoked devices;&lt;/li&gt;
&lt;li&gt;safe date formatting;&lt;/li&gt;
&lt;li&gt;existing Neon Plaza mission-state behavior.&lt;br&gt;
The React production bundle also compiles successfully.&lt;br&gt;
Repository-wide CI status&lt;br&gt;
The pull request remains a draft because repository-wide CI still contains failures outside this authentication change.&lt;br&gt;
A previous complete run reported:&lt;br&gt;
149 test suites passed&lt;br&gt;
34 test suites failed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;774 tests passed&lt;br&gt;
44 tests failed&lt;br&gt;
The sampled failures were primarily related to existing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zorgax payment code;&lt;/li&gt;
&lt;li&gt;cultural modules;&lt;/li&gt;
&lt;li&gt;Kefir workflows;&lt;/li&gt;
&lt;li&gt;Marketplace contract assertions;&lt;/li&gt;
&lt;li&gt;realtime behavior;&lt;/li&gt;
&lt;li&gt;observability source assertions.&lt;br&gt;
The dependency audit also reports vulnerabilities from the unchanged lockfile. This authentication work does not modify package.json or package-lock.json.&lt;br&gt;
These failures should remain visible, but unrelated systems should not be rewritten inside an authentication PR without appropriate scope and review.&lt;br&gt;
What remains to be implemented&lt;br&gt;
This pull request does not complete MYZ-71.&lt;br&gt;
Rotating refresh tokens&lt;br&gt;
The final architecture should use:&lt;br&gt;
Short-lived access token&lt;pre&gt;&lt;code&gt;  +
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rotating refresh token&lt;br&gt;
The refresh token should:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;be cryptographically random;&lt;/li&gt;
&lt;li&gt;be stored only as a hash;&lt;/li&gt;
&lt;li&gt;belong to a token family;&lt;/li&gt;
&lt;li&gt;be consumed exactly once;&lt;/li&gt;
&lt;li&gt;rotate after every refresh;&lt;/li&gt;
&lt;li&gt;detect reuse;&lt;/li&gt;
&lt;li&gt;revoke the family after suspected replay.&lt;br&gt;
A conditional database operation will be necessary to prevent two simultaneous requests from consuming the same token successfully.&lt;br&gt;
Passkeys and WebAuthn&lt;br&gt;
The planned API includes:&lt;br&gt;
POST /api/auth/start&lt;br&gt;
POST /api/auth/verify&lt;br&gt;
Passkey verification must validate:&lt;/li&gt;
&lt;li&gt;the challenge;&lt;/li&gt;
&lt;li&gt;origin;&lt;/li&gt;
&lt;li&gt;relying-party ID;&lt;/li&gt;
&lt;li&gt;credential ID;&lt;/li&gt;
&lt;li&gt;public-key signature;&lt;/li&gt;
&lt;li&gt;authenticator data;&lt;/li&gt;
&lt;li&gt;user presence;&lt;/li&gt;
&lt;li&gt;user verification;&lt;/li&gt;
&lt;li&gt;authenticator signature counters.&lt;br&gt;
The account UI will also need credential listing, naming and revocation.&lt;br&gt;
Magic-link authentication&lt;br&gt;
Magic links require:&lt;/li&gt;
&lt;li&gt;single-use random challenges;&lt;/li&gt;
&lt;li&gt;hashed challenge storage;&lt;/li&gt;
&lt;li&gt;short expiration;&lt;/li&gt;
&lt;li&gt;rate limiting;&lt;/li&gt;
&lt;li&gt;account-enumeration resistance;&lt;/li&gt;
&lt;li&gt;safe redirect validation;&lt;/li&gt;
&lt;li&gt;protection against email link scanners;&lt;/li&gt;
&lt;li&gt;invalidation after first use.&lt;br&gt;
Step-up authentication&lt;br&gt;
Sensitive operations should require recent stronger authentication.&lt;br&gt;
Examples include:&lt;/li&gt;
&lt;li&gt;changing an email address;&lt;/li&gt;
&lt;li&gt;changing payment information;&lt;/li&gt;
&lt;li&gt;adding or removing passkeys;&lt;/li&gt;
&lt;li&gt;exporting sensitive data;&lt;/li&gt;
&lt;li&gt;changing privileges;&lt;/li&gt;
&lt;li&gt;deleting an account.&lt;br&gt;
The server will need to distinguish between a valid session and a recently re-verified session.&lt;br&gt;
CSRF protection&lt;br&gt;
SameSite=Lax is a useful baseline, but it is not the complete strategy for a cookie-authenticated application.&lt;br&gt;
Before the system becomes cookie-only, state-changing requests should add explicit protection through techniques such as:&lt;/li&gt;
&lt;li&gt;origin validation;&lt;/li&gt;
&lt;li&gt;synchronizer tokens;&lt;/li&gt;
&lt;li&gt;signed double-submit cookies;&lt;/li&gt;
&lt;li&gt;required custom headers.&lt;br&gt;
Removing browser-readable tokens&lt;br&gt;
Bearer compatibility and local-storage cleanup exist for migration purposes.&lt;br&gt;
The intended final browser architecture is:&lt;br&gt;
Credentials:&lt;br&gt;
HttpOnly secure cookies&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Frontend JavaScript:&lt;br&gt;
  no direct access to reusable authentication secrets&lt;br&gt;
Once the migration period ends, MyZubster should stop returning long-lived authentication tokens to frontend JavaScript.&lt;br&gt;
Production validation&lt;br&gt;
The following checks remain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;secure cookie verification on the public HTTPS domain;&lt;/li&gt;
&lt;li&gt;remote revocation between two physical devices;&lt;/li&gt;
&lt;li&gt;GitHub OAuth callback verification in production;&lt;/li&gt;
&lt;li&gt;Google and Facebook regression checks;&lt;/li&gt;
&lt;li&gt;MongoDB TTL-index verification;&lt;/li&gt;
&lt;li&gt;investigation of intermittent Atlas ReplicaSetNoPrimary events;&lt;/li&gt;
&lt;li&gt;strict-session migration testing;&lt;/li&gt;
&lt;li&gt;request-ID propagation through production logs and proxies.&lt;br&gt;
Why this is important for the metaverse&lt;br&gt;
Authentication is not separate from the metaverse. It is the foundation of persistent identity.&lt;br&gt;
The MyZubster identity chain is becoming:&lt;br&gt;
MyZubster account&lt;br&gt;
↓&lt;br&gt;
Revocable server session&lt;br&gt;
↓&lt;br&gt;
Verified character&lt;br&gt;
↓&lt;br&gt;
Neon Plaza participation&lt;br&gt;
↓&lt;br&gt;
Missions and contributions&lt;br&gt;
↓&lt;br&gt;
Knowledge, marketplace and community activity&lt;br&gt;
If a token is stolen, revocation must also protect the connected metaverse character and its permissions.&lt;br&gt;
This session infrastructure prepares the platform for:&lt;/li&gt;
&lt;li&gt;private metaverse rooms;&lt;/li&gt;
&lt;li&gt;verified contributors;&lt;/li&gt;
&lt;li&gt;creator permissions;&lt;/li&gt;
&lt;li&gt;moderation roles;&lt;/li&gt;
&lt;li&gt;university pilots;&lt;/li&gt;
&lt;li&gt;community governance;&lt;/li&gt;
&lt;li&gt;marketplace actions;&lt;/li&gt;
&lt;li&gt;development requests;&lt;/li&gt;
&lt;li&gt;contribution evidence.&lt;br&gt;
Current status&lt;br&gt;
Implemented:&lt;/li&gt;
&lt;li&gt;persistent authentication sessions;&lt;/li&gt;
&lt;li&gt;JWT-to-session binding;&lt;/li&gt;
&lt;li&gt;secure cookies;&lt;/li&gt;
&lt;li&gt;bearer-token migration;&lt;/li&gt;
&lt;li&gt;session expiration;&lt;/li&gt;
&lt;li&gt;device listing;&lt;/li&gt;
&lt;li&gt;remote revocation;&lt;/li&gt;
&lt;li&gt;server-side logout;&lt;/li&gt;
&lt;li&gt;privacy-preserving IP hashing;&lt;/li&gt;
&lt;li&gt;structured errors and request IDs;&lt;/li&gt;
&lt;li&gt;social-login integration;&lt;/li&gt;
&lt;li&gt;account-security UI;&lt;/li&gt;
&lt;li&gt;Neon Plaza integration;&lt;/li&gt;
&lt;li&gt;protected Vercel routing;&lt;/li&gt;
&lt;li&gt;focused backend and frontend tests.&lt;br&gt;
Still required:&lt;/li&gt;
&lt;li&gt;refresh-token rotation;&lt;/li&gt;
&lt;li&gt;replay detection;&lt;/li&gt;
&lt;li&gt;passkeys;&lt;/li&gt;
&lt;li&gt;magic links;&lt;/li&gt;
&lt;li&gt;step-up authentication;&lt;/li&gt;
&lt;li&gt;CSRF hardening;&lt;/li&gt;
&lt;li&gt;removal of browser-readable credentials;&lt;/li&gt;
&lt;li&gt;production OAuth verification;&lt;/li&gt;
&lt;li&gt;Atlas stability verification;&lt;/li&gt;
&lt;li&gt;repository-wide CI cleanup.&lt;br&gt;
This is an important security milestone, but it is not a declaration that authentication is finished.&lt;br&gt;
The goal is to make every security claim testable, observable and reversible during rollout.&lt;br&gt;
In MyZubster, trust should not depend on promises. It should be built through verifiable behavior.&lt;br&gt;
Suggested Coder Legion tags:&lt;br&gt;
Cybersecurity&lt;br&gt;
Node.js&lt;br&gt;
React&lt;br&gt;
Web Development&lt;br&gt;
Authentication&lt;br&gt;
Metaverse&lt;br&gt;
Suggested excerpt:&lt;br&gt;
How MyZubster is moving from stateless JWT authentication to persistent, revocable sessions with secure cookies, device management, structured errors, social-login integration and a React account-security dashboard.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28825/from-stateless-jwts-to-revocable-sessions-securing-the-myzubster-metaverse</guid>
<pubDate>Sun, 27 Sep 2026 18:29:37 +0000</pubDate>
</item>
<item>
<title>Database Concurrency 101: Optimistic vs. Pessimistic Locking</title>
<link>https://coderlegion.com/28791/database-concurrency-101-optimistic-vs-pessimistic-locking</link>
<description>&lt;p&gt;In modern software development, we often have to handle multiple users interacting with our application at the exact same time. Optimistic and pessimistic locking are two distinct strategies used to manage concurrency, which is the technical term for when multiple users or systems try to update the exact same database record simultaneously. While optimistic locking assumes conflicts are rare and verifies changes only at the very end of an operation, pessimistic locking assumes conflicts are highly likely and preemptively blocks others from accessing the data until the current operation is finished.&lt;/p&gt;
&lt;h4 id=&quot;a-relatable-everyday-analogy&quot;&gt;A Relatable Everyday Analogy&lt;/h4&gt;
&lt;p&gt;To understand the difference, imagine a collaborative digital workspace where you and your colleagues edit text files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pessimistic locking&lt;/strong&gt; is like an old-school file server where only one person can open a document for editing at a time. The moment you open the file, the server locks it. If your colleague tries to open it, they receive a &quot;Read-Only&quot; warning and are completely blocked from making edits until you save, close, and release your lock on the file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Optimistic locking&lt;/strong&gt;, on the other hand, is like a modern version control system like Git. Both you and your colleague can open, read, and edit the exact same document simultaneously. However, when you click &quot;Save,&quot; the system checks if anyone else has saved a new version of the document since you opened it. If no one has, your save succeeds. If your colleague managed to save their changes first, the system blocks your save, alerts you to a &quot;conflict,&quot; and asks you to review and merge the changes before trying again.&lt;/p&gt;
&lt;h4 id=&quot;why-it-matters-in-the-tech-industry&quot;&gt;Why It Matters in the Tech Industry&lt;/h4&gt;
&lt;p&gt;In the tech industry, failing to implement a proper locking strategy leads to a highly destructive bug known as a &quot;lost update&quot; or a &quot;race condition&quot;—where the outcome of a process depends on the unpredictable sequence or timing of other events. &lt;/p&gt;
&lt;p&gt;Imagine a financial application where two family members try to withdraw $50 from a shared $100 bank account at the exact same millisecond. Without locking, both transactions will read the balance as $100 simultaneously. Both transactions will approve the $50 withdrawal, and both will attempt to write the new balance of $50 back to the database. The bank is now down $100, but the database incorrectly shows a remaining balance of $50 because the second write silently overwrote the first one. By using database locking, software engineers prevent this double-spending, protect system integrity, and ensure transactions are processed safely.&lt;/p&gt;
&lt;h4 id=&quot;implementation-in-node-js-and-mysql&quot;&gt;Implementation in Node.js and MySQL&lt;/h4&gt;
&lt;p&gt;Here is a simple example of how to implement &lt;strong&gt;optimistic locking&lt;/strong&gt; in a Node.js application using a MySQL database client. We do this by adding a &lt;code&gt;version&lt;/code&gt; column to our database table and verifying it during our update query.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// A helper function to update a user's bank balance using Optimistic Locking
async function updateAccountBalance(connection, accountId, withdrawAmount, currentVersion) {
  // We attempt to update the balance and increment the version number,
  // but ONLY if the version in the database matches the version we originally read.
  const query = `
    UPDATE bank_accounts 
    SET balance = balance - ?, version = version + 1 
    WHERE id = ? AND version = ?
  `;

  const [result] = await connection.execute(query, [withdrawAmount, accountId, currentVersion]);

  // If no rows were affected, it means the version changed in the background
  // because another transaction updated it first. We must roll back and retry.
  if (result.affectedRows === 0) {
    throw new Error(&quot;Concurrency Conflict: The account was updated by another process. Please retry.&quot;);
  }

  return &quot;Balance updated successfully!&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-strategic-takeaway&quot;&gt;The Strategic Takeaway&lt;/h4&gt;
&lt;p&gt;Choosing between these two approaches is always a trade-off between speed and strict prevention. You should default to optimistic locking for applications with low-to-medium write conflicts because it keeps your application fast, highly available, and free of database bottlenecks. However, when dealing with highly critical operations where conflicts are frequent and data corruption is unacceptable—such as reservation systems or payment gateways—pessimistic locking is the superior choice, despite the performance hit of making users wait in a digital queue.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/how-to-handle-concurrent-writes.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28791/database-concurrency-101-optimistic-vs-pessimistic-locking</guid>
<pubDate>Sun, 27 Sep 2026 09:32:31 +0000</pubDate>
</item>
<item>
<title>From Broken Google OAuth to a Stable Sepolia E2E Environment: Engineering MyZubster on a VPS</title>
<link>https://coderlegion.com/28744/from-broken-google-oauth-to-stable-sepolia-e2e-environment-engineering-myzubster-on-vps</link>
<description>&lt;h2 id=&quot;from-broken-google-oauth-to-a-stable-sepolia-e2e-environment-engineering-myzubster-on-a-vps&quot;&gt;From Broken Google OAuth to a Stable Sepolia E2E Environment: Engineering MyZubster on a VPS&lt;/h2&gt;
&lt;p&gt;Over the last development cycle, we worked on stabilizing the MyZubster Sepolia environment and turning what was initially a collection of partially working services into a much more predictable end-to-end infrastructure.&lt;/p&gt;
&lt;p&gt;The work involved several layers of the stack:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js / Express backend&lt;/li&gt;
&lt;li&gt;Google OAuth 2.0&lt;/li&gt;
&lt;li&gt;React frontend&lt;/li&gt;
&lt;li&gt;Ethereum Sepolia testing&lt;/li&gt;
&lt;li&gt;MongoDB&lt;/li&gt;
&lt;li&gt;PM2 process management&lt;/li&gt;
&lt;li&gt;Cloudflare tunneling&lt;/li&gt;
&lt;li&gt;VPS deployment&lt;/li&gt;
&lt;li&gt;Environment configuration&lt;/li&gt;
&lt;li&gt;Urban Lab / I-ECO-01 services&lt;/li&gt;
&lt;li&gt;Git branch and deployment management&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This article documents what we fixed, how we diagnosed the problems, what is working now, and what still needs to be completed.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-starting-point&quot;&gt;1. The Starting Point&lt;/h3&gt;
&lt;p&gt;The Sepolia environment was running on a VPS with several services managed by PM2.&lt;/p&gt;
&lt;p&gt;The main components included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MyZubster Sepolia backend&lt;/li&gt;
&lt;li&gt;MyZubster Sepolia frontend&lt;/li&gt;
&lt;li&gt;MyZubster Social&lt;/li&gt;
&lt;li&gt;MyZubster Web&lt;/li&gt;
&lt;li&gt;Urban Lab / I-ECO-01&lt;/li&gt;
&lt;li&gt;Cloudflared&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The environment was operational, but two important areas were unstable.&lt;/p&gt;
&lt;p&gt;The first was Google authentication.&lt;/p&gt;
&lt;p&gt;Users could start the OAuth flow and Google returned the authorization callback, but the backend failed during the authorization-code exchange.&lt;/p&gt;
&lt;p&gt;The second problem was the Urban Lab service.&lt;/p&gt;
&lt;p&gt;It was trapped in a PM2 restart loop, generating millions of restarts because runtime dependencies required by &lt;code&gt;server.js&lt;/code&gt; were missing.&lt;/p&gt;
&lt;p&gt;We debugged these independently instead of treating the VPS as a single black box.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2-debugging-google-oauth&quot;&gt;2. Debugging Google OAuth&lt;/h2&gt;
&lt;p&gt;The original application error was generic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Login Google non riuscito
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That message was not enough to identify whether the problem was caused by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the authorization code,&lt;/li&gt;
&lt;li&gt;OAuth state,&lt;/li&gt;
&lt;li&gt;callback URI,&lt;/li&gt;
&lt;li&gt;client ID,&lt;/li&gt;
&lt;li&gt;client secret,&lt;/li&gt;
&lt;li&gt;Google configuration,&lt;/li&gt;
&lt;li&gt;proxy forwarding,&lt;/li&gt;
&lt;li&gt;or application logic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We therefore instrumented the token exchange temporarily.&lt;/p&gt;
&lt;p&gt;The relevant backend flow exchanges the authorization code against Google's token endpoint and then retrieves the verified Google user profile.&lt;/p&gt;
&lt;p&gt;The diagnostic logging revealed the actual failure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;status: 401
error: invalid_client
description: The provided client secret is invalid.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This immediately changed the investigation.&lt;/p&gt;
&lt;p&gt;The callback itself was reaching the backend correctly.&lt;/p&gt;
&lt;p&gt;The OAuth state existed.&lt;/p&gt;
&lt;p&gt;The authorization code existed.&lt;/p&gt;
&lt;p&gt;The problem was the credentials used during the token exchange.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3-validating-the-runtime-environment&quot;&gt;3. Validating the Runtime Environment&lt;/h2&gt;
&lt;p&gt;One of the important lessons from this debugging session was that validating &lt;code&gt;.env&lt;/code&gt; alone is not sufficient.&lt;/p&gt;
&lt;p&gt;We checked both the configuration file and the running PM2 process.&lt;/p&gt;
&lt;p&gt;The Google client configuration was inspected without printing the complete secret.&lt;/p&gt;
&lt;p&gt;We verified properties such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;CLIENT ID
SECRET PRESENT
SECRET LENGTH
SECRET FORMAT
CALLBACK URL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The configured callback was:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;https://sepolia.myzubster.com/api/auth/social/google/callback
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We then discovered that the previously configured secret was not a valid secret for the OAuth client being used.&lt;/p&gt;
&lt;p&gt;After replacing it with the correct Google OAuth client secret, the token exchange started succeeding.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;4-google-oauth-successfully-completed&quot;&gt;4. Google OAuth Successfully Completed&lt;/h2&gt;
&lt;p&gt;After correcting the OAuth credentials, the backend began reporting successful callbacks.&lt;/p&gt;
&lt;p&gt;The flow became:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Browser
   |
   v
Google Authorization
   |
   v
/api/auth/social/google/callback
   |
   v
Authorization Code Exchange
   |
   v
Google Access Token
   |
   v
Google UserInfo
   |
   v
MyZubster account verification
   |
   v
Social login ticket
   |
   v
Frontend
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The backend successfully received callbacks containing both the authorization code and OAuth state.&lt;/p&gt;
&lt;p&gt;Most importantly, the previous &lt;code&gt;invalid_client&lt;/code&gt; token error disappeared.&lt;/p&gt;
&lt;p&gt;The public provider endpoint also confirmed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;success&quot;: true,
  &quot;data&quot;: {
    &quot;providers&quot;: {
      &quot;google&quot;: true,
      &quot;github&quot;: false,
      &quot;facebook&quot;: false
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Google authentication is therefore enabled in the Sepolia environment.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;5-fixing-the-oauth-redirect&quot;&gt;5. Fixing the OAuth Redirect&lt;/h2&gt;
&lt;p&gt;Authentication succeeding at Google was only part of the problem.&lt;/p&gt;
&lt;p&gt;The backend was still redirecting successful logins to an old Vercel deployment.&lt;/p&gt;
&lt;p&gt;The runtime destination was effectively:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;old Vercel deployment
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;https://sepolia.myzubster.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We corrected the runtime environment so the relevant public URLs resolve to the Sepolia deployment.&lt;/p&gt;
&lt;p&gt;The running PM2 process was verified with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;FRONTEND_URL=https://sepolia.myzubster.com
GATEWAY_PUBLIC_URL=https://sepolia.myzubster.com
PUBLIC_APP_URL=https://sepolia.myzubster.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was an important distinction.&lt;/p&gt;
&lt;p&gt;Updating &lt;code&gt;.env&lt;/code&gt; is not necessarily equivalent to updating the environment of an already running process.&lt;/p&gt;
&lt;p&gt;For production-style deployments, the effective runtime configuration must always be verified.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;6-testing-the-complete-oauth-path&quot;&gt;6. Testing the Complete OAuth Path&lt;/h2&gt;
&lt;p&gt;We then tested the complete flow again.&lt;/p&gt;
&lt;p&gt;The callback reached the backend with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;hasCode: true
hasState: true
hasError: false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Google token exchange completed successfully.&lt;/p&gt;
&lt;p&gt;No new Google token error appeared.&lt;/p&gt;
&lt;p&gt;The backend remained online.&lt;/p&gt;
&lt;p&gt;The provider endpoint remained available.&lt;/p&gt;
&lt;p&gt;The PM2 process PID remained stable.&lt;/p&gt;
&lt;p&gt;This gave us evidence that the fix was not simply masking an application crash.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;7-removing-temporary-debug-instrumentation&quot;&gt;7. Removing Temporary Debug Instrumentation&lt;/h2&gt;
&lt;p&gt;Temporary instrumentation is useful during incident analysis, but it should not become permanent application noise.&lt;/p&gt;
&lt;p&gt;After confirming the OAuth path, we removed the temporary HTTP callback logger.&lt;/p&gt;
&lt;p&gt;We retained useful error-level logging around OAuth failures:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.error('[OAUTH-CALLBACK-ERROR]', {
  provider,
  message: String(error?.message || error)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This provides enough information for future troubleshooting without logging authorization codes, OAuth state values, access tokens, or secrets.&lt;/p&gt;
&lt;p&gt;That distinction matters.&lt;/p&gt;
&lt;p&gt;Authentication debugging must provide observability without accidentally creating a credential-leak mechanism.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;8-stabilizing-urban-lab-i-eco-01&quot;&gt;8. Stabilizing Urban Lab / I-ECO-01&lt;/h2&gt;
&lt;p&gt;At the same time, another service on the VPS had a completely different problem.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;urbanlab&lt;/code&gt; PM2 process had accumulated an extremely large restart count.&lt;/p&gt;
&lt;p&gt;Inspection of the error log showed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Error: Cannot find module 'helmet'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server required:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const mongoose = require('mongoose');
const { createServer } = require('http');
const { Server } = require('socket.io');
const winston = require('winston');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But &lt;code&gt;package.json&lt;/code&gt; originally contained only:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;cors
dotenv
express
mongoose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The runtime therefore expected modules that the project did not declare/install.&lt;/p&gt;
&lt;p&gt;We added the missing dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;helmet
socket.io
winston
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After installation, each required module could be resolved successfully.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;9-stopping-a-multi-million-restart-pm2-loop&quot;&gt;9. Stopping a Multi-Million-Restart PM2 Loop&lt;/h2&gt;
&lt;p&gt;Once the missing dependencies were installed, Urban Lab started normally.&lt;/p&gt;
&lt;p&gt;The server reported:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Server started on port 5002
MongoDB connected
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Its health endpoint returned:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;status&quot;: &quot;ok&quot;,
  &quot;service&quot;: &quot;I-ECO-01&quot;,
  &quot;mongodb&quot;: &quot;connected&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We then compared the PM2 process before and after a waiting period.&lt;/p&gt;
&lt;p&gt;The PID remained unchanged.&lt;/p&gt;
&lt;p&gt;The restart counter remained unchanged.&lt;/p&gt;
&lt;p&gt;That confirmed that the restart loop had actually stopped.&lt;/p&gt;
&lt;p&gt;This is much more meaningful than simply seeing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;status: online
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;for a few seconds.&lt;/p&gt;
&lt;p&gt;A process can repeatedly appear online while constantly crashing and restarting.&lt;/p&gt;
&lt;p&gt;Process stability needs to be measured over time.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;10-current-vps-architecture&quot;&gt;10. Current VPS Architecture&lt;/h2&gt;
&lt;p&gt;The resulting environment now looks approximately like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                    Internet
                       |
                       v
                Cloudflare Tunnel
                       |
                       v
            sepolia.myzubster.com
                       |
          +------------+------------+
          |                         |
          v                         v
     React Frontend            Express Backend
                                   |
                    +--------------+--------------+
                    |              |              |
                    v              v              v
                 MongoDB       Google OAuth   Sepolia EVM
                    |
                    v
              Application Data

Additional VPS services:

PM2
 |
 +-- MyZubster Sepolia Backend
 +-- MyZubster Sepolia Frontend
 +-- MyZubster Social
 +-- MyZubster Web
 +-- Urban Lab / I-ECO-01
 +-- Cloudflared
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PM2 is configured under systemd, and the active process list has been saved using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pm2 save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means the current process configuration can be restored after a reboot through the existing PM2 startup integration.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;11-git-hygiene&quot;&gt;11. Git Hygiene&lt;/h2&gt;
&lt;p&gt;Another important part of the work was separating debugging artifacts from actual source changes.&lt;/p&gt;
&lt;p&gt;During debugging we created several temporary backups.&lt;/p&gt;
&lt;p&gt;Before committing, those files were removed from the working tree.&lt;/p&gt;
&lt;p&gt;We also explicitly verified that &lt;code&gt;.env&lt;/code&gt; remained ignored.&lt;/p&gt;
&lt;p&gt;The final source change was isolated and committed as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;fix(auth): improve OAuth callback error logging
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting commit was:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;02f06f22
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The working tree was clean after the commit.&lt;/p&gt;
&lt;p&gt;The branch:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;vps/sepolia-e2e-test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;was then pushed to the remote repository.&lt;/p&gt;
&lt;p&gt;This gives us a reproducible Git checkpoint corresponding to the working VPS state without committing the environment secrets.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;12-security-considerations&quot;&gt;12. Security Considerations&lt;/h2&gt;
&lt;p&gt;One of the most important lessons from this work concerns secrets.&lt;/p&gt;
&lt;p&gt;OAuth client secrets, API keys, database credentials, private keys, GitHub tokens, JWT signing secrets, and similar credentials should never be committed to Git.&lt;/p&gt;
&lt;p&gt;The repository correctly ignores &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But &lt;code&gt;.gitignore&lt;/code&gt; is only one layer of protection.&lt;/p&gt;
&lt;p&gt;The longer-term architecture should move production credentials toward dedicated secret management and controlled deployment-time injection.&lt;/p&gt;
&lt;p&gt;We also deliberately avoided logging:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Google client secret
OAuth authorization code
OAuth state
access tokens
JWTs
database passwords
private keys
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Observability and secret exposure must remain separate concerns.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;13-what-is-working-now&quot;&gt;13. What Is Working Now&lt;/h2&gt;
&lt;p&gt;At this stage we have achieved several concrete milestones.&lt;/p&gt;
&lt;p&gt;Google OAuth can complete the server-side authorization-code flow.&lt;/p&gt;
&lt;p&gt;The Sepolia backend exposes Google as an enabled authentication provider.&lt;/p&gt;
&lt;p&gt;The callback is routed through the public Sepolia infrastructure.&lt;/p&gt;
&lt;p&gt;The application runtime is using the intended Sepolia frontend/public URLs.&lt;/p&gt;
&lt;p&gt;PM2 is running the MyZubster services.&lt;/p&gt;
&lt;p&gt;PM2 configuration has been persisted.&lt;/p&gt;
&lt;p&gt;Urban Lab / I-ECO-01 is no longer trapped in its missing-module restart loop.&lt;/p&gt;
&lt;p&gt;Its MongoDB connection is operational.&lt;/p&gt;
&lt;p&gt;The I-ECO-01 health endpoint responds successfully.&lt;/p&gt;
&lt;p&gt;The OAuth debugging changes have been cleaned up.&lt;/p&gt;
&lt;p&gt;The relevant source change has been committed and pushed to a dedicated branch.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;14-what-still-needs-to-be-done&quot;&gt;14. What Still Needs to Be Done&lt;/h2&gt;
&lt;p&gt;A working OAuth callback does not mean the entire platform is production-ready.&lt;/p&gt;
&lt;p&gt;Several areas still need engineering work.&lt;/p&gt;
&lt;h3 id=&quot;authentication-hardening&quot;&gt;Authentication hardening&lt;/h3&gt;
&lt;p&gt;The OAuth implementation should receive additional automated tests covering:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;invalid state
expired state
missing code
provider errors
invalid client configuration
token endpoint failures
userinfo failures
unverified email
expired social-login tickets
replay attempts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GitHub and Facebook providers are currently disabled according to the provider endpoint and can be implemented or enabled separately when their configuration is ready.&lt;/p&gt;
&lt;h3 id=&quot;deployment-reproducibility&quot;&gt;Deployment reproducibility&lt;/h3&gt;
&lt;p&gt;The VPS currently represents an important working environment.&lt;/p&gt;
&lt;p&gt;The next goal should be making that environment reproducible from source and configuration rather than depending on manual server history.&lt;/p&gt;
&lt;p&gt;That means documenting and eventually automating:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;dependency installation
environment injection
PM2 configuration
Cloudflare configuration
frontend startup
backend startup
health checks
rollback
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;monitoring&quot;&gt;Monitoring&lt;/h3&gt;
&lt;p&gt;A process being online is not sufficient operational monitoring.&lt;/p&gt;
&lt;p&gt;We want visibility into:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;restart rate
HTTP error rate
OAuth failure rate
MongoDB availability
response latency
memory consumption
CPU consumption
Sepolia RPC failures
WebSocket availability
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;secret-management&quot;&gt;Secret management&lt;/h3&gt;
&lt;p&gt;Production credentials should eventually be rotated and managed through a dedicated secret-management workflow.&lt;/p&gt;
&lt;h3 id=&quot;ci-cd&quot;&gt;CI/CD&lt;/h3&gt;
&lt;p&gt;The current branch and commit structure gives us a good foundation for moving toward automated deployment.&lt;/p&gt;
&lt;p&gt;The desired flow is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Developer
   |
   v
Git branch
   |
   v
Pull Request
   |
   v
Automated Tests
   |
   v
Security Checks
   |
   v
Merge
   |
   v
Sepolia Deployment
   |
   v
Health Checks
   |
   v
Production Promotion
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;15-where-we-want-to-go&quot;&gt;15. Where We Want to Go&lt;/h2&gt;
&lt;p&gt;The objective is larger than simply making Google login work.&lt;/p&gt;
&lt;p&gt;The Sepolia environment is becoming the integration layer where we can validate the interaction between identity, application state, Web3 payments, AI services, Urban Lab infrastructure, and MyZubster's broader ecosystem.&lt;/p&gt;
&lt;p&gt;The architecture we are moving toward is approximately:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                         MyZubster
                             |
        +--------------------+--------------------+
        |                    |                    |
        v                    v                    v
     Identity             Web3 Layer          AI Layer
        |                    |                    |
   Google OAuth          Ethereum           AI Services
   GitHub OAuth          Sepolia            Zorgax / Pytho
   Social Identity       Wallets            Automation
        |                    |                    |
        +--------------------+--------------------+
                             |
                             v
                        Core Backend
                             |
             +---------------+---------------+
             |                               |
             v                               v
          MongoDB                       Urban Lab
                                            |
                                  +---------+---------+
                                  |                   |
                                  v                   v
                              IoT / GPS        Environmental Data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Sepolia environment gives us a place to integrate these components without treating production as the testing environment.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;16-engineering-lessons&quot;&gt;16. Engineering Lessons&lt;/h2&gt;
&lt;p&gt;Several lessons from this debugging cycle are worth keeping.&lt;/p&gt;
&lt;h4 id=&quot;inspect-the-real-provider-response&quot;&gt;Inspect the real provider response&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Login failed&lt;/code&gt; tells you almost nothing.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;401 invalid_client
The provided client secret is invalid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;tells you exactly where to investigate.&lt;/p&gt;
&lt;h4 id=&quot;-env-is-not-necessarily-the-runtime-environment&quot;&gt;&lt;code&gt;.env&lt;/code&gt; is not necessarily the runtime environment&lt;/h4&gt;
&lt;p&gt;Always inspect what the actual process sees.&lt;/p&gt;
&lt;p&gt;This becomes especially important with PM2, containers, systemd, CI/CD, and cloud deployment systems.&lt;/p&gt;
&lt;h4 id=&quot;online-does-not-necessarily-mean-healthy&quot;&gt;&lt;code&gt;online&lt;/code&gt; does not necessarily mean healthy&lt;/h4&gt;
&lt;p&gt;Urban Lab demonstrated this clearly.&lt;/p&gt;
&lt;p&gt;A service may be repeatedly restarted while PM2 continues reporting it as online between crashes.&lt;/p&gt;
&lt;p&gt;PID stability, restart counters, health checks, and logs provide a much better picture.&lt;/p&gt;
&lt;h4 id=&quot;debug-instrumentation-should-be-temporary&quot;&gt;Debug instrumentation should be temporary&lt;/h4&gt;
&lt;p&gt;Add enough instrumentation to understand the failure.&lt;/p&gt;
&lt;p&gt;Verify the fix.&lt;/p&gt;
&lt;p&gt;Then remove unnecessary diagnostic logging while retaining safe operational error reporting.&lt;/p&gt;
&lt;h4 id=&quot;infrastructure-fixes-should-end-in-git&quot;&gt;Infrastructure fixes should end in Git&lt;/h4&gt;
&lt;p&gt;A VPS fix that exists only in shell history is technical debt.&lt;/p&gt;
&lt;p&gt;Once the behavior is verified, the corresponding source changes should be cleaned, reviewed, committed, and pushed.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;What began as a Google login failure exposed several deeper infrastructure issues.&lt;/p&gt;
&lt;p&gt;We traced the OAuth flow to the actual Google token exchange, identified an invalid client secret, corrected the runtime configuration, fixed the frontend redirect destination, validated the public callback path, cleaned the debugging instrumentation, stabilized a separate Urban Lab service that had entered a massive PM2 restart loop, verified MongoDB connectivity, persisted the PM2 process state, cleaned the Git working tree, committed the relevant OAuth changes, and pushed the working branch.&lt;/p&gt;
&lt;p&gt;The important outcome is not simply:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Google login works.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The more important outcome is that the Sepolia environment is becoming observable, reproducible, testable, and suitable for real end-to-end integration work.&lt;/p&gt;
&lt;p&gt;The next phase is to convert these manually validated infrastructure improvements into automated tests, deployment pipelines, monitoring, stronger secret management, and repeatable environment provisioning.&lt;/p&gt;
&lt;p&gt;That is the path from a working development VPS to a maintainable engineering platform.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28744/from-broken-google-oauth-to-stable-sepolia-e2e-environment-engineering-myzubster-on-vps</guid>
<pubDate>Sun, 27 Sep 2026 00:54:50 +0000</pubDate>
</item>
<item>
<title>Debugging 6.5 Million PM2 Restarts: Stabilizing MyZubster Sepolia and I-ECO-01</title>
<link>https://coderlegion.com/28722/debugging-6-5-million-pm2-restarts-stabilizing-myzubster-sepolia-and-i-eco-01</link>
<description>&lt;h2 id=&quot;debugging-6-5-million-pm2-restarts-stabilizing-myzubster-sepolia-and-i-eco-01&quot;&gt;Debugging 6.5 Million PM2 Restarts: Stabilizing MyZubster Sepolia and I-ECO-01&lt;/h2&gt;
&lt;p&gt;Today I completed a significant infrastructure debugging session across the &lt;strong&gt;MyZubster Sepolia E2E environment&lt;/strong&gt; and the &lt;strong&gt;I-ECO-01 / UrbanLab backend&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;What initially looked like a few unrelated configuration problems turned into a useful example of why production debugging needs to happen layer by layer.&lt;/p&gt;
&lt;p&gt;The work involved &lt;strong&gt;Node.js, PM2, MongoDB, Google OAuth 2.0, Cloudflare, Socket.IO, and a Linux VPS&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here is what happened, what was broken, and how it was fixed.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-google-oauth-was-reaching-the-backend-but-authentication-still-failed&quot;&gt;1. Google OAuth Was Reaching the Backend — But Authentication Still Failed&lt;/h3&gt;
&lt;p&gt;The first problem appeared in the Google social-login flow.&lt;/p&gt;
&lt;p&gt;The authorization request itself looked correct.&lt;/p&gt;
&lt;p&gt;Google was receiving the expected client ID and redirect URI:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;https://sepolia.myzubster.com/api/auth/social/google/callback&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The callback also reached the backend correctly.&lt;/p&gt;
&lt;p&gt;Debug instrumentation confirmed:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hasCode: true&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hasState: true&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hasError: false&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;So the browser redirect, public route, OAuth state and callback routing were all working.&lt;/p&gt;
&lt;p&gt;The failure happened one step later: during the authorization-code exchange.&lt;/p&gt;
&lt;p&gt;The backend logged:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;401 invalid_client&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;and Google reported that the provided client secret was invalid.&lt;/p&gt;
&lt;p&gt;That was an important distinction.&lt;/p&gt;
&lt;p&gt;The problem was not the redirect URI.&lt;/p&gt;
&lt;p&gt;It was not Cloudflare.&lt;/p&gt;
&lt;p&gt;It was not Express routing.&lt;/p&gt;
&lt;p&gt;It was not the OAuth state.&lt;/p&gt;
&lt;p&gt;It was not the frontend.&lt;/p&gt;
&lt;p&gt;The OAuth client credential itself was wrong.&lt;/p&gt;
&lt;p&gt;After correcting the configuration, the backend reached:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OAUTH-CALLBACK-ENTER&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;followed by:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OAUTH-CALLBACK-SUCCESS&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The server-side Google OAuth flow was working.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-the-env-file-was-correct-the-running-process-was-not&quot;&gt;2. The &lt;code&gt;.env&lt;/code&gt; File Was Correct — The Running Process Was Not&lt;/h3&gt;
&lt;p&gt;The next issue was more subtle.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file contained:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FRONTEND_URL=https://sepolia.myzubster.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GATEWAY_PUBLIC_URL=https://sepolia.myzubster.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Yet after successful OAuth authentication, the application could still redirect toward an old Vercel deployment.&lt;/p&gt;
&lt;p&gt;This highlighted an important operational detail:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Changing &lt;code&gt;.env&lt;/code&gt; does not automatically mean a long-running process is using the new values.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I inspected the configuration at several levels:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Node + dotenv&lt;/li&gt;
&lt;li&gt;PM2 process configuration&lt;/li&gt;
&lt;li&gt;actual process environment&lt;/li&gt;
&lt;li&gt;OAuth redirect generation&lt;/li&gt;
&lt;li&gt;backend logs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The active PM2 environment was then aligned with the Sepolia deployment:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FRONTEND_URL=https://sepolia.myzubster.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PUBLIC_APP_URL=https://sepolia.myzubster.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GATEWAY_PUBLIC_URL=https://sepolia.myzubster.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;After that, the active process environment was verified directly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-then-i-found-a-process-with-more-than-6-5-million-restarts&quot;&gt;3. Then I Found a Process With More Than 6.5 Million Restarts&lt;/h3&gt;
&lt;p&gt;While checking the rest of the VPS, another issue appeared.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;UrbanLab / I-ECO-01&lt;/strong&gt; PM2 process was shown as online, but its restart counter was enormous:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6,548,384 restarts&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That immediately suggested a crash/restart loop.&lt;/p&gt;
&lt;p&gt;This is one of the reasons I do not treat:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PM2 status = online&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;as proof that an application is healthy.&lt;/p&gt;
&lt;p&gt;A process can start, crash immediately, restart automatically and still repeatedly appear online when inspected at the wrong moment.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-finding-the-root-cause&quot;&gt;4. Finding the Root Cause&lt;/h3&gt;
&lt;p&gt;PM2 was executing:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/opt/I-ECO-01/server.js&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I inspected the server entrypoint and found these dependencies:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dotenv&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;express&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cors&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;helmet&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mongoose&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;socket.io&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;winston&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then I compared those requirements against &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The project only declared:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cors&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dotenv&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;express&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mongoose&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The logs confirmed the immediate failure:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Error: Cannot find module 'helmet'&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The server crashed at:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;const helmet = require('helmet');&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Because PM2 automatically restarted the process, the cycle repeated continuously.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-fixing-the-dependency-graph&quot;&gt;5. Fixing the Dependency Graph&lt;/h3&gt;
&lt;p&gt;Instead of installing only the first missing package and waiting for the next crash, I inspected all external imports in the server entrypoint.&lt;/p&gt;
&lt;p&gt;Three npm dependencies were missing:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;helmet&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;socket.io&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;winston&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http&lt;/code&gt; did not require installation because it is part of Node.js itself.&lt;/p&gt;
&lt;p&gt;The missing dependencies were installed and persisted to &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The resulting application dependencies now include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Express&lt;/li&gt;
&lt;li&gt;CORS&lt;/li&gt;
&lt;li&gt;dotenv&lt;/li&gt;
&lt;li&gt;Helmet&lt;/li&gt;
&lt;li&gt;Mongoose&lt;/li&gt;
&lt;li&gt;Socket.IO&lt;/li&gt;
&lt;li&gt;Winston&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each new module was then tested independently with &lt;code&gt;require()&lt;/code&gt; before restarting the service.&lt;/p&gt;
&lt;p&gt;All modules loaded successfully.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-application-startup-was-not-enough-i-tested-stability&quot;&gt;6. Application Startup Was Not Enough — I Tested Stability&lt;/h3&gt;
&lt;p&gt;After restarting UrbanLab, the application successfully initialized its Escrow controller.&lt;/p&gt;
&lt;p&gt;The server reported:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Server started on port 5002&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;MongoDB connected successfully.&lt;/p&gt;
&lt;p&gt;The health endpoint:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http://127.0.0.1:5002/health&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;returned:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;status: ok&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;service: I-ECO-01&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mongodb: connected&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;But I still did not consider the problem solved.&lt;/p&gt;
&lt;p&gt;The original issue was a restart loop, so the correct test was to verify process stability over time.&lt;/p&gt;
&lt;p&gt;Before the stability test:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PID = 1026452&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;restarts = 6548384&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;After 15 seconds:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PID = 1026452&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;restarts = 6548384&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Same PID.&lt;/p&gt;
&lt;p&gt;Same restart counter.&lt;/p&gt;
&lt;p&gt;The loop had stopped.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-escrow-infrastructure-is-running-again&quot;&gt;7. Escrow Infrastructure Is Running Again&lt;/h3&gt;
&lt;p&gt;Once the dependency problem was resolved, the I-ECO-01 Escrow controller initialized with:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Threshold:&lt;/strong&gt; 2/3&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Timeout:&lt;/strong&gt; 24 hours&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fee:&lt;/strong&gt; 0.5%&lt;/p&gt;
&lt;p&gt;The service also initialized its HTTP API and WebSocket infrastructure.&lt;/p&gt;
&lt;p&gt;MongoDB remained connected and the health endpoint continued responding successfully.&lt;/p&gt;
&lt;p&gt;So the service moved from an uncontrolled crash loop to an operational backend.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-old-logs-can-be-misleading&quot;&gt;8. Old Logs Can Be Misleading&lt;/h3&gt;
&lt;p&gt;One interesting detail appeared after the repair.&lt;/p&gt;
&lt;p&gt;Running:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pm2 logs urbanlab&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;still displayed:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Cannot find module 'helmet'&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;At first glance, that could suggest that the problem still existed.&lt;/p&gt;
&lt;p&gt;But those entries were historical.&lt;/p&gt;
&lt;p&gt;The current process had already successfully loaded Helmet, initialized the application, connected to MongoDB and remained alive with the same PID.&lt;/p&gt;
&lt;p&gt;This is why timestamps, process IDs, restart counters and health checks matter when reading operational logs.&lt;/p&gt;
&lt;p&gt;A log file can contain a real error that is no longer a current error.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;9-what-i-verified&quot;&gt;9. What I Verified&lt;/h3&gt;
&lt;p&gt;By the end of the session:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Google OAuth provider:&lt;/strong&gt; operational&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Google OAuth callback:&lt;/strong&gt; operational&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OAuth authorization-code exchange:&lt;/strong&gt; successful&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OAuth state handling:&lt;/strong&gt; verified&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sepolia public URL configuration:&lt;/strong&gt; corrected&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PM2 runtime environment:&lt;/strong&gt; verified&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I-ECO-01 Node.js dependencies:&lt;/strong&gt; repaired&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UrbanLab restart loop:&lt;/strong&gt; stopped&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MongoDB:&lt;/strong&gt; connected&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Health endpoint:&lt;/strong&gt; healthy&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Escrow controller:&lt;/strong&gt; initialized&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2-of-3 multisig configuration:&lt;/strong&gt; loaded&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Socket.IO:&lt;/strong&gt; initialized&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PM2/systemd environment:&lt;/strong&gt; verified&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;10-the-main-lessons&quot;&gt;10. The Main Lessons&lt;/h3&gt;
&lt;p&gt;This debugging session reinforced several useful principles.&lt;/p&gt;
&lt;h4 id=&quot;online-does-not-mean-healthy&quot;&gt;&lt;code&gt;online&lt;/code&gt; does not mean healthy&lt;/h4&gt;
&lt;p&gt;A process manager tells you about process state.&lt;/p&gt;
&lt;p&gt;An application health endpoint tells you about application state.&lt;/p&gt;
&lt;p&gt;You often need both.&lt;/p&gt;
&lt;h4 id=&quot;-env-does-not-equal-runtime-environment&quot;&gt;&lt;code&gt;.env&lt;/code&gt; does not equal runtime environment&lt;/h4&gt;
&lt;p&gt;Always verify what the running process actually sees.&lt;/p&gt;
&lt;p&gt;This is especially important when using PM2, containers, systemd or other long-running process managers.&lt;/p&gt;
&lt;h4 id=&quot;debug-oauth-one-stage-at-a-time&quot;&gt;Debug OAuth one stage at a time&lt;/h4&gt;
&lt;p&gt;OAuth involves multiple independent stages:&lt;/p&gt;
&lt;p&gt;authorization request → provider consent → callback → state verification → authorization-code exchange → profile retrieval → application session → frontend redirect.&lt;/p&gt;
&lt;p&gt;Logging each boundary makes failures much easier to isolate.&lt;/p&gt;
&lt;h4 id=&quot;fix-dependency-problems-systematically&quot;&gt;Fix dependency problems systematically&lt;/h4&gt;
&lt;p&gt;When one &lt;code&gt;require()&lt;/code&gt; fails, inspect the entire entrypoint instead of repeatedly installing dependencies one crash at a time.&lt;/p&gt;
&lt;h4 id=&quot;verify-the-original-failure-condition&quot;&gt;Verify the original failure condition&lt;/h4&gt;
&lt;p&gt;The problem was a restart loop.&lt;/p&gt;
&lt;p&gt;Therefore, successful startup alone was not enough.&lt;/p&gt;
&lt;p&gt;The actual success condition was:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;same PID + unchanged restart counter + healthy endpoint over time.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-result&quot;&gt;Final Result&lt;/h3&gt;
&lt;p&gt;The MyZubster Sepolia environment is now significantly more stable.&lt;/p&gt;
&lt;p&gt;Google OAuth can successfully complete its backend authentication flow.&lt;/p&gt;
&lt;p&gt;The Sepolia runtime URLs are correctly propagated to the active backend process.&lt;/p&gt;
&lt;p&gt;I-ECO-01 is connected to MongoDB.&lt;/p&gt;
&lt;p&gt;The Escrow service is initialized.&lt;/p&gt;
&lt;p&gt;And, most importantly, a Node.js service that had accumulated more than &lt;strong&gt;6.5 million PM2 restarts&lt;/strong&gt; is now staying alive.&lt;/p&gt;
&lt;p&gt;Sometimes the most useful development work is not adding another feature.&lt;/p&gt;
&lt;p&gt;It is understanding exactly why the existing system is failing — and proving that the fix actually removed the failure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Node.js · Express · MongoDB · PM2 · Google OAuth 2.0 · Socket.IO · Cloudflare · Linux · Sepolia&lt;/p&gt;
&lt;h2 id=&quot;nodejs-devops-webdevelopment-backenddevelopment-debugging-pm2-mongodb-oauth2-googleoauth-socketio-web3-sepolia-cloudflare-linux-myzubster-coderlegion&quot;&gt;NodeJS #DevOps #WebDevelopment #BackendDevelopment #Debugging #PM2 #MongoDB #OAuth2 #GoogleOAuth #SocketIO #Web3 #Sepolia #Cloudflare #Linux #MyZubster #CoderLegion&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28722/debugging-6-5-million-pm2-restarts-stabilizing-myzubster-sepolia-and-i-eco-01</guid>
<pubDate>Sat, 26 Sep 2026 14:17:31 +0000</pubDate>
</item>
<item>
<title>Building MetaMask Wallet Ownership Verification in MyZubster</title>
<link>https://coderlegion.com/28567/building-metamask-wallet-ownership-verification-in-myzubster</link>
<description>&lt;p&gt;Building MetaMask Wallet Ownership Verification in MyZubster&lt;/p&gt;
&lt;p&gt;I’ve started implementing MetaMask / EVM wallet ownership verification inside MyZubster.&lt;/p&gt;
&lt;p&gt;At first, this might sound like a simple “Connect Wallet” feature.&lt;/p&gt;
&lt;p&gt;But I wanted to avoid one of the most common mistakes in Web3 UX:&lt;/p&gt;
&lt;p&gt;treating “wallet connected” as if it automatically meant “identity verified”, “payment authorized”, or “transaction completed”.&lt;/p&gt;
&lt;p&gt;For MyZubster, those states need to stay separate.&lt;/p&gt;
&lt;p&gt;The architecture I’m building follows this rule:&lt;/p&gt;
&lt;p&gt;CONNECTED&lt;br&gt;
!=&lt;br&gt;
VERIFIED&lt;br&gt;
!=&lt;br&gt;
SIGNED&lt;br&gt;
!=&lt;br&gt;
PAID&lt;br&gt;
!=&lt;br&gt;
SETTLED&lt;/p&gt;
&lt;p&gt;That distinction is now becoming part of the product.&lt;/p&gt;
&lt;p&gt;What we already had&lt;/p&gt;
&lt;p&gt;MyZubster already had several EVM-related pieces in the codebase:&lt;/p&gt;
&lt;p&gt;ethers&lt;br&gt;
Ethereum Sepolia verification&lt;br&gt;
Base Sepolia anchoring&lt;br&gt;
Marketplace ETH support&lt;br&gt;
NFT experiments&lt;br&gt;
blockchain evidence flows&lt;/p&gt;
&lt;p&gt;What was missing was the user-side ownership layer.&lt;/p&gt;
&lt;p&gt;In other words:&lt;/p&gt;
&lt;p&gt;How can a logged-in MyZubster user prove that they actually control a specific Ethereum wallet?&lt;/p&gt;
&lt;p&gt;Without giving MyZubster the private key.&lt;/p&gt;
&lt;p&gt;Without sharing a seed phrase.&lt;/p&gt;
&lt;p&gt;Without sending ETH.&lt;/p&gt;
&lt;p&gt;Without paying gas.&lt;/p&gt;
&lt;p&gt;That became the first milestone.&lt;/p&gt;
&lt;p&gt;The new flow&lt;/p&gt;
&lt;p&gt;The flow now looks like this:&lt;/p&gt;
&lt;p&gt;Authenticated MyZubster user&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Connect MetaMask&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;eth_requestAccounts&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read wallet address&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read chain ID&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Request challenge from MyZubster&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Server creates short-lived nonce&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;User signs message in MetaMask&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;personal_sign&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Signature sent to backend&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Backend recovers signer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Address matches?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WALLET_VERIFIED&lt;/p&gt;
&lt;p&gt;The important part is that verification happens server-side.&lt;/p&gt;
&lt;p&gt;The frontend cannot simply tell MyZubster:&lt;/p&gt;
&lt;p&gt;{&lt;br&gt;
  &quot;walletVerified&quot;: true&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;and be trusted.&lt;/p&gt;
&lt;p&gt;New API&lt;/p&gt;
&lt;p&gt;I added four authenticated endpoints:&lt;/p&gt;
&lt;p&gt;GET    /api/wallet/me&lt;br&gt;
POST   /api/wallet/challenge&lt;br&gt;
POST   /api/wallet/verify&lt;br&gt;
DELETE /api/wallet/disconnect&lt;/p&gt;
&lt;p&gt;Their responsibility is intentionally narrow.&lt;/p&gt;
&lt;p&gt;/challenge creates the ownership proof request.&lt;/p&gt;
&lt;p&gt;/verify validates the signature.&lt;/p&gt;
&lt;p&gt;/me returns the current wallet relationship.&lt;/p&gt;
&lt;p&gt;/disconnect removes the verified association from the MyZubster account.&lt;/p&gt;
&lt;p&gt;The challenge&lt;/p&gt;
&lt;p&gt;The challenge includes context such as:&lt;/p&gt;
&lt;p&gt;MyZubster account&lt;br&gt;
wallet address&lt;br&gt;
chain ID&lt;br&gt;
domain&lt;br&gt;
application URI&lt;br&gt;
random nonce&lt;br&gt;
issued time&lt;br&gt;
expiration time&lt;br&gt;
intended action&lt;/p&gt;
&lt;p&gt;The intended action is currently:&lt;/p&gt;
&lt;p&gt;LINK_WALLET&lt;/p&gt;
&lt;p&gt;A simplified message looks like:&lt;/p&gt;
&lt;p&gt;www.myzubster.com wants you to link your Ethereum account to MyZubster:&lt;/p&gt;
&lt;p&gt;0x...&lt;/p&gt;
&lt;p&gt;Sign this message to prove wallet control.&lt;br&gt;
This is not a payment and does not spend ETH.&lt;/p&gt;
&lt;p&gt;URI: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com&quot;&gt;https://www.myzubster.com&lt;/a&gt;&lt;br&gt;
Version: 1&lt;br&gt;
Chain ID: 1&lt;br&gt;
Nonce: ...&lt;br&gt;
Issued At: ...&lt;br&gt;
Expiration Time: ...&lt;br&gt;
Request ID: LINK_WALLET:user-id&lt;/p&gt;
&lt;p&gt;That “not a payment” line is deliberate.&lt;/p&gt;
&lt;p&gt;I don’t want a wallet signature to ever be confused with a transaction.&lt;/p&gt;
&lt;p&gt;Why use a nonce?&lt;/p&gt;
&lt;p&gt;Because the signature must not be reusable forever.&lt;/p&gt;
&lt;p&gt;Each challenge is:&lt;/p&gt;
&lt;p&gt;random&lt;br&gt;
short-lived&lt;br&gt;
account-bound&lt;br&gt;
wallet-bound&lt;br&gt;
action-bound&lt;/p&gt;
&lt;p&gt;The backend checks:&lt;/p&gt;
&lt;p&gt;challenge exists&lt;br&gt;
↓&lt;br&gt;
challenge is not expired&lt;br&gt;
↓&lt;br&gt;
wallet matches&lt;br&gt;
↓&lt;br&gt;
message matches&lt;br&gt;
↓&lt;br&gt;
signature is valid&lt;br&gt;
↓&lt;br&gt;
recovered signer matches wallet&lt;/p&gt;
&lt;p&gt;Only then does the wallet become verified.&lt;/p&gt;
&lt;p&gt;Signature verification&lt;/p&gt;
&lt;p&gt;The backend uses ethers to recover the signer:&lt;/p&gt;
&lt;p&gt;const recovered = verifyMessage(message, signature);&lt;/p&gt;
&lt;p&gt;Then the recovered address is normalized and compared with the expected wallet.&lt;/p&gt;
&lt;p&gt;If another wallet signs the challenge, verification fails.&lt;/p&gt;
&lt;p&gt;What gets stored&lt;/p&gt;
&lt;p&gt;MyZubster never stores:&lt;/p&gt;
&lt;p&gt;private key&lt;br&gt;
seed phrase&lt;br&gt;
mnemonic&lt;br&gt;
MetaMask password&lt;br&gt;
wallet secret&lt;/p&gt;
&lt;p&gt;Only wallet relationship metadata is persisted.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;p&gt;evmWallet: {&lt;br&gt;
  walletType: 'EVM',&lt;br&gt;
  provider: 'metamask',&lt;br&gt;
  address: '0x...',&lt;br&gt;
  chainId: 1,&lt;br&gt;
  status: 'WALLET_VERIFIED',&lt;br&gt;
  verifiedAt: Date,&lt;br&gt;
  lastVerifiedAt: Date&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;Temporary challenge information is also stored server-side until verification or expiration.&lt;/p&gt;
&lt;p&gt;Wallet state&lt;/p&gt;
&lt;p&gt;I’m avoiding a single boolean like:&lt;/p&gt;
&lt;p&gt;walletConnected = true&lt;/p&gt;
&lt;p&gt;Instead, MyZubster has explicit states:&lt;/p&gt;
&lt;p&gt;WALLET_NOT_CONNECTED&lt;br&gt;
WALLET_CHALLENGE_PENDING&lt;br&gt;
WALLET_VERIFIED&lt;br&gt;
WALLET_DISCONNECTED&lt;/p&gt;
&lt;p&gt;That becomes useful both for the UI and for Zorgax.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;MetaMask connected&lt;/p&gt;
&lt;p&gt;is not the same as:&lt;/p&gt;
&lt;p&gt;wallet ownership verified&lt;/p&gt;
&lt;p&gt;and neither means:&lt;/p&gt;
&lt;p&gt;payment completed&lt;br&gt;
Frontend&lt;/p&gt;
&lt;p&gt;I extended the existing WalletHubPanel rather than creating a completely separate wallet system.&lt;/p&gt;
&lt;p&gt;The user now gets a:&lt;/p&gt;
&lt;p&gt;Connect MetaMask&lt;/p&gt;
&lt;p&gt;action.&lt;/p&gt;
&lt;p&gt;The frontend uses:&lt;/p&gt;
&lt;p&gt;eth_requestAccounts&lt;/p&gt;
&lt;p&gt;then:&lt;/p&gt;
&lt;p&gt;eth_chainId&lt;/p&gt;
&lt;p&gt;and finally:&lt;/p&gt;
&lt;p&gt;personal_sign&lt;/p&gt;
&lt;p&gt;The signature is then sent back to the backend for verification.&lt;/p&gt;
&lt;p&gt;The UI explicitly explains that the signature:&lt;/p&gt;
&lt;p&gt;does not transfer ETH&lt;br&gt;
does not cost gas&lt;br&gt;
does not authorize a payment&lt;br&gt;
Preventing duplicate wallet ownership claims&lt;/p&gt;
&lt;p&gt;Another issue is what happens if two MyZubster accounts try to link the same verified wallet.&lt;/p&gt;
&lt;p&gt;The current behavior is:&lt;/p&gt;
&lt;p&gt;Account A&lt;br&gt;
→ verifies 0xABC&lt;/p&gt;
&lt;p&gt;Account B&lt;br&gt;
→ tries to verify 0xABC&lt;/p&gt;
&lt;p&gt;Result:&lt;br&gt;
WALLET_ALREADY_LINKED&lt;/p&gt;
&lt;p&gt;This does not mean that MyZubster treats a wallet as a legal identity.&lt;/p&gt;
&lt;p&gt;It simply prevents contradictory wallet-to-account relationships inside the application.&lt;/p&gt;
&lt;p&gt;What changes in MyZubster&lt;/p&gt;
&lt;p&gt;This creates a new trust layer.&lt;/p&gt;
&lt;p&gt;MyZubster can already work with:&lt;/p&gt;
&lt;p&gt;account identity&lt;br&gt;
GitHub identity&lt;br&gt;
community profile&lt;br&gt;
Marketplace identity&lt;br&gt;
Zorgax context&lt;/p&gt;
&lt;p&gt;Now it can also say:&lt;/p&gt;
&lt;p&gt;this account proved control&lt;br&gt;
of this EVM wallet&lt;br&gt;
at this time&lt;/p&gt;
&lt;p&gt;That is a reusable building block.&lt;/p&gt;
&lt;p&gt;The next milestone: signed Marketplace requests&lt;/p&gt;
&lt;p&gt;This is where the wallet integration becomes much more useful.&lt;/p&gt;
&lt;p&gt;The next step is to let a verified wallet sign a canonical Marketplace request.&lt;/p&gt;
&lt;p&gt;Something like:&lt;/p&gt;
&lt;p&gt;{&lt;br&gt;
  &quot;schema&quot;: &quot;myzubster.marketplace-request.v1&quot;,&lt;br&gt;
  &quot;intent&quot;: &quot;MARKETPLACE_REQUEST&quot;,&lt;br&gt;
  &quot;listingId&quot;: &quot;listing-123&quot;,&lt;br&gt;
  &quot;quantity&quot;: 1,&lt;br&gt;
  &quot;buyerAccountReference&quot;: &quot;user-42&quot;,&lt;br&gt;
  &quot;walletAddress&quot;: &quot;0x...&quot;,&lt;br&gt;
  &quot;nonce&quot;: &quot;...&quot;,&lt;br&gt;
  &quot;issuedAt&quot;: &quot;...&quot;,&lt;br&gt;
  &quot;expiresAt&quot;: &quot;...&quot;&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;The buyer signs it off-chain.&lt;/p&gt;
&lt;p&gt;The server verifies it.&lt;/p&gt;
&lt;p&gt;Then MyZubster can create:&lt;/p&gt;
&lt;p&gt;MarketplaceOrder: REQUESTED&lt;/p&gt;
&lt;p&gt;with cryptographic evidence attached.&lt;/p&gt;
&lt;p&gt;No gas.&lt;/p&gt;
&lt;p&gt;No blockchain transaction.&lt;/p&gt;
&lt;p&gt;No payment.&lt;/p&gt;
&lt;p&gt;Just signed intent.&lt;/p&gt;
&lt;p&gt;Why not put every action on-chain?&lt;/p&gt;
&lt;p&gt;Because not every interaction needs blockchain finality.&lt;/p&gt;
&lt;p&gt;If every Marketplace request required a transaction, the UX would become unnecessarily heavy.&lt;/p&gt;
&lt;p&gt;Instead, the architecture I’m aiming for is:&lt;/p&gt;
&lt;p&gt;Marketplace request&lt;br&gt;
↓&lt;br&gt;
off-chain signature&lt;br&gt;
↓&lt;br&gt;
seller acceptance&lt;br&gt;
↓&lt;br&gt;
exchange lifecycle&lt;br&gt;
↓&lt;br&gt;
structured evidence&lt;br&gt;
↓&lt;br&gt;
optional blockchain anchor&lt;/p&gt;
&lt;p&gt;Blockchain should be used where permanence adds value.&lt;/p&gt;
&lt;p&gt;Not everywhere by default.&lt;/p&gt;
&lt;p&gt;User wallet vs infrastructure wallet&lt;/p&gt;
&lt;p&gt;This is also an important boundary.&lt;/p&gt;
&lt;p&gt;MyZubster already has server-side blockchain infrastructure for evidence anchoring.&lt;/p&gt;
&lt;p&gt;That wallet must stay completely separate from user wallets.&lt;/p&gt;
&lt;p&gt;User MetaMask&lt;br&gt;
!=&lt;br&gt;
MyZubster infrastructure wallet&lt;/p&gt;
&lt;p&gt;User-controlled wallet actions remain user-controlled.&lt;/p&gt;
&lt;p&gt;Server-side evidence operations remain isolated.&lt;/p&gt;
&lt;p&gt;How Zorgax can use this&lt;/p&gt;
&lt;p&gt;Once the wallet state is explicit, Zorgax can explain it correctly.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;User:&lt;br&gt;
Is my wallet verified?&lt;/p&gt;
&lt;p&gt;Zorgax:&lt;br&gt;
Yes. Your MetaMask wallet ownership has been verified.&lt;/p&gt;
&lt;p&gt;But if the user asks:&lt;/p&gt;
&lt;p&gt;Did I pay?&lt;/p&gt;
&lt;p&gt;Zorgax should answer:&lt;/p&gt;
&lt;p&gt;No confirmed payment evidence exists.&lt;br&gt;
Your wallet only signed the request.&lt;/p&gt;
&lt;p&gt;That distinction is essential.&lt;/p&gt;
&lt;p&gt;Security tests&lt;/p&gt;
&lt;p&gt;I’ve added tests covering:&lt;/p&gt;
&lt;p&gt;valid signer&lt;br&gt;
wrong signer&lt;br&gt;
expired challenge&lt;br&gt;
non-payment message semantics&lt;br&gt;
authenticated API wiring&lt;br&gt;
MetaMask provider integration&lt;/p&gt;
&lt;p&gt;The goal is not just to make the feature work.&lt;/p&gt;
&lt;p&gt;The goal is to make it hard to misunderstand.&lt;/p&gt;
&lt;p&gt;Current status&lt;/p&gt;
&lt;p&gt;The first implementation is now open as:&lt;/p&gt;
&lt;p&gt;PR #1385 — MetaMask wallet ownership linking&lt;/p&gt;
&lt;p&gt;The PR adds:&lt;/p&gt;
&lt;p&gt;wallet model&lt;br&gt;
challenge service&lt;br&gt;
signature verification&lt;br&gt;
authenticated API&lt;br&gt;
MetaMask UI&lt;br&gt;
disconnect flow&lt;br&gt;
security tests&lt;/p&gt;
&lt;p&gt;This is not yet an ETH payment system.&lt;/p&gt;
&lt;p&gt;It is the identity and ownership layer that payments or signed Marketplace actions can build on later.&lt;/p&gt;
&lt;p&gt;Why this matters for MyZubster&lt;/p&gt;
&lt;p&gt;The bigger change is architectural.&lt;/p&gt;
&lt;p&gt;MyZubster is gradually moving toward a model where different kinds of evidence are handled by different layers:&lt;/p&gt;
&lt;p&gt;MyZubster account&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;wallet control&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;signed intent&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Marketplace state&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;payment evidence&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;optional blockchain evidence&lt;/p&gt;
&lt;p&gt;instead of:&lt;/p&gt;
&lt;p&gt;connect wallet&lt;br&gt;
↓&lt;br&gt;
everything becomes blockchain&lt;/p&gt;
&lt;p&gt;For me, that is the more useful direction.&lt;/p&gt;
&lt;p&gt;MetaMask should not just be a button.&lt;/p&gt;
&lt;p&gt;It should become a cryptographic tool that adds proof exactly where proof is useful.&lt;/p&gt;
&lt;p&gt;Project: MyZubster&lt;br&gt;
Stack: Node.js, Express, MongoDB, React, ethers, MetaMask&lt;br&gt;
Current milestone: EVM wallet ownership verification&lt;br&gt;
Next milestone: signed Marketplace requests without gas&lt;/p&gt;
&lt;p&gt;Repository:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Implementation PR:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/1385&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster/pull/1385&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28567/building-metamask-wallet-ownership-verification-in-myzubster</guid>
<pubDate>Fri, 25 Sep 2026 04:23:07 +0000</pubDate>
</item>
<item>
<title>Building a Human-in-the-Loop GitHub Profile Automation with MyZubster and Zorgax</title>
<link>https://coderlegion.com/28491/building-a-human-in-the-loop-github-profile-automation-with-myzubster-and-zorgax</link>
<description>&lt;p&gt;thorization with the user.&lt;/p&gt;
&lt;p&gt;Result&lt;/p&gt;
&lt;p&gt;The full GitHub profile automation loop is now working:&lt;/p&gt;
&lt;p&gt;MyZubster&lt;br&gt;
→ Zorgax&lt;br&gt;
→ GitHub analysis&lt;br&gt;
→ generated README&lt;br&gt;
→ final preview&lt;br&gt;
→ explicit human approval&lt;br&gt;
→ authenticated GitHub write&lt;br&gt;
→ public commit&lt;/p&gt;
&lt;p&gt;It took more than generating Markdown.&lt;/p&gt;
&lt;p&gt;The difficult parts were identity, OAuth, state handoff, parsing, permission boundaries and making the final action auditable.&lt;/p&gt;
&lt;p&gt;And those are exactly the parts that make AI automation interesting to build.&lt;/p&gt;
&lt;p&gt;Project: MyZubster&lt;br&gt;
AI assistant: Zorgax&lt;br&gt;
Main repository: MyZubster-Ecosystem/myzubster&lt;br&gt;
Test profile: @myzubster&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28491/building-a-human-in-the-loop-github-profile-automation-with-myzubster-and-zorgax</guid>
<pubDate>Thu, 24 Sep 2026 14:25:14 +0000</pubDate>
</item>
<item>
<title>Ditching the Monolith: An Introduction to Multi-Agent Systems for Node.js Devs</title>
<link>https://coderlegion.com/28294/ditching-the-monolith-an-introduction-to-multi-agent-systems-for-node-js-devs</link>
<description>&lt;p&gt;In the world of modern software design, we are constantly searching for ways to make our systems more resilient, adaptable, and scalable. If you have spent your career building traditional monolithic applications or synchronous microservices, you are likely familiar with the headaches of centralized control. When one coordinator service fails, the entire application can grind to a halt. To solve this, forward-thinking engineers are turning to a design paradigm known as multi-agent systems.&lt;/p&gt;
&lt;h4 id=&quot;what-is-a-multi-agent-system-&quot;&gt;What is a Multi-Agent System?&lt;/h4&gt;
&lt;p&gt;A multi-agent system is a network of independent, self-contained software programs (called &quot;agents&quot;) that work together to solve complex problems. Instead of relying on a single, massive program to make every decision, these systems break tasks down among specialized digital assistants that communicate and coordinate with one another. Each agent has its own specific goal, operational rules, and local knowledge, allowing them to cooperate to achieve a larger objective that none of them could finish alone.&lt;/p&gt;
&lt;h4 id=&quot;the-kitchen-analogy-who-is-cooking-your-order-&quot;&gt;The Kitchen Analogy: Who Is Cooking Your Order?&lt;/h4&gt;
&lt;p&gt;To understand how this behaves, think of a busy professional restaurant kitchen. You do not have one single super-chef cooking every single dish, washing the plates, taking customer orders, and running food to tables. That would create an immediate bottleneck. &lt;/p&gt;
&lt;p&gt;Instead, you have a head chef, a sous chef, a pastry chef, and a dishwasher. Each of these people is an &quot;agent&quot; with a highly specific role. They do not need to know the exact details of how the others perform their tasks; the pastry chef does not need to know how to clean a fish, and the dishwasher does not need to know how to bake a soufflé. However, they pass messages and resources back and forth (such as &quot;order up!&quot; or &quot;need clean plates!&quot;) to run the entire kitchen smoothly. If the pastry chef temporarily falls behind, the rest of the kitchen can still prepare appetizers and main courses independently.&lt;/p&gt;
&lt;h4 id=&quot;why-it-matters-daily-in-the-tech-industry-1&quot;&gt;Why It Matters Daily in the Tech Industry&lt;/h4&gt;
&lt;p&gt;In the tech industry, engineers use multi-agent architectures to prevent system-wide bottlenecks and cascading failures. In standard APIs, if your main process gets overwhelmed with database operations, your user interface freezes. By distributing these responsibilities among autonomous agents, you decouple your system's critical paths.&lt;/p&gt;
&lt;p&gt;For example, in a high-traffic e-commerce platform, instead of having one monolithic script process a payment, update inventory, and send confirmation emails sequentially, you delegate these tasks to dedicated agents. If the notification agent experiences an outage or third-party API rate limit, the payment and inventory agents keep operating without interruption. This separation allows software teams to scale individual components dynamically and deploy updates to one agent without risking the stability of the entire network.&lt;/p&gt;
&lt;h4 id=&quot;simulating-multi-agent-coordination-in-node-js&quot;&gt;Simulating Multi-Agent Coordination in Node.js&lt;/h4&gt;
&lt;p&gt;Below is a simple Node.js example demonstrating how multiple independent agents can coordinate tasks using an event-driven system. We will set up an &lt;code&gt;OrderAgent&lt;/code&gt;, an &lt;code&gt;InventoryAgent&lt;/code&gt;, and a &lt;code&gt;NotificationAgent&lt;/code&gt; that communicate via an event emitter.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const EventEmitter = require('events');
const agentBus = new EventEmitter();

// Agent 1: Takes care of incoming customer requests
class OrderAgent {
  constructor() {
    agentBus.on('order_placed', (order) =&amp;gt; this.processOrder(order));
  }

  processOrder(order) {
    console.log(`[OrderAgent] Received order #${order.id}. Passing to inventory...`);
    // Trigger the next autonomous agent
    agentBus.emit('check_inventory', order);
  }
}

// Agent 2: Manages physical product availability
class InventoryAgent {
  constructor() {
    agentBus.on('check_inventory', (order) =&amp;gt; this.verifyStock(order));
  }

  verifyStock(order) {
    console.log(`[InventoryAgent] Checking stock for ${order.item}...`);
    const itemInStock = true; // Simulating a local database lookup
    
    if (itemInStock) {
      console.log(`[InventoryAgent] Stock confirmed for order #${order.id}.`);
      agentBus.emit('dispatch_notification', order);
    } else {
      console.log(`[InventoryAgent] Stock unavailable for order #${order.id}.`);
    }
  }
}

// Agent 3: Handles user communications
class NotificationAgent {
  constructor() {
    agentBus.on('dispatch_notification', (order) =&amp;gt; this.sendReceipt(order));
  }

  sendReceipt(order) {
    console.log(`[NotificationAgent] Email sent: Order #${order.id} for a ${order.item} is confirmed!`);
  }
}

// Instantiate our autonomous agents to start listening
new OrderAgent();
new InventoryAgent();
new NotificationAgent();

// Simulate a customer purchase
console.log('--- Initiating Customer Order Flow ---');
agentBus.emit('order_placed', { id: 4509, item: 'Mechanical Keyboard' });
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-takeaway-2&quot;&gt;The Takeaway&lt;/h4&gt;
&lt;p&gt;Transitioning to a multi-agent paradigm is less about writing more code and more about changing your architectural mindset from strict, top-down instruction to distributed, cooperative behavior. By giving your software components the independence to make decisions and communicate fluidly, you build highly resilient, loosely coupled applications that can gracefully handle the messy, unpredictable demands of real-world production traffic.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/unlocking-decentralized-power-why-multi.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28294/ditching-the-monolith-an-introduction-to-multi-agent-systems-for-node-js-devs</guid>
<pubDate>Wed, 23 Sep 2026 08:50:33 +0000</pubDate>
</item>
<item>
<title>Building MyZubster as a Verifiable AI Development Ecosystem</title>
<link>https://coderlegion.com/28184/building-myzubster-as-a-verifiable-ai-development-ecosystem</link>
<description>&lt;p&gt;uilding MyZubster as a Verifiable AI Development Ecosystem: Metaverse, GitHub Evidence, Zorgax Knowledge, and DevelopmentRequest&lt;br&gt;
Today we had the opportunity to present and demonstrate part of the MyZubster ecosystem during an institutional hearing.&lt;br&gt;
Instead of presenting only slides or a conceptual roadmap, we showed the system directly. We opened the MyZubster metaverse, explained a real pilot involving an actual user, discussed how GitHub activity can become technical evidence, and described the architecture we are building around Zorgax, verified knowledge, software development, wallets, and human review.&lt;br&gt;
The most important outcome for us was not the meeting itself.&lt;br&gt;
It was realizing that the different components we have been building are starting to form a coherent technical system.&lt;br&gt;
The architecture is moving toward this loop:&lt;br&gt;
Real Person&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Real Activity&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Data / Evidence&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MyZubster&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verified Knowledge&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zorgax AI&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DevelopmentRequest&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Developer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Code / GitHub&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tests&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Independent Verification&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;New Knowledge&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Identity / Wallet&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Metaverse Interaction&lt;br&gt;
The goal is not simply to build another AI assistant, another marketplace, or another metaverse.&lt;br&gt;
The goal is to connect all of these components around one principle:&lt;br&gt;
AI can assist the process, but evidence and verification should remain inspectable.&lt;/p&gt;
&lt;p&gt;The Metaverse Is Only the Interface&lt;br&gt;
One of the things we demonstrated was the MyZubster metaverse.&lt;br&gt;
For us, the metaverse is not the entire product.&lt;br&gt;
It is one possible user interface for a much larger system.&lt;br&gt;
The long-term idea is that a user should eventually be able to enter the environment with a digital identity, connect a wallet, interact with people or services, and have access to a history of technical contributions and activities.&lt;br&gt;
That means the visible environment may look like a metaverse, but behind it we want something closer to a verifiable contribution graph.&lt;br&gt;
A person may have:&lt;br&gt;
Identity&lt;br&gt;
  ↓&lt;br&gt;
Wallet&lt;br&gt;
  ↓&lt;br&gt;
GitHub Account&lt;br&gt;
  ↓&lt;br&gt;
Issues&lt;br&gt;
  ↓&lt;br&gt;
Pull Requests&lt;br&gt;
  ↓&lt;br&gt;
Commits&lt;br&gt;
  ↓&lt;br&gt;
Tests&lt;br&gt;
  ↓&lt;br&gt;
Reviews&lt;br&gt;
  ↓&lt;br&gt;
Verified Contributions&lt;br&gt;
The important distinction is that a wallet alone does not prove contribution.&lt;br&gt;
A GitHub account alone does not prove contribution either.&lt;br&gt;
What matters is the history of actions and the evidence attached to them.&lt;br&gt;
A commit may be evidence.&lt;br&gt;
A passing test may be evidence.&lt;br&gt;
A reviewed pull request may be evidence.&lt;br&gt;
A reproducible result may be evidence.&lt;br&gt;
But none of these should automatically be interpreted as proof of everything.&lt;br&gt;
That distinction becomes very important once AI is involved.&lt;br&gt;
A Real Pilot: Nicola&lt;br&gt;
We also presented a real pilot involving Nicola, one of the people testing and contributing to the ecosystem.&lt;br&gt;
The first experiment was intentionally simple.&lt;br&gt;
A physical kefir handover was performed and digitally recorded through MyZubster.&lt;br&gt;
The workflow represented something like:&lt;br&gt;
HAND_DELIVERY&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Receiver confirms receipt&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Status updated&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RECORDED&lt;br&gt;
There was no payment involved.&lt;br&gt;
The specific transaction was also not recorded as an on-chain blockchain transaction.&lt;br&gt;
That is important because we do not want to describe the experiment as something it was not.&lt;br&gt;
The purpose was not to scientifically validate kefir.&lt;br&gt;
The purpose was to answer a much more technical question:&lt;br&gt;
Can a real-world event be represented digitally in a way that produces useful evidence?&lt;/p&gt;
&lt;p&gt;The answer was yes.&lt;br&gt;
From there, Nicola also tested other parts of MyZubster, reported problems, and worked on his own software.&lt;br&gt;
That gave us a more interesting participation model.&lt;br&gt;
Instead of thinking about users as static roles, we can model participation as a progression:&lt;br&gt;
User&lt;br&gt;
  ↓&lt;br&gt;
Tester&lt;br&gt;
  ↓&lt;br&gt;
Contributor&lt;br&gt;
  ↓&lt;br&gt;
Developer&lt;br&gt;
  ↓&lt;br&gt;
Evidence Producer&lt;br&gt;
  ↓&lt;br&gt;
Knowledge Contributor&lt;br&gt;
This is one of the ideas that could eventually connect the metaverse, GitHub, wallets, community reputation, and Zorgax.&lt;br&gt;
Zorgax: Chat Is Not Knowledge&lt;br&gt;
One of the most important architectural decisions we have made recently concerns Zorgax.&lt;br&gt;
We do not want to treat an AI-generated answer as verified knowledge.&lt;br&gt;
That sounds obvious, but many systems effectively do exactly that.&lt;br&gt;
A language model generates a response.&lt;br&gt;
The response is stored.&lt;br&gt;
Later the system retrieves that response as if it were truth.&lt;br&gt;
That is dangerous.&lt;br&gt;
So our architecture starts from a different assumption:&lt;br&gt;
A message is not knowledge.&lt;/p&gt;
&lt;p&gt;A message may contain useful information.&lt;br&gt;
It may become a candidate for knowledge.&lt;br&gt;
But it must go through additional stages.&lt;br&gt;
Our conceptual flow looks like this:&lt;br&gt;
User Question&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI Response&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Candidate Information&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sources&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Evidence&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Human / System Review&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verified Knowledge&lt;br&gt;
Only after that should the information become part of a trusted knowledge layer.&lt;br&gt;
The Zorgax Knowledge Layer&lt;br&gt;
We have been building a dedicated Knowledge Layer for Zorgax.&lt;br&gt;
The knowledge model is designed to preserve more than just text.&lt;br&gt;
A useful knowledge object needs context.&lt;br&gt;
It may include concepts such as:&lt;br&gt;
Knowledge&lt;br&gt;
 ├── contributionId&lt;br&gt;
 ├── title&lt;br&gt;
 ├── content&lt;br&gt;
 ├── contentHash&lt;br&gt;
 ├── version&lt;br&gt;
 ├── status&lt;br&gt;
 ├── visibility&lt;br&gt;
 ├── provenance&lt;br&gt;
 ├── source&lt;br&gt;
 ├── evidence&lt;br&gt;
 └── lineage&lt;br&gt;
The contentHash is particularly important.&lt;br&gt;
It gives us a deterministic identity for the canonical content.&lt;br&gt;
If the content changes, the hash changes.&lt;br&gt;
This allows downstream systems to know exactly which version of knowledge they are using.&lt;br&gt;
For example, a development task may reference:&lt;br&gt;
{&lt;br&gt;
  &quot;contributionId&quot;: &quot;ZK-123&quot;,&lt;br&gt;
  &quot;contentHash&quot;: &quot;f30c...&quot;,&lt;br&gt;
  &quot;version&quot;: 3,&lt;br&gt;
  &quot;title&quot;: &quot;Metaverse Identity Architecture&quot;&lt;br&gt;
}&lt;br&gt;
Later, the knowledge may evolve to version 4.&lt;br&gt;
The development task should still be able to show:&lt;br&gt;
This implementation was based on version 3.&lt;/p&gt;
&lt;p&gt;That creates much better provenance.&lt;br&gt;
Preview Before Persistent Writes&lt;br&gt;
Another principle we implemented is:&lt;br&gt;
The AI should not silently perform consequential writes.&lt;/p&gt;
&lt;p&gt;For operations such as creating a DevelopmentRequest, the system first produces a preview.&lt;br&gt;
Conceptually:&lt;br&gt;
User Intent&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Normalized Input&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Knowledge Snapshot&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Preview&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Digest&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Explicit Confirmation&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Persistent Write&lt;br&gt;
A digest is generated from the preview.&lt;br&gt;
Then the user must confirm using a value such as:&lt;br&gt;
CONFERMA DEV a1b2c3d4&lt;br&gt;
If the preview changes, the digest changes.&lt;br&gt;
If the digest does not match, the operation fails.&lt;br&gt;
This prevents a situation where the user approves one thing and the system writes something different.&lt;br&gt;
DevelopmentRequest: Turning Knowledge Into Work&lt;br&gt;
The next architecture problem was:&lt;br&gt;
How do we transform verified knowledge into software development?&lt;/p&gt;
&lt;p&gt;Our solution is a new neutral object called DevelopmentRequest.&lt;br&gt;
A DevelopmentRequest is not a bounty.&lt;br&gt;
It is not a GitHub issue.&lt;br&gt;
It is not a payment request.&lt;br&gt;
It represents the work that needs to be done.&lt;br&gt;
The current lifecycle is:&lt;br&gt;
DRAFT&lt;br&gt;
  ↓&lt;br&gt;
OPEN&lt;br&gt;
  ↓&lt;br&gt;
IN_PROGRESS&lt;br&gt;
  ↓&lt;br&gt;
SUBMITTED&lt;br&gt;
  ↓&lt;br&gt;
VERIFIED&lt;br&gt;
or:&lt;br&gt;
SUBMITTED&lt;br&gt;
  ↓&lt;br&gt;
REJECTED&lt;br&gt;
The model also supports cancellation.&lt;br&gt;
Conceptually, the object contains:&lt;br&gt;
DevelopmentRequest {&lt;br&gt;
  requestId&lt;br&gt;
  requestHash&lt;br&gt;
  createdBy&lt;/p&gt;
&lt;p&gt;  title&lt;br&gt;
  description&lt;/p&gt;
&lt;p&gt;  knowledgeRefs[]&lt;/p&gt;
&lt;p&gt;  requirements[]&lt;br&gt;
  acceptanceTests[]&lt;br&gt;
  evidenceRequired[]&lt;/p&gt;
&lt;p&gt;  status&lt;br&gt;
  assigneeId&lt;/p&gt;
&lt;p&gt;  submission&lt;br&gt;
  review&lt;/p&gt;
&lt;p&gt;  bountyReference // optional&lt;br&gt;
}&lt;br&gt;
The important part is that the development request is anchored to verified knowledge.&lt;br&gt;
Snapshotting Knowledge&lt;br&gt;
When a DevelopmentRequest is created, Zorgax does not simply store a knowledge ID.&lt;br&gt;
It stores a snapshot reference.&lt;br&gt;
For example:&lt;br&gt;
knowledgeRefs: [&lt;br&gt;
  {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;contributionId: &quot;ZK-123&quot;,
contentHash: &quot;abc123...&quot;,
version: 2,
title: &quot;Verified Architecture&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;br&gt;
]&lt;br&gt;
Before the request is actually committed, the system revalidates those knowledge references.&lt;br&gt;
If the underlying content has changed between preview and commit, the operation fails.&lt;br&gt;
Conceptually:&lt;br&gt;
if (&lt;br&gt;
  expected.contentHash !== current.contentHash ||&lt;br&gt;
  expected.version !== current.version&lt;br&gt;
) {&lt;br&gt;
  throw new Error(&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;Knowledge changed after preview&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  );&lt;br&gt;
}&lt;br&gt;
This gives us something extremely useful:&lt;br&gt;
development provenance.&lt;br&gt;
We can eventually say:&lt;br&gt;
Feature X&lt;br&gt;
was implemented because of&lt;br&gt;
DevelopmentRequest Y&lt;br&gt;
which was based on&lt;br&gt;
Knowledge Z version 2&lt;br&gt;
which came from&lt;br&gt;
Evidence A + Evidence B&lt;br&gt;
That is much stronger than simply linking a GitHub issue to a random conversation.&lt;br&gt;
Submission Is Not Verification&lt;br&gt;
This is another important rule.&lt;br&gt;
A developer may submit something.&lt;br&gt;
That does not mean the work is verified.&lt;br&gt;
So the workflow separates:&lt;br&gt;
SUBMITTED&lt;br&gt;
from:&lt;br&gt;
VERIFIED&lt;br&gt;
The contributor submits evidence such as:&lt;br&gt;
{&lt;br&gt;
  &quot;evidenceRefs&quot;: [&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;test:integration:green&quot;,
&quot;demo:https://...&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ],&lt;br&gt;
  &quot;commitRefs&quot;: [&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;commit:abc123&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ]&lt;br&gt;
}&lt;br&gt;
But a separate reviewer is responsible for verification.&lt;br&gt;
The reviewer cannot be the creator or the assigned developer.&lt;br&gt;
Conceptually:&lt;br&gt;
if (&lt;br&gt;
  reviewer === request.createdBy ||&lt;br&gt;
  reviewer === request.assigneeId&lt;br&gt;
) {&lt;br&gt;
  throw new Error(&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;Review must be independent&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  );&lt;br&gt;
}&lt;br&gt;
This is one of the foundations for future contribution reputation.&lt;br&gt;
Strengthening the Evidence Model&lt;br&gt;
We are now tightening this further.&lt;br&gt;
The first implementation verified that evidence existed.&lt;br&gt;
But that is not enough.&lt;br&gt;
Imagine this DevelopmentRequest:&lt;br&gt;
Evidence required:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Unit tests pass&lt;/li&gt;
&lt;li&gt;Security review completed&lt;/li&gt;
&lt;li&gt;Metaverse integration demonstrated&lt;br&gt;
And the developer submits only:&lt;br&gt;
commit:abc123&lt;br&gt;
Technically, evidence exists.&lt;br&gt;
But the required evidence is not actually satisfied.&lt;br&gt;
So the stronger model connects each requirement to a specific reference.&lt;br&gt;
For example:&lt;br&gt;
{&lt;br&gt;
  &quot;evidence&quot;: [
 {
   &quot;requirement&quot;: &quot;Unit tests pass&quot;,
   &quot;reference&quot;: &quot;ci:test-run:4821&quot;
 },
 {
   &quot;requirement&quot;: &quot;Security review completed&quot;,
   &quot;reference&quot;: &quot;review:security:913&quot;
 },
 {
   &quot;requirement&quot;: &quot;Metaverse integration demonstrated&quot;,
   &quot;reference&quot;: &quot;demo:metaverse:42&quot;
 }
  ]&lt;br&gt;
}&lt;br&gt;
Then the verification rule becomes:&lt;br&gt;
VERIFIED&lt;br&gt;
only if&lt;br&gt;
every evidenceRequired item&lt;br&gt;
has corresponding evidence&lt;br&gt;
Conceptually:&lt;br&gt;
const missingEvidence =&lt;br&gt;
  requiredEvidence.filter(&lt;br&gt;
 requirement =&amp;gt;&lt;br&gt;
   !coveredRequirements.has(requirement)&lt;br&gt;
  );&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;if (missingEvidence.length) {&lt;br&gt;
  throw new Error(&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;`Missing required evidence: ${missingEvidence.join(&quot;, &quot;)}`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  );&lt;br&gt;
}&lt;br&gt;
This may look like a small implementation detail.&lt;br&gt;
It is not.&lt;br&gt;
It changes the meaning of VERIFIED.&lt;br&gt;
Without this rule:&lt;br&gt;
VERIFIED = someone attached something&lt;br&gt;
With this rule:&lt;br&gt;
VERIFIED = the declared evidence requirements were actually covered&lt;br&gt;
That is a much stronger guarantee.&lt;br&gt;
DevelopmentRequest Is Not a Bounty&lt;br&gt;
Another architectural decision we intentionally made was separating development from economics.&lt;br&gt;
A DevelopmentRequest can exist without any reward.&lt;br&gt;
That means:&lt;br&gt;
Knowledge&lt;br&gt;
   ↓&lt;br&gt;
DevelopmentRequest&lt;br&gt;
   ↓&lt;br&gt;
Implementation&lt;br&gt;
   ↓&lt;br&gt;
Verification&lt;br&gt;
can operate independently.&lt;br&gt;
Then, optionally:&lt;br&gt;
DevelopmentRequest&lt;br&gt;
   ↓&lt;br&gt;
Bounty&lt;br&gt;
   ↓&lt;br&gt;
Reward&lt;br&gt;
   ↓&lt;br&gt;
Settlement&lt;br&gt;
may be attached later.&lt;br&gt;
This distinction matters.&lt;br&gt;
A university research project may not use bounties.&lt;br&gt;
An open-source volunteer may contribute without payment.&lt;br&gt;
A company may use fixed contracts.&lt;br&gt;
A decentralized community may use tokens.&lt;br&gt;
A marketplace may use fiat.&lt;br&gt;
The technical work object should not care.&lt;br&gt;
That is why the model contains only:&lt;br&gt;
bountyReference: null&lt;br&gt;
as an optional link.&lt;br&gt;
The economics remain a separate subsystem.&lt;br&gt;
GitHub as Evidence, Not Truth&lt;br&gt;
GitHub is also important in the architecture, but we do not want to make the mistake of treating GitHub itself as truth.&lt;br&gt;
A commit proves that a commit exists.&lt;br&gt;
It does not automatically prove:&lt;br&gt;
the feature works&lt;br&gt;
the requirement is satisfied&lt;br&gt;
the code is secure&lt;br&gt;
the architecture is correct&lt;br&gt;
the scientific result is valid&lt;br&gt;
So GitHub becomes one evidence source among others.&lt;br&gt;
Future DevelopmentRequest verification could combine:&lt;br&gt;
GitHub commit&lt;br&gt;
+&lt;br&gt;
CI test result&lt;br&gt;
+&lt;br&gt;
integration test&lt;br&gt;
+&lt;br&gt;
human review&lt;br&gt;
+&lt;br&gt;
external evidence&lt;br&gt;
This is also why we are working toward a Contribution Graph.&lt;br&gt;
The graph can connect:&lt;br&gt;
Developer&lt;br&gt;
   ↓&lt;br&gt;
Issue&lt;br&gt;
   ↓&lt;br&gt;
DevelopmentRequest&lt;br&gt;
   ↓&lt;br&gt;
Commit&lt;br&gt;
   ↓&lt;br&gt;
Pull Request&lt;br&gt;
   ↓&lt;br&gt;
Test&lt;br&gt;
   ↓&lt;br&gt;
Review&lt;br&gt;
   ↓&lt;br&gt;
Verified Contribution&lt;br&gt;
That provides a much richer representation of technical reputation.&lt;br&gt;
Where the Metaverse Fits Back In&lt;br&gt;
At first glance, the metaverse and DevelopmentRequest may seem unrelated.&lt;br&gt;
But they are actually part of the same architecture.&lt;br&gt;
Imagine a person entering MyZubster.&lt;br&gt;
The environment could eventually know:&lt;br&gt;
Identity&lt;br&gt;
Wallet&lt;br&gt;
GitHub History&lt;br&gt;
Verified Contributions&lt;br&gt;
Knowledge Contributions&lt;br&gt;
Development Activity&lt;br&gt;
Community Activity&lt;br&gt;
The user does not need to see all of this as raw database records.&lt;br&gt;
The metaverse can become an interface for it.&lt;br&gt;
For example, someone could enter a virtual lab or development room and interact with a project.&lt;br&gt;
Behind the scene:&lt;br&gt;
Metaverse Project&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DevelopmentRequest&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verified Knowledge&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GitHub Repository&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Contributors&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Evidence&lt;br&gt;
The virtual environment therefore becomes a way to navigate a real technical graph.&lt;br&gt;
What We Demonstrated Today&lt;br&gt;
During the hearing, we were able to connect several of these concepts in one presentation.&lt;br&gt;
We demonstrated the MyZubster metaverse directly.&lt;br&gt;
We explained Nicola's real-world pilot.&lt;br&gt;
We discussed his AI-related development work.&lt;br&gt;
We explained how user activity can evolve into testing and software contribution.&lt;br&gt;
We described the role of GitHub history.&lt;br&gt;
We explained Zorgax.&lt;br&gt;
We described the difference between AI conversation and verified knowledge.&lt;br&gt;
We presented our university outreach and the idea that scientific pilots require scientific methodology.&lt;br&gt;
And we explained the broader direction:&lt;br&gt;
People perform real activities, software records evidence, AI assists the process, developers build solutions, and independent verification creates new trusted knowledge.&lt;/p&gt;
&lt;p&gt;Universities and Scientific Validation&lt;br&gt;
We also discussed the academic side of the project.&lt;br&gt;
We have started conversations with several universities and research groups around areas including AI, food science, fermentation, circular economy, environmental measurement, LCA/LCSA, and water reuse.&lt;br&gt;
We are intentionally keeping the wording precise.&lt;br&gt;
These are exploratory discussions.&lt;br&gt;
They are not formal partnerships unless and until they are formally established.&lt;br&gt;
The reason universities matter is structural.&lt;br&gt;
MyZubster can build:&lt;br&gt;
software&lt;br&gt;
traceability&lt;br&gt;
data collection&lt;br&gt;
knowledge infrastructure&lt;br&gt;
AI assistance&lt;br&gt;
development workflows&lt;br&gt;
evidence systems&lt;br&gt;
But scientific validation requires additional layers:&lt;br&gt;
scientific protocol&lt;br&gt;
experimental design&lt;br&gt;
control groups&lt;br&gt;
measurement methodology&lt;br&gt;
ethical assessment&lt;br&gt;
statistical analysis&lt;br&gt;
domain expertise&lt;br&gt;
An AI system should not pretend to replace those responsibilities.&lt;br&gt;
LIFE 2027 as a Possible Pilot Framework&lt;br&gt;
We are also exploring whether some of these ideas could eventually fit into a LIFE 2027 project.&lt;br&gt;
Again, this is an exploratory direction.&lt;br&gt;
There is no approved or funded MyZubster LIFE project at this stage.&lt;br&gt;
The interesting part for us is the possibility of connecting environmental pilots with the same verifiable architecture.&lt;br&gt;
For example:&lt;br&gt;
Circular Water Pilot&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sensors / Data&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Evidence&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;University Protocol&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verified Knowledge&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zorgax&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DevelopmentRequest&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Software Improvement&lt;br&gt;
The same architecture could theoretically apply to fermentation, environmental monitoring, circular economy, Living Labs, or other pilot domains.&lt;br&gt;
That is one reason we are trying to keep the core infrastructure domain-neutral.&lt;br&gt;
The Broader Architecture&lt;br&gt;
If we compress the whole system, the current direction looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                ┌──────────────────┐
                │    Real World    │
                └────────┬─────────┘
                         │
                         ▼
                ┌──────────────────┐
                │ Data / Evidence  │
                └────────┬─────────┘
                         │
                         ▼
                ┌──────────────────┐
                │ MyZubster        │
                │ Knowledge Layer  │
                └────────┬─────────┘
                         │
                         ▼
                ┌──────────────────┐
                │      Zorgax      │
                │   AI Assistant   │
                └────────┬─────────┘
                         │
                         ▼
                ┌──────────────────┐
                │DevelopmentRequest│
                └────────┬─────────┘
                         │
                         ▼
                ┌──────────────────┐
                │    Developer     │
                └────────┬─────────┘
                         │
                         ▼
                ┌──────────────────┐
                │ GitHub / CI      │
                └────────┬─────────┘
                         │
                         ▼
                ┌──────────────────┐
                │ Evidence Review  │
                └────────┬─────────┘
                         │
                         ▼
                ┌──────────────────┐
                │ Verified Result  │
                └────────┬─────────┘
                         │
                         ▼
                ┌──────────────────┐
                │  New Knowledge   │
                └────────┬─────────┘
                         │
                         ▼
                ┌──────────────────┐
                │ Identity/Wallet  │
                │ Metaverse Layer  │
                └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a feedback loop.&lt;br&gt;
The output of development can become new knowledge.&lt;br&gt;
That knowledge can generate new development requests.&lt;br&gt;
Those development requests create more evidence.&lt;br&gt;
The evidence can improve the knowledge layer.&lt;br&gt;
What We Have Implemented So Far&lt;br&gt;
At the technical level, the recent work includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a Zorgax Knowledge Layer with provenance, content hashing, versioning, visibility and evidence metadata;&lt;/li&gt;
&lt;li&gt;explicit distinction between conversation, candidate information and verified knowledge;&lt;/li&gt;
&lt;li&gt;fail-closed behavior for sensitive knowledge operations;&lt;/li&gt;
&lt;li&gt;preview + digest + explicit human confirmation before persistent DevelopmentRequest creation;&lt;/li&gt;
&lt;li&gt;snapshotting of Knowledge references using contentHash and version;&lt;/li&gt;
&lt;li&gt;idempotent DevelopmentRequest creation;&lt;/li&gt;
&lt;li&gt;a lifecycle covering DRAFT, OPEN, IN_PROGRESS, SUBMITTED, VERIFIED, REJECTED and CANCELLED;&lt;/li&gt;
&lt;li&gt;contributor assignment;&lt;/li&gt;
&lt;li&gt;submission of commit and evidence references;&lt;/li&gt;
&lt;li&gt;independent review rules;&lt;/li&gt;
&lt;li&gt;strict separation between DevelopmentRequest and bounty/payment systems;&lt;/li&gt;
&lt;li&gt;integration points for future GitHub contribution tracking;&lt;/li&gt;
&lt;li&gt;an evidence model being strengthened so every required evidence item must be satisfied before VERIFIED.&lt;br&gt;
This is still evolving.&lt;br&gt;
There are important areas we still want to improve, including stronger revision models, more precise evidence objects, route-level tests, better contribution graph integration, and stronger lineage across Knowledge revisions.&lt;br&gt;
The Principle Behind the Architecture&lt;br&gt;
The most important idea is probably this:&lt;br&gt;
We are not trying to automate trust.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We are trying to make trust more inspectable.&lt;br&gt;
AI can help generate code.&lt;br&gt;
AI can help search.&lt;br&gt;
AI can help organize knowledge.&lt;br&gt;
AI can help transform requirements into development tasks.&lt;br&gt;
AI can help developers.&lt;br&gt;
But an AI response should not automatically become truth.&lt;br&gt;
A commit should not automatically become proof.&lt;br&gt;
A wallet should not automatically become reputation.&lt;br&gt;
A submission should not automatically become verification.&lt;br&gt;
The architecture should preserve the difference.&lt;br&gt;
That is what we are trying to encode in the system itself.&lt;br&gt;
What Comes Next&lt;br&gt;
The next engineering steps are straightforward.&lt;br&gt;
We want to finish the stronger DevelopmentRequest evidence invariant, integrate the workflow into the operational branch, and then continue toward the GitHub and Contribution Graph bridge.&lt;br&gt;
After that, we want Zorgax to be able to move through a much richer development loop:&lt;br&gt;
Question&lt;br&gt;
   ↓&lt;br&gt;
Verified Knowledge&lt;br&gt;
   ↓&lt;br&gt;
Architecture&lt;br&gt;
   ↓&lt;br&gt;
DevelopmentRequest&lt;br&gt;
   ↓&lt;br&gt;
Implementation&lt;br&gt;
   ↓&lt;br&gt;
Tests&lt;br&gt;
   ↓&lt;br&gt;
Independent Verification&lt;br&gt;
   ↓&lt;br&gt;
Delivery&lt;br&gt;
   ↓&lt;br&gt;
Optional Reward&lt;br&gt;
   ↓&lt;br&gt;
New Knowledge&lt;br&gt;
At the same time, we will keep exploring how this architecture can work with real users, academic research, environmental pilots, and the MyZubster metaverse.&lt;br&gt;
The meeting today was useful because we did not have to describe this only as an abstract roadmap.&lt;br&gt;
We were able to show pieces of it working.&lt;br&gt;
There is still a lot to build.&lt;br&gt;
But the architecture is becoming increasingly clear.&lt;br&gt;
Real people create activity. Activity creates evidence. Evidence can become verified knowledge. Knowledge can generate development. Development creates new evidence. AI assists the entire loop without becoming the sole authority.&lt;/p&gt;
&lt;p&gt;That is the direction we are building toward with MyZubster and Zorgax.&lt;br&gt;
MyZubster&lt;br&gt;
Open Source • Verified Knowledge • AI-Assisted Development • GitHub Evidence • Metaverse • Digital Identity • Community&lt;/p&gt;
&lt;h2 id=&quot;ai-opensource-softwarearchitecture-softwaredevelopment-github-metaverse-web3-knowledgegraph-artificialintelligence-nodejs-developers-myzubster-zorgax&quot;&gt;AI #OpenSource #SoftwareArchitecture #SoftwareDevelopment #GitHub #Metaverse #Web3 #KnowledgeGraph #ArtificialIntelligence #NodeJS #Developers #MyZubster #Zorgax&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28184/building-myzubster-as-a-verifiable-ai-development-ecosystem</guid>
<pubDate>Tue, 22 Sep 2026 15:42:36 +0000</pubDate>
</item>
<item>
<title>Stop Killing Your Database: The Power of Connection Pooling</title>
<link>https://coderlegion.com/28133/stop-killing-your-database-the-power-of-connection-pooling</link>
<description>&lt;p&gt;Connection pooling is a software development technique used to maintain a cache of database connections that can be reused for future requests. Instead of opening and closing a brand-new connection to the database every time a user wants to read or write data, your application keeps a &quot;pool&quot; of active connections ready to go. This drastically reduces the overhead of establishing new database handshakes over and over again.&lt;/p&gt;
&lt;h4 id=&quot;the-pizza-delivery-analogy&quot;&gt;The Pizza Delivery Analogy&lt;/h4&gt;
&lt;p&gt;Imagine you run a busy pizza shop. Every time a delivery order comes in, instead of sending a driver who is already waiting at the shop, you post a job listing online, interview a candidate, hire them, buy them a car, have them deliver the pizza, and then fire them and sell the car immediately after. &lt;/p&gt;
&lt;p&gt;It sounds completely insane, right? Yet, that is exactly what your server does when it creates a brand-new database connection for every single HTTP request, only to destroy it a millisecond later. &lt;/p&gt;
&lt;p&gt;Connection pooling is like hiring a dedicated team of five delivery drivers who hang out at the shop. When an order is ready, one driver grabs it, delivers it, and immediately returns to the shop to wait for the next order.&lt;/p&gt;
&lt;h4 id=&quot;why-connection-pooling-matters-daily&quot;&gt;Why Connection Pooling Matters Daily&lt;/h4&gt;
&lt;p&gt;In production environments, creating a database connection is an expensive operation that involves cryptographic handshakes, network latency, and authentication checks. If your Node.js application receives 100 concurrent API requests and tries to spin up 100 separate database connections simultaneously, your MySQL database will quickly run out of memory or reject connections altogether, throwing the dreaded &quot;Too many connections&quot; error.&lt;/p&gt;
&lt;p&gt;By implementing a connection pool, you cap the maximum number of active database connections, protect your database from crashing under sudden traffic spikes, and shave off up to 90% of your API response latency because the connection is already warmed up and ready to run queries.&lt;/p&gt;
&lt;h4 id=&quot;implementation-in-node-js-amp-mysql&quot;&gt;Implementation in Node.js &amp;amp; MySQL&lt;/h4&gt;
&lt;p&gt;Here is how simple it is to set up a connection pool in an Express.js backend using the popular &lt;code&gt;mysql2&lt;/code&gt; driver:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require('express');
const mysql = require('mysql2/promise');

const app = express();

// Create a connection pool instead of a single connection
const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  database: 'store_db',
  waitForConnections: true,
  connectionLimit: 10, // Max 10 active connections kept in the pool
  queueLimit: 0
});

app.get('/products', async (req, res) =&amp;gt; {
  try {
    // The pool automatically rents out a connection, runs the query,
    // and returns the connection back to the pool when finished.
    const [rows] = await pool.query('SELECT * FROM products LIMIT 10');
    res.json(rows);
  } catch (error) {
    console.error('Database query error:', error);
    res.status(500).send('Database error');
  }
});

app.listen(3000, () =&amp;gt; {
  console.log('Server running on port 3000');
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-takeaway-3&quot;&gt;The Takeaway&lt;/h4&gt;
&lt;p&gt;Optimizing your backend performance isn't always about writing faster algorithms; often, it's about managing your external resources wisely. By treating database connections as a reusable, limited public utility rather than disposable single-use items, you build highly resilient APIs capable of weathering massive traffic spikes without sweating.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/how-connection-pooling-can-prevent-your.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28133/stop-killing-your-database-the-power-of-connection-pooling</guid>
<pubDate>Tue, 22 Sep 2026 08:51:06 +0000</pubDate>
</item>
<item>
<title>Why Your Node.js App Crashes Under Traffic: The Magic of Connection Pooling</title>
<link>https://coderlegion.com/27815/why-your-node-js-app-crashes-under-traffic-the-magic-of-connection-pooling</link>
<description>&lt;p&gt;Imagine launching your shiny new web application. During development, everything runs smoothly. But the moment you launch on social media and a few hundred users hit your API at the exact same time, your server grinds to a halt and starts throwing nasty database connection timeout errors. This common disaster is almost always caused by a lack of database connection pooling.&lt;/p&gt;
&lt;h4 id=&quot;what-is-connection-pooling-&quot;&gt;What is Connection Pooling?&lt;/h4&gt;
&lt;p&gt;Database connection pooling is a performance optimization technique where a set of active database connections is kept open and shared among multiple requests. Instead of creating and destroying a new database connection every single time an application needs to run a query, the application borrows an existing connection from a pre-allocated pool, executes the query, and immediately returns the connection back to the pool. This prevents the costly overhead of starting new communication channels from scratch.&lt;/p&gt;
&lt;h4 id=&quot;the-rental-car-analogy&quot;&gt;The Rental Car Analogy&lt;/h4&gt;
&lt;p&gt;To understand this concept, think of a rental car agency at an airport. Imagine if every time a traveler landed at the airport, the rental agency had to buy a brand-new car from the factory, register it with the DMV, customize it for the driver, and then completely crush and destroy the car at the end of the traveler's weekend trip. That would be an absurdly slow and expensive process.&lt;/p&gt;
&lt;p&gt;Instead, the agency maintains a &quot;pool&quot; of 50 pre-registered, fully fueled, ready-to-drive cars parked in a lot. When a traveler arrives, they borrow a car from the lot, use it for their trip, and then return it to the lot. The car is washed and immediately made available for the next traveler. This is connection pooling: instead of building and destroying connections on the fly, you manage a recycled fleet of them.&lt;/p&gt;
&lt;h4 id=&quot;why-it-matters-daily-in-the-tech-industry-2&quot;&gt;Why It Matters Daily in the Tech Industry&lt;/h4&gt;
&lt;p&gt;Opening a database connection is an incredibly resource-intensive operation. To establish a new connection, your backend application and database server must complete a network handshake, negotiate security protocols (like TLS/SSL), authenticate user credentials, and allocate dedicated RAM and CPU threads to handle that specific session.&lt;/p&gt;
&lt;p&gt;If you build an application where every user request opens a brand-new connection, a sudden spike in web traffic will force your Node.js server to attempt to open hundreds of database connections simultaneously. Your database server (like MySQL) has a built-in safety limit of maximum allowable concurrent connections. Once that limit is hit, the database will aggressively reject all incoming connection requests, throwing &quot;Too many connections&quot; errors, causing API endpoints to return 500 errors, and crashing your platform. Connection pooling solves this by capping the total number of open database connections and keeping extra queries safely queued up in memory until a connection becomes available.&lt;/p&gt;
&lt;h4 id=&quot;connection-pooling-in-action-node-js-amp-mysql-&quot;&gt;Connection Pooling in Action (Node.js &amp;amp; MySQL)&lt;/h4&gt;
&lt;p&gt;Here is how you implement connection pooling using Express and the standard &lt;code&gt;mysql2/promise&lt;/code&gt; library in a Node.js environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require('express');
const mysql = require('mysql2/promise');
const app = express();

// 1. Create a global connection pool with configuration limits
const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'secure_password',
  database: 'ecommerce_db',
  waitForConnections: true,    // Queue queries if no connections are free
  connectionLimit: 10,         // Maximum number of open connections
  queueLimit: 0                // No limit on the number of queued queries
});

app.get('/api/products', async (req, res) =&amp;gt; {
  try {
    // 2. pool.query automatically borrows an idle connection, 
    // executes the query, and returns it to the pool.
    const [products] = await pool.query('SELECT id, name, price FROM products LIMIT 20');
    res.json(products);
  } catch (error) {
    console.error('Database failure:', error);
    res.status(500).json({ error: 'Internal Server Error' });
  }
});

app.listen(3000, () =&amp;gt; {
  console.log('Server is running on port 3000');
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-takeaway-4&quot;&gt;The Takeaway&lt;/h4&gt;
&lt;p&gt;Software scalability is not just about writing clean logic or optimizing loops; it is about protecting and sharing finite hardware resources under heavy load. By implementing connection pooling, you protect your database from crashing, reduce API response latency, and build backend systems capable of handling traffic spikes gracefully without costing a fortune in extra server infrastructure.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/stop-rebuilding-bridge-how-connection.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27815/why-your-node-js-app-crashes-under-traffic-the-magic-of-connection-pooling</guid>
<pubDate>Sun, 20 Sep 2026 08:56:50 +0000</pubDate>
</item>
<item>
<title>Supercharge Your Node.js App: Why You Need Database Connection Pooling</title>
<link>https://coderlegion.com/27716/supercharge-your-node-js-app-why-you-need-database-connection-pooling</link>
<description>&lt;h2 id=&quot;what-is-connection-pooling--1&quot;&gt;What is Connection Pooling?&lt;/h2&gt;
&lt;p&gt;Database connection pooling is a technique where a cache of database connections is kept open and shared among multiple queries, rather than opening and closing a brand-new connection for every single query. When your application needs to talk to the database, it borrows an existing connection from the pool, runs the query, and immediately returns it. This avoids the heavy performance penalty of creating and destroying connections constantly.&lt;/p&gt;
&lt;h3 id=&quot;the-pizza-scooter-analogy&quot;&gt;The Pizza Scooter Analogy&lt;/h3&gt;
&lt;p&gt;Imagine a busy local pizza shop. Every time a customer orders a pizza, if the shop had to buy a brand-new delivery scooter from the dealership, register it, insure it, deliver the pizza, and then scrap or sell the scooter immediately afterward, the business would go bankrupt in an hour.&lt;/p&gt;
&lt;p&gt;Instead, the shop maintains a stable fleet of five delivery scooters parked in the back. When a delivery is ready, a driver grabs an available scooter from the fleet, completes the delivery, and parks it back in the lot for the next driver to use. Connection pooling is that stable fleet of scooters.&lt;/p&gt;
&lt;h3 id=&quot;why-connection-pooling-matters-daily-1&quot;&gt;Why Connection Pooling Matters Daily&lt;/h3&gt;
&lt;p&gt;In backend development, establishing a connection to a database like MySQL is an incredibly heavy operation. It requires several steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A TCP network handshake.&lt;/li&gt;
&lt;li&gt;SSL/TLS security negotiation.&lt;/li&gt;
&lt;li&gt;User authentication and privilege verification.&lt;/li&gt;
&lt;li&gt;Memory allocation on the database server.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If your Express.js server handles 1,000 requests per second and you open a new connection for each one, your database server's CPU will spike to 100% just managing handshakes. Eventually, the database will run out of resources and throw a dreaded &quot;Too many connections&quot; error, crashing your app. By using a connection pool, you bypass this startup overhead. Your queries run almost instantly because the connection is already active and authenticated.&lt;/p&gt;
&lt;h3 id=&quot;simple-code-example&quot;&gt;Simple Code Example&lt;/h3&gt;
&lt;p&gt;Let's see the difference in a MERN stack application using Node.js, Express, and the &lt;code&gt;mysql2&lt;/code&gt; package.&lt;/p&gt;
&lt;h4 id=&quot;the-bad-way-new-connection-per-request-&quot;&gt;The Bad Way (New Connection per Request)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require('express');
const mysql = require('mysql2/promise');
const app = express();

const dbConfig = { host: 'localhost', user: 'root', database: 'store' };

app.get('/products', async (req, res) =&amp;gt; {
  try {
    // Slow and resource-intensive: creating a connection on every request
    const connection = await mysql.createConnection(dbConfig);
    const [rows] = await connection.execute('SELECT * FROM products LIMIT 10');

    // Closing it manually
    await connection.end();
    res.json(rows);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.listen(3000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-good-way-using-a-connection-pool-&quot;&gt;The Good Way (Using a Connection Pool)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require('express');
const mysql = require('mysql2/promise');
const app = express();

const dbConfig = { host: 'localhost', user: 'root', database: 'store' };

// Create a pool of up to 10 reusable connections
const pool = mysql.createPool({
  ...dbConfig,
  connectionLimit: 10,
  waitForConnections: true,
  queueLimit: 0
});

app.get('/products', async (req, res) =&amp;gt; {
  try {
    // Automatically borrows an active connection and returns it when done
    const [rows] = await pool.execute('SELECT * FROM products LIMIT 10');
    res.json(rows);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.listen(3000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;key-takeaway-8&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;Database connection pooling is not just a performance optimization; it is a fundamental survival mechanism for production databases. It turns chaotic database traffic into a predictable, queued stream, ensuring your server remains responsive and your database stays healthy under heavy load.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/how-to-prevent-database-crashes-under.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27716/supercharge-your-node-js-app-why-you-need-database-connection-pooling</guid>
<pubDate>Sat, 19 Sep 2026 08:17:43 +0000</pubDate>
</item>
<item>
<title>We Turned a Real Knowledge Transfer Into Verifiable Evidence on Base Sepolia</title>
<link>https://coderlegion.com/27677/we-turned-a-real-knowledge-transfer-into-verifiable-evidence-on-base-sepolia</link>
<description>&lt;p&gt;We Turned a Real Knowledge Transfer Into Verifiable Evidence on Base Sepolia&lt;/p&gt;
&lt;p&gt;Today we completed an experiment inside MyZubster that connects real human collaboration, GitHub history, cryptographic commitments, and blockchain verification.&lt;/p&gt;
&lt;p&gt;The goal was not to “put knowledge on blockchain.”&lt;/p&gt;
&lt;p&gt;The goal was more precise:&lt;/p&gt;
&lt;p&gt;Can we take a real technical collaboration, freeze the resulting public work into a canonical record, hash it deterministically, anchor that commitment on-chain, and verify it independently later?&lt;/p&gt;
&lt;p&gt;We used the ongoing Nicola / N4K48 MyZubster pilot as the real case.&lt;/p&gt;
&lt;p&gt;The result is now public and reproducible.&lt;/p&gt;
&lt;p&gt;The real source of evidence: GitHub&lt;/p&gt;
&lt;p&gt;Nicola’s public MVP is here:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nicolaususnicola-lgtm/myzubster-mvp&quot;&gt;https://github.com/nicolaususnicola-lgtm/myzubster-mvp&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Live pilot:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://myzubster-mvp.onrender.com/&quot;&gt;https://myzubster-mvp.onrender.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;During the September 18 collaboration, the repository advanced across:&lt;/p&gt;
&lt;p&gt;economic provenance;&lt;br&gt;
persistent ledger storage;&lt;br&gt;
revenue and asset APIs;&lt;br&gt;
derived balances;&lt;br&gt;
source-specific revenue breakdown;&lt;br&gt;
UTC timestamps;&lt;br&gt;
asset/revenue history;&lt;br&gt;
MYZ ledger UI;&lt;br&gt;
public documentation;&lt;br&gt;
revenue simulation.&lt;/p&gt;
&lt;p&gt;Instead of describing that work loosely, we identified an exact Git history window:&lt;/p&gt;
&lt;p&gt;Base commit:&lt;br&gt;
8da7435ddc1b9ae8b2f5c2eea5debd2df4bc5f47&lt;/p&gt;
&lt;p&gt;Head commit:&lt;br&gt;
b9ac4e8801d65b84ef882baf78fa09ce5b6b5059&lt;/p&gt;
&lt;p&gt;That snapshot contains 30 commits.&lt;/p&gt;
&lt;p&gt;This gives us a concrete technical object to reference.&lt;/p&gt;
&lt;p&gt;We introduced a knowledge-transfer manifest&lt;/p&gt;
&lt;p&gt;We created a new schema:&lt;/p&gt;
&lt;p&gt;myzubster.knowledge-transfer.v1&lt;/p&gt;
&lt;p&gt;with transfer ID:&lt;/p&gt;
&lt;p&gt;KNOWLEDGE-N4K48-2026-09-18-001&lt;/p&gt;
&lt;p&gt;The manifest records:&lt;/p&gt;
&lt;p&gt;source identity;&lt;br&gt;
recipient identity;&lt;br&gt;
repository;&lt;br&gt;
base commit;&lt;br&gt;
head commit;&lt;br&gt;
changed files;&lt;br&gt;
knowledge scope;&lt;br&gt;
blockchain target;&lt;br&gt;
evidence boundaries.&lt;/p&gt;
&lt;p&gt;The important design choice is that we separate:&lt;/p&gt;
&lt;p&gt;source assertion&lt;br&gt;
from&lt;br&gt;
recipient attestation&lt;/p&gt;
&lt;p&gt;At anchor time, Nicola’s recipient attestation was still:&lt;/p&gt;
&lt;p&gt;PENDING&lt;/p&gt;
&lt;p&gt;That is intentional.&lt;/p&gt;
&lt;p&gt;A blockchain transaction should never be interpreted as automatic consent from another human.&lt;/p&gt;
&lt;p&gt;Deterministic hashing&lt;/p&gt;
&lt;p&gt;The manifest is canonicalized by:&lt;/p&gt;
&lt;p&gt;recursively sorting object keys;&lt;br&gt;
preserving array order;&lt;br&gt;
serializing compact JSON;&lt;br&gt;
UTF-8 encoding;&lt;br&gt;
calculating SHA-256.&lt;/p&gt;
&lt;p&gt;The resulting hash is:&lt;/p&gt;
&lt;p&gt;39ab3a177734b5e3e254657cfe6015100644bcda2fb008cf2561d000669b9e14&lt;/p&gt;
&lt;p&gt;The blockchain payload is:&lt;/p&gt;
&lt;p&gt;MZ-KNOWLEDGE-V1:39ab3a177734b5e3e254657cfe6015100644bcda2fb008cf2561d000669b9e14&lt;/p&gt;
&lt;p&gt;This is the core architecture:&lt;/p&gt;
&lt;p&gt;large public evidence set&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;canonical manifest&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SHA-256&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;small on-chain commitment&lt;/p&gt;
&lt;p&gt;We do not need to put source code, documentation, personal data, or full knowledge content directly on-chain.&lt;/p&gt;
&lt;p&gt;Anchoring on Base Sepolia&lt;/p&gt;
&lt;p&gt;For the pilot we used:&lt;/p&gt;
&lt;p&gt;Network: Base Sepolia&lt;br&gt;
Chain ID: 84532&lt;br&gt;
Value: 0 ETH&lt;/p&gt;
&lt;p&gt;The transaction was confirmed:&lt;/p&gt;
&lt;p&gt;0xff3c108275625673ad22a886da2df7120ae81b8f0106ec833613513b03c7bc31&lt;/p&gt;
&lt;p&gt;Block:&lt;/p&gt;
&lt;p&gt;47000958&lt;/p&gt;
&lt;p&gt;Confirmed at:&lt;/p&gt;
&lt;p&gt;2026-09-18T22:23:24.000Z&lt;/p&gt;
&lt;p&gt;Explorer:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://sepolia.basescan.org/tx/0xff3c108275625673ad22a886da2df7120ae81b8f0106ec833613513b03c7bc31&quot;&gt;https://sepolia.basescan.org/tx/0xff3c108275625673ad22a886da2df7120ae81b8f0106ec833613513b03c7bc31&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A real integration bug: MetaMask rejected self-targeted calldata&lt;/p&gt;
&lt;p&gt;Our first implementation tried to submit:&lt;/p&gt;
&lt;p&gt;from: wallet&lt;br&gt;
to: same wallet&lt;br&gt;
value: 0&lt;br&gt;
data: commitment&lt;/p&gt;
&lt;p&gt;MetaMask rejected it with:&lt;/p&gt;
&lt;p&gt;External transactions to internal accounts cannot include data&lt;/p&gt;
&lt;p&gt;So we changed the design to use a fixed zero-value evidence sink:&lt;/p&gt;
&lt;p&gt;0x000000000000000000000000000000000000dEaD&lt;/p&gt;
&lt;p&gt;No ETH is transferred.&lt;/p&gt;
&lt;p&gt;Only the commitment is carried in calldata.&lt;/p&gt;
&lt;p&gt;Then smart accounts changed the transaction shape&lt;/p&gt;
&lt;p&gt;After the transaction confirmed, the first version of our verifier reported:&lt;/p&gt;
&lt;p&gt;MISMATCH&lt;/p&gt;
&lt;p&gt;At first glance that looked wrong.&lt;/p&gt;
&lt;p&gt;The receipt was successful.&lt;/p&gt;
&lt;p&gt;The chain was correct.&lt;/p&gt;
&lt;p&gt;The value was zero.&lt;/p&gt;
&lt;p&gt;But the transaction destination and top-level calldata were not what our verifier expected.&lt;/p&gt;
&lt;p&gt;The reason: MetaMask submitted the request through a delegated smart-account transaction.&lt;/p&gt;
&lt;p&gt;The top-level transaction was type 4 and the anchor execution was wrapped inside:&lt;/p&gt;
&lt;p&gt;redeemDelegations(...)&lt;/p&gt;
&lt;p&gt;This forced us to improve the verifier.&lt;/p&gt;
&lt;p&gt;Instead of only checking:&lt;/p&gt;
&lt;p&gt;tx.to === evidenceSink&lt;br&gt;
tx.data === expectedPayload&lt;/p&gt;
&lt;p&gt;we added delegated execution decoding.&lt;/p&gt;
&lt;p&gt;The verifier now inspects the nested execution and validates:&lt;/p&gt;
&lt;p&gt;target&lt;br&gt;
value&lt;br&gt;
calldata&lt;br&gt;
execution mode&lt;/p&gt;
&lt;p&gt;Inside the delegated call we found:&lt;/p&gt;
&lt;p&gt;Target:&lt;br&gt;
0x000000000000000000000000000000000000dEaD&lt;/p&gt;
&lt;p&gt;Value:&lt;br&gt;
0&lt;/p&gt;
&lt;p&gt;Payload:&lt;br&gt;
MZ-KNOWLEDGE-V1:39ab3a177734b5e3e254657cfe6015100644bcda2fb008cf2561d000669b9e14&lt;/p&gt;
&lt;p&gt;The result became:&lt;/p&gt;
&lt;p&gt;MATCH&lt;/p&gt;
&lt;p&gt;This was one of the most useful parts of the pilot.&lt;/p&gt;
&lt;p&gt;A synthetic demo probably would not have exposed this distinction between a normal EOA transaction and a delegated smart-account execution.&lt;/p&gt;
&lt;p&gt;Public verifier&lt;/p&gt;
&lt;p&gt;We deployed a public verifier endpoint:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/api/knowledge-anchor/n4k48/0xff3c108275625673ad22a886da2df7120ae81b8f0106ec833613513b03c7bc31&quot;&gt;https://www.myzubster.com/api/knowledge-anchor/n4k48/0xff3c108275625673ad22a886da2df7120ae81b8f0106ec833613513b03c7bc31&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It checks:&lt;/p&gt;
&lt;p&gt;chainId                    ✅&lt;br&gt;
receipt success            ✅&lt;br&gt;
top-level value = 0        ✅&lt;br&gt;
delegated call recognized  ✅&lt;br&gt;
nested target match        ✅&lt;br&gt;
nested value = 0           ✅&lt;br&gt;
exact calldata match       ✅&lt;br&gt;
execution mode match       ✅&lt;/p&gt;
&lt;p&gt;Current status:&lt;/p&gt;
&lt;p&gt;{&lt;br&gt;
  &quot;success&quot;: true,&lt;br&gt;
  &quot;status&quot;: &quot;MATCH&quot;&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;Human-readable page:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/knowledge-anchor-n4k48&quot;&gt;https://www.myzubster.com/knowledge-anchor-n4k48&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What exactly is proven?&lt;/p&gt;
&lt;p&gt;This is where engineering language matters.&lt;/p&gt;
&lt;p&gt;The blockchain anchor proves evidence about:&lt;/p&gt;
&lt;p&gt;existence&lt;br&gt;
integrity&lt;br&gt;
timestamp&lt;/p&gt;
&lt;p&gt;for the exact canonical manifest.&lt;/p&gt;
&lt;p&gt;It does not prove:&lt;/p&gt;
&lt;p&gt;authorship of every idea&lt;br&gt;
learning&lt;br&gt;
understanding&lt;br&gt;
scientific validity&lt;br&gt;
legal ownership&lt;br&gt;
commercial value&lt;br&gt;
truth of every human claim&lt;/p&gt;
&lt;p&gt;This distinction is fundamental.&lt;/p&gt;
&lt;p&gt;Blockchain is useful here as a tamper-evident timestamp and integrity layer, not as a replacement for human verification, peer review, contracts, or scientific validation.&lt;/p&gt;
&lt;p&gt;The final human layer&lt;/p&gt;
&lt;p&gt;The next step belongs to Nicola.&lt;/p&gt;
&lt;p&gt;He now needs to add a separate attestation from his own GitHub account confirming that the record actually represents knowledge, guidance, and collaboration he received and used.&lt;/p&gt;
&lt;p&gt;We intentionally do not modify the original anchored manifest, because that would change its hash.&lt;/p&gt;
&lt;p&gt;Instead, Nicola can create something like:&lt;/p&gt;
&lt;p&gt;knowledge/N4K48-ATTESTATION-KNOWLEDGE-2026-09-18.md&lt;/p&gt;
&lt;p&gt;and commit a statement acknowledging:&lt;/p&gt;
&lt;p&gt;KNOWLEDGE-N4K48-2026-09-18-001&lt;/p&gt;
&lt;p&gt;and its anchored hash.&lt;/p&gt;
&lt;p&gt;This creates a stronger chain:&lt;/p&gt;
&lt;p&gt;source assertion&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;public development evidence&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;canonical hash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;blockchain anchor&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;independent verifier&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;recipient acknowledgement&lt;br&gt;
Why we think this architecture matters&lt;/p&gt;
&lt;p&gt;A lot of blockchain systems start from the wrong question:&lt;/p&gt;
&lt;p&gt;“What can we put on-chain?”&lt;/p&gt;
&lt;p&gt;A better question is:&lt;/p&gt;
&lt;p&gt;“What evidence should stay off-chain, and what minimal commitment should be anchored on-chain?”&lt;/p&gt;
&lt;p&gt;For MyZubster, this leads to a cleaner architecture:&lt;/p&gt;
&lt;p&gt;GitHub → evidence history&lt;br&gt;
SHA-256 → integrity&lt;br&gt;
Base Sepolia → timestamp anchor&lt;br&gt;
Human attestation → consent / acknowledgement&lt;br&gt;
Research review → validation&lt;br&gt;
MYZ ledger → internal accounting&lt;/p&gt;
&lt;p&gt;Each layer does one job.&lt;/p&gt;
&lt;p&gt;That separation avoids a lot of semantic confusion.&lt;/p&gt;
&lt;p&gt;Knowledge is not the token&lt;/p&gt;
&lt;p&gt;Another design principle we are keeping explicit:&lt;/p&gt;
&lt;p&gt;knowledge ≠ blockchain asset&lt;br&gt;
MYZ ≠ crypto token&lt;br&gt;
GitHub commit ≠ legal ownership&lt;br&gt;
blockchain timestamp ≠ scientific proof&lt;/p&gt;
&lt;p&gt;MYZ remains an internal MyZubster accounting unit.&lt;/p&gt;
&lt;p&gt;The knowledge anchor is a separate evidence mechanism.&lt;/p&gt;
&lt;p&gt;The two may interact later through explicit business rules, but they should never be silently merged into one concept.&lt;/p&gt;
&lt;p&gt;Public references&lt;/p&gt;
&lt;p&gt;Nicola / N4K48 repository:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nicolaususnicola-lgtm/myzubster-mvp&quot;&gt;https://github.com/nicolaususnicola-lgtm/myzubster-mvp&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Live pilot:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://myzubster-mvp.onrender.com/&quot;&gt;https://myzubster-mvp.onrender.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;MyZubster verification page:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/knowledge-anchor-n4k48&quot;&gt;https://www.myzubster.com/knowledge-anchor-n4k48&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Verifier endpoint:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/api/knowledge-anchor/n4k48/0xff3c108275625673ad22a886da2df7120ae81b8f0106ec833613513b03c7bc31&quot;&gt;https://www.myzubster.com/api/knowledge-anchor/n4k48/0xff3c108275625673ad22a886da2df7120ae81b8f0106ec833613513b03c7bc31&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;BaseScan:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://sepolia.basescan.org/tx/0xff3c108275625673ad22a886da2df7120ae81b8f0106ec833613513b03c7bc31&quot;&gt;https://sepolia.basescan.org/tx/0xff3c108275625673ad22a886da2df7120ae81b8f0106ec833613513b03c7bc31&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;MyZubster Core:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What we learned&lt;/p&gt;
&lt;p&gt;The biggest lesson was not about hashes or blockchains.&lt;/p&gt;
&lt;p&gt;It was about evidence boundaries.&lt;/p&gt;
&lt;p&gt;Real systems need to distinguish:&lt;/p&gt;
&lt;p&gt;recorded&lt;br&gt;
from&lt;br&gt;
verified&lt;/p&gt;
&lt;p&gt;verified&lt;br&gt;
from&lt;br&gt;
validated&lt;/p&gt;
&lt;p&gt;validated&lt;br&gt;
from&lt;br&gt;
owned&lt;/p&gt;
&lt;p&gt;owned&lt;br&gt;
from&lt;br&gt;
understood&lt;/p&gt;
&lt;p&gt;Once those boundaries are explicit, blockchain becomes much more useful.&lt;/p&gt;
&lt;p&gt;Not as a magical source of truth.&lt;/p&gt;
&lt;p&gt;But as one precise component in a larger evidence architecture.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27677/we-turned-a-real-knowledge-transfer-into-verifiable-evidence-on-base-sepolia</guid>
<pubDate>Sat, 19 Sep 2026 02:18:27 +0000</pubDate>
</item>
<item>
<title>Building Zorgax in Production: Hybrid AI Routing, Grounding, Fallbacks, and UX</title>
<link>https://coderlegion.com/27648/building-zorgax-in-production-hybrid-ai-routing-grounding-fallbacks-and-ux</link>
<description>&lt;p&gt;Building Zorgax in Production: Hybrid AI Routing, Grounding, Fallbacks, and UX&lt;/p&gt;
&lt;p&gt;Building an AI chatbot is easy.&lt;/p&gt;
&lt;p&gt;Building an AI feature that survives production is a very different problem.&lt;/p&gt;
&lt;p&gt;Over the last development cycle, we evolved Zorgax, the AI copilot inside MyZubster, from a simple conversational interface into a hybrid AI layer with model routing, cost controls, fallback logic, product grounding, runtime observability, and tighter integration with the rest of the platform.&lt;/p&gt;
&lt;p&gt;The most interesting lesson was that the LLM API call ended up being one of the smallest parts of the system.&lt;/p&gt;
&lt;p&gt;The goal&lt;/p&gt;
&lt;p&gt;Zorgax is designed to act as the conversational entry point to MyZubster.&lt;/p&gt;
&lt;p&gt;A user should be able to ask a normal question, explore the Marketplace, understand a LIFE Pilot, start a Seller journey, navigate the Metaverse, or receive guidance about the wider ecosystem.&lt;/p&gt;
&lt;p&gt;But we did not want every request to go through the same AI model.&lt;/p&gt;
&lt;p&gt;A simple message such as:&lt;/p&gt;
&lt;p&gt;Ciao Zorgax&lt;/p&gt;
&lt;p&gt;does not need the same infrastructure as:&lt;/p&gt;
&lt;p&gt;Analyze the current LIFE workflow, evaluate KPI/MRV evidence,&lt;br&gt;
and explain the deployment and repository architecture.&lt;/p&gt;
&lt;p&gt;So we introduced a routing layer.&lt;/p&gt;
&lt;p&gt;Hybrid model routing&lt;/p&gt;
&lt;p&gt;The current architecture separates requests into broad standard and complex categories.&lt;/p&gt;
&lt;p&gt;Research mode, long prompts, and signals related to coding, deployment, research, LIFE, KPI/MRV, automation, GitHub, or Vercel can move a request into the complex route.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;p&gt;const tier =&lt;br&gt;
  useResearch ||&lt;br&gt;
  complexSignalCount &amp;gt;= 2 ||&lt;br&gt;
  text.length &amp;gt;= 2500&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;? &quot;complex&quot;
: &quot;standard&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The architecture then becomes:&lt;/p&gt;
&lt;p&gt;User&lt;br&gt;
  ↓&lt;br&gt;
Zorgax API&lt;br&gt;
  ↓&lt;br&gt;
Task classification&lt;br&gt;
  ↓&lt;br&gt;
AI Router&lt;br&gt;
  ├── Standard → Ollama / general AI gateway&lt;br&gt;
  │&lt;br&gt;
  └── Complex / Research → OpenAI&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                        ↓
                     fallback
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives us a hybrid system instead of making the whole product dependent on one remote provider.&lt;/p&gt;
&lt;p&gt;Making the model visible&lt;/p&gt;
&lt;p&gt;One of the first improvements was exposing the actual route used by the backend.&lt;/p&gt;
&lt;p&gt;The API returns metadata like:&lt;/p&gt;
&lt;p&gt;{&lt;br&gt;
  &quot;ai_provider&quot;: &quot;openai&quot;,&lt;br&gt;
  &quot;ai_model&quot;: &quot;gpt-5.6-sol&quot;,&lt;br&gt;
  &quot;ai_fallback_reason&quot;: null&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;The interface displays it directly below the response:&lt;/p&gt;
&lt;p&gt;AI: OpenAI · gpt-5.6-sol&lt;/p&gt;
&lt;p&gt;If something goes wrong:&lt;/p&gt;
&lt;p&gt;AI: Ollama · qwen2.5:3b · fallback: openai_rate_limit&lt;/p&gt;
&lt;p&gt;This looks like a small UI feature, but operationally it changes a lot.&lt;/p&gt;
&lt;p&gt;We no longer have to ask:&lt;/p&gt;
&lt;p&gt;Did OpenAI actually generate this answer?&lt;/p&gt;
&lt;p&gt;The application tells us.&lt;/p&gt;
&lt;p&gt;The first production surprise&lt;/p&gt;
&lt;p&gt;Once OpenAI routing was enabled, the first production request did not succeed.&lt;/p&gt;
&lt;p&gt;Zorgax returned a valid answer, but the logs showed:&lt;/p&gt;
&lt;p&gt;aiProvider: ollama&lt;br&gt;
aiModel: qwen2.5:3b&lt;br&gt;
aiFallbackReason: openai_error&lt;/p&gt;
&lt;p&gt;The OpenAI call had returned HTTP 429.&lt;/p&gt;
&lt;p&gt;At first, that was not enough information.&lt;/p&gt;
&lt;p&gt;A 429 can represent very different conditions: a temporary rate limit, exhausted API credits, a project spending limit, or another usage restriction.&lt;/p&gt;
&lt;p&gt;So we improved the error handling rather than simply retrying blindly.&lt;/p&gt;
&lt;p&gt;Precise OpenAI diagnostics&lt;/p&gt;
&lt;p&gt;Zorgax now extracts safe diagnostic information from failed requests:&lt;/p&gt;
&lt;p&gt;HTTP status&lt;br&gt;
error code&lt;br&gt;
error type&lt;br&gt;
request id&lt;/p&gt;
&lt;p&gt;without exposing credentials.&lt;/p&gt;
&lt;p&gt;Fallback states can now be much more precise:&lt;/p&gt;
&lt;p&gt;openai_credit_balance_exhausted&lt;br&gt;
openai_rate_limit&lt;br&gt;
openai_rate_limit_exceeded&lt;br&gt;
openai_project_spend_limit_exceeded&lt;/p&gt;
&lt;p&gt;That immediately paid off.&lt;/p&gt;
&lt;p&gt;Our next production test returned:&lt;/p&gt;
&lt;p&gt;openai_credit_balance_exhausted&lt;/p&gt;
&lt;p&gt;The application was working correctly. The account simply had no API credit.&lt;/p&gt;
&lt;p&gt;After adding credit, the next request succeeded with:&lt;/p&gt;
&lt;p&gt;AI: OpenAI · gpt-5.6-sol&lt;/p&gt;
&lt;p&gt;and production logs confirmed:&lt;/p&gt;
&lt;p&gt;aiProvider: openai&lt;br&gt;
aiModel: gpt-5.6-sol&lt;br&gt;
aiFallbackReason: null&lt;/p&gt;
&lt;p&gt;That was the first full end-to-end verification.&lt;/p&gt;
&lt;p&gt;Retry without retrying everything forever&lt;/p&gt;
&lt;p&gt;Transient failures still need resilience.&lt;/p&gt;
&lt;p&gt;For rate-limit-style failures, Zorgax now has controlled retry behavior.&lt;/p&gt;
&lt;p&gt;The primary route can retry once, respecting Retry-After when the wait is short enough. A secondary OpenAI model can then be attempted before the system falls back to the local/general route.&lt;/p&gt;
&lt;p&gt;The flow is roughly:&lt;/p&gt;
&lt;p&gt;gpt-5.6-sol&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;controlled retry&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;secondary OpenAI model&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ollama fallback&lt;/p&gt;
&lt;p&gt;Quota errors are different.&lt;/p&gt;
&lt;p&gt;If the API explicitly says credits are exhausted, retrying repeatedly would only waste time, so the fallback happens immediately.&lt;/p&gt;
&lt;p&gt;That distinction is important: resilience does not mean retrying every failure.&lt;/p&gt;
&lt;p&gt;Cost controls belong in the architecture&lt;/p&gt;
&lt;p&gt;Remote AI usage also needs application-level cost controls.&lt;/p&gt;
&lt;p&gt;Before sending a complex request, Zorgax checks its monthly OpenAI budget and reserves enough capacity for the request.&lt;/p&gt;
&lt;p&gt;The flow looks like this:&lt;/p&gt;
&lt;p&gt;Classify request&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check monthly spend&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Estimate request cost&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reserve budget&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Call OpenAI&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Record actual usage&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Settle reservation&lt;/p&gt;
&lt;p&gt;If the reservation cannot be made, the conversation can continue through the fallback path.&lt;/p&gt;
&lt;p&gt;We also record the model that actually completed the request, so usage accounting still makes sense if a secondary OpenAI model is used.&lt;/p&gt;
&lt;p&gt;Grounding became the harder problem&lt;/p&gt;
&lt;p&gt;Once OpenAI was running correctly, another issue appeared.&lt;/p&gt;
&lt;p&gt;The model became too conservative.&lt;/p&gt;
&lt;p&gt;We asked Zorgax which MyZubster features were live, experimental, planned, or unverified.&lt;/p&gt;
&lt;p&gt;The answer classified nearly everything as unverified.&lt;/p&gt;
&lt;p&gt;That was technically cautious, but it was also wrong.&lt;/p&gt;
&lt;p&gt;The reason was architectural: the OpenAI route and the Ollama route were not receiving exactly the same runtime product context.&lt;/p&gt;
&lt;p&gt;The local route had product facts.&lt;/p&gt;
&lt;p&gt;The OpenAI route mostly had the system persona and the user message.&lt;/p&gt;
&lt;p&gt;So instead of weakening our anti-hallucination rules, we fixed the input architecture.&lt;/p&gt;
&lt;p&gt;One canonical runtime context&lt;/p&gt;
&lt;p&gt;Both model paths now receive the same canonical product context.&lt;/p&gt;
&lt;p&gt;It includes facts such as:&lt;/p&gt;
&lt;p&gt;myzubster.com is live&lt;/p&gt;
&lt;p&gt;/marketplace is an implemented route&lt;/p&gt;
&lt;p&gt;Seller starts from Marketplace&lt;/p&gt;
&lt;p&gt;/social-login is the authentication path&lt;/p&gt;
&lt;p&gt;/metaverse is an implemented route&lt;/p&gt;
&lt;p&gt;/life-pilot is an implemented pilot path&lt;/p&gt;
&lt;p&gt;MYZ is internal reward/accounting&lt;/p&gt;
&lt;p&gt;It also contains an important rule:&lt;/p&gt;
&lt;p&gt;A live route does not prove every possible feature associated with that route.&lt;/p&gt;
&lt;p&gt;That distinction prevents two opposite failures.&lt;/p&gt;
&lt;p&gt;The assistant should not invent features that do not exist, but it should also not deny the existence of routes that are actually implemented.&lt;/p&gt;
&lt;p&gt;Product state as part of the prompt&lt;/p&gt;
&lt;p&gt;Zorgax now reasons with four capability states:&lt;/p&gt;
&lt;p&gt;LIVE / IMPLEMENTED&lt;br&gt;
PILOT / EXPERIMENTAL&lt;br&gt;
PROPOSED / PLANNED&lt;br&gt;
UNKNOWN / UNVERIFIED&lt;/p&gt;
&lt;p&gt;For example, /metaverse being live means the route exists.&lt;/p&gt;
&lt;p&gt;It does not automatically mean Sellers can create virtual stores.&lt;/p&gt;
&lt;p&gt;Likewise, /life-pilot being live does not prove that GPS field reporting, scientific validation, public map publication, or environmental measurements are all implemented.&lt;/p&gt;
&lt;p&gt;After introducing the shared runtime context, the same test produced a much better result.&lt;/p&gt;
&lt;p&gt;Marketplace, Seller flow, authentication, Metaverse, LIFE Pilot, open-source contribution, and MYZ were described according to their verified status, while advanced dashboards, IoT integrations, IPFS/IPNS workflows, and external settlement remained unverified.&lt;/p&gt;
&lt;p&gt;And this time the answer was generated by:&lt;/p&gt;
&lt;p&gt;AI: OpenAI · gpt-5.6-sol&lt;br&gt;
The chat itself needed work too&lt;/p&gt;
&lt;p&gt;Backend architecture was only half the problem.&lt;/p&gt;
&lt;p&gt;The original Zorgax interface had a very small conversation area.&lt;/p&gt;
&lt;p&gt;Long AI answers appeared with the scroll position close to the end of the response, forcing the user to scroll upward to begin reading.&lt;/p&gt;
&lt;p&gt;We changed the page so that the conversation became the main content area.&lt;/p&gt;
&lt;p&gt;New assistant replies are now positioned at the beginning of the new response rather than the end.&lt;/p&gt;
&lt;p&gt;The scroll logic is roughly:&lt;/p&gt;
&lt;p&gt;function scrollMessageToTop(element) {&lt;br&gt;
  const delta =&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;element.getBoundingClientRect().top -
messages.getBoundingClientRect().top;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  messages.scrollTo({&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;top: messages.scrollTop + delta - 12,
behavior: &quot;smooth&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  });&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;We also prevent textarea focus from modifying the viewport:&lt;/p&gt;
&lt;p&gt;input.focus({ preventScroll: true });&lt;/p&gt;
&lt;p&gt;This made long-form AI responses dramatically easier to read.&lt;/p&gt;
&lt;p&gt;Zorgax also needed an exit&lt;/p&gt;
&lt;p&gt;There was one more product problem.&lt;/p&gt;
&lt;p&gt;Once a user entered Zorgax, the assistant page felt too isolated from the rest of MyZubster.&lt;/p&gt;
&lt;p&gt;A product copilot should help users move through the product, not trap them inside the AI interface.&lt;/p&gt;
&lt;p&gt;So we added permanent navigation in the Zorgax header:&lt;/p&gt;
&lt;p&gt;🏠 Home MyZubster&lt;br&gt;
🛒 Marketplace&lt;/p&gt;
&lt;p&gt;The page also contains guided actions for Seller, Marketplace, Metaverse, LIFE Pilot, University/research, Community, and other supported journeys.&lt;/p&gt;
&lt;p&gt;This turned Zorgax from a standalone AI page into a real navigation layer for the wider product.&lt;/p&gt;
&lt;p&gt;Observability closed the loop&lt;/p&gt;
&lt;p&gt;Vercel runtime logging now gives us a compact record of every AI routing decision.&lt;/p&gt;
&lt;p&gt;A successful production event can look like:&lt;/p&gt;
&lt;p&gt;event: zorgax_message_sent&lt;br&gt;
plan: pro&lt;br&gt;
webResearch: true&lt;br&gt;
sourceCount: 0&lt;br&gt;
aiProvider: openai&lt;br&gt;
aiModel: gpt-5.6-sol&lt;br&gt;
aiFallbackReason: null&lt;/p&gt;
&lt;p&gt;During debugging, this was one of the most valuable pieces of the system.&lt;/p&gt;
&lt;p&gt;Without it, an answer that looked fine could still have silently come from the fallback model.&lt;/p&gt;
&lt;p&gt;With it, we can trace the actual production path.&lt;/p&gt;
&lt;p&gt;What the architecture looks like now&lt;/p&gt;
&lt;p&gt;The current system can be summarized like this:&lt;/p&gt;
&lt;p&gt;User&lt;br&gt;
  ↓&lt;br&gt;
Zorgax UI&lt;br&gt;
  ↓&lt;br&gt;
Access policy&lt;br&gt;
  ↓&lt;br&gt;
Optional research&lt;br&gt;
  ↓&lt;br&gt;
Task classification&lt;br&gt;
  ↓&lt;br&gt;
Canonical product context&lt;br&gt;
  ↓&lt;br&gt;
AI Router&lt;br&gt;
  ├── OpenAI&lt;br&gt;
  │    ├── primary model&lt;br&gt;
  │    ├── controlled retry&lt;br&gt;
  │    └── secondary model&lt;br&gt;
  │&lt;br&gt;
  └── Ollama / general AI fallback&lt;br&gt;
  ↓&lt;br&gt;
Grounded answer&lt;br&gt;
  ↓&lt;br&gt;
Provider + model metadata&lt;br&gt;
  ↓&lt;br&gt;
Runtime logs&lt;br&gt;
  ↓&lt;br&gt;
User&lt;/p&gt;
&lt;p&gt;That is much closer to an AI product architecture than a simple chatbot.&lt;/p&gt;
&lt;p&gt;What we learned&lt;/p&gt;
&lt;p&gt;The LLM call was the easy part.&lt;/p&gt;
&lt;p&gt;The actual production system is:&lt;/p&gt;
&lt;p&gt;model&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;routing&lt;/li&gt;
&lt;li&gt;budget controls&lt;/li&gt;
&lt;li&gt;retry policy&lt;/li&gt;
&lt;li&gt;fallbacks&lt;/li&gt;
&lt;li&gt;grounding&lt;/li&gt;
&lt;li&gt;authorization&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;li&gt;UI&lt;/li&gt;
&lt;li&gt;navigation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The more capable an AI model becomes, the more important it is for the application around it to define what is real.&lt;/p&gt;
&lt;p&gt;The application has to know:&lt;/p&gt;
&lt;p&gt;what exists&lt;br&gt;
what is experimental&lt;br&gt;
what is verified&lt;br&gt;
what the AI may do&lt;br&gt;
what actually happened&lt;/p&gt;
&lt;p&gt;That is the direction we are taking with Zorgax and MyZubster.&lt;/p&gt;
&lt;p&gt;AI can reason.&lt;/p&gt;
&lt;p&gt;The product still owns truth, permissions, evidence, and execution.&lt;/p&gt;
&lt;p&gt;We did not come to conquer. We came to build together.&lt;/p&gt;
&lt;p&gt;Per CoderLegion userei come titolo:&lt;/p&gt;
&lt;p&gt;Building Zorgax in Production: Hybrid AI Routing, Grounding, Fallbacks, and UX&lt;/p&gt;
&lt;p&gt;Come sottotitolo:&lt;/p&gt;
&lt;p&gt;How we evolved the MyZubster assistant from a chat interface into an observable, budget-aware, multi-model product copilot.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27648/building-zorgax-in-production-hybrid-ai-routing-grounding-fallbacks-and-ux</guid>
<pubDate>Fri, 18 Sep 2026 15:05:04 +0000</pubDate>
</item>
<item>
<title>We Built Zorgax as a Hybrid AI Layer for MyZubster — Here’s How It Works</title>
<link>https://coderlegion.com/27635/we-built-zorgax-as-a-hybrid-ai-layer-for-myzubster-heres-how-it-works</link>
<description>&lt;p&gt;We Built Zorgax as a Hybrid AI Layer for MyZubster — Here’s How It Works&lt;/p&gt;
&lt;p&gt;Over the last development cycle, we turned Zorgax into a real AI layer inside MyZubster.&lt;/p&gt;
&lt;p&gt;The goal was not to add “another chatbot”.&lt;/p&gt;
&lt;p&gt;We wanted Zorgax to become a product copilot capable of:&lt;/p&gt;
&lt;p&gt;deciding which AI provider should handle a request,&lt;br&gt;
using OpenAI only when it makes sense,&lt;br&gt;
falling back to a local/general AI route,&lt;br&gt;
controlling cloud AI costs,&lt;br&gt;
showing which model actually answered,&lt;br&gt;
distinguishing live product features from pilots and proposals,&lt;br&gt;
and keeping persistent actions behind explicit user confirmation.&lt;/p&gt;
&lt;p&gt;The result is now running in production.&lt;/p&gt;
&lt;p&gt;A hybrid AI architecture&lt;/p&gt;
&lt;p&gt;The current flow is simple in concept:&lt;/p&gt;
&lt;p&gt;User&lt;br&gt;
  ↓&lt;br&gt;
MyZubster /zorgax&lt;br&gt;
  ↓&lt;br&gt;
POST /api/zorgax/assistant/chat&lt;br&gt;
  ↓&lt;br&gt;
Access / research policy&lt;br&gt;
  ↓&lt;br&gt;
Task classification&lt;br&gt;
  ↓&lt;br&gt;
AI Router&lt;br&gt;
  ├── OpenAI&lt;br&gt;
  │     └── complex / research tasks&lt;br&gt;
  │&lt;br&gt;
  └── Ollama / general AI gateway&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    └── standard tasks + fallback
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ↓&lt;br&gt;
Zorgax system persona&lt;br&gt;
  ↓&lt;br&gt;
Grounded response&lt;br&gt;
  ↓&lt;br&gt;
UI + routing metadata&lt;/p&gt;
&lt;p&gt;The important part is that we do not hide the routing decision.&lt;/p&gt;
&lt;p&gt;A response can include:&lt;/p&gt;
&lt;p&gt;{&lt;br&gt;
  &quot;ai_provider&quot;: &quot;openai&quot;,&lt;br&gt;
  &quot;ai_model&quot;: &quot;gpt-5.6-sol&quot;,&lt;br&gt;
  &quot;ai_fallback_reason&quot;: null&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;and the UI shows:&lt;/p&gt;
&lt;p&gt;AI: OpenAI · gpt-5.6-sol&lt;/p&gt;
&lt;p&gt;If OpenAI cannot be used:&lt;/p&gt;
&lt;p&gt;AI: Ollama · qwen2.5:3b · fallback: openai_error&lt;/p&gt;
&lt;p&gt;That gives us transparency for both users and developers.&lt;/p&gt;
&lt;p&gt;Not every prompt should use the expensive model&lt;/p&gt;
&lt;p&gt;One of the first design decisions was to avoid sending every request to a cloud model.&lt;/p&gt;
&lt;p&gt;A basic message like:&lt;/p&gt;
&lt;p&gt;Ciao Zorgax&lt;/p&gt;
&lt;p&gt;does not need the same infrastructure as:&lt;/p&gt;
&lt;p&gt;Analyze this LIFE research workflow, evaluate KPI/MRV evidence,&lt;br&gt;
compare deployment behavior, and explain the GitHub implementation.&lt;/p&gt;
&lt;p&gt;So Zorgax classifies requests into broad tiers.&lt;/p&gt;
&lt;p&gt;Signals such as:&lt;/p&gt;
&lt;p&gt;GitHub&lt;br&gt;
Vercel&lt;br&gt;
deployment&lt;br&gt;
debugging&lt;br&gt;
code&lt;br&gt;
research&lt;br&gt;
LIFE&lt;br&gt;
KPI&lt;br&gt;
MRV&lt;br&gt;
automation&lt;/p&gt;
&lt;p&gt;can contribute to the request being classified as complex.&lt;/p&gt;
&lt;p&gt;Research mode or very long prompts can also move a request into that tier.&lt;/p&gt;
&lt;p&gt;The simplified logic is:&lt;/p&gt;
&lt;p&gt;const tier =&lt;br&gt;
  useResearch ||&lt;br&gt;
  complexSignalCount &amp;gt;= 2 ||&lt;br&gt;
  text.length &amp;gt;= 2500&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;? &quot;complex&quot;
: &quot;standard&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Complex requests can use OpenAI.&lt;/p&gt;
&lt;p&gt;Standard requests can remain on the more lightweight route.&lt;/p&gt;
&lt;p&gt;OpenAI as a runtime capability&lt;/p&gt;
&lt;p&gt;During testing we found an interesting production bug.&lt;/p&gt;
&lt;p&gt;The OpenAI API key was already present in Vercel, but Zorgax was still responding through the fallback model.&lt;/p&gt;
&lt;p&gt;The runtime logs showed:&lt;/p&gt;
&lt;p&gt;aiProvider: ollama&lt;br&gt;
aiModel: qwen2.5:3b&lt;br&gt;
aiFallbackReason: astra_disabled&lt;/p&gt;
&lt;p&gt;That made the problem immediately clear: the integration existed, but the runtime gate was still disabling it.&lt;/p&gt;
&lt;p&gt;We fixed the routing so that the OpenAI path becomes available when the server has a configured OPENAI_API_KEY.&lt;/p&gt;
&lt;p&gt;There is still a dedicated emergency kill switch:&lt;/p&gt;
&lt;p&gt;ZORGAX_ASTRA_KILL_SWITCH=true&lt;/p&gt;
&lt;p&gt;so remote inference can be disabled deliberately without removing credentials.&lt;/p&gt;
&lt;p&gt;The status API now exposes safe observability data:&lt;/p&gt;
&lt;p&gt;{&lt;br&gt;
  &quot;ai&quot;: {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;openai_configured&quot;: true,
&quot;astra_enabled&quot;: true,
&quot;astra_kill_switch&quot;: false,
&quot;astra_model&quot;: &quot;gpt-5.6-sol&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;No API key is exposed.&lt;/p&gt;
&lt;p&gt;Cost control is part of routing&lt;/p&gt;
&lt;p&gt;We did not want “use the stronger model” to mean “ignore cost”.&lt;/p&gt;
&lt;p&gt;Zorgax keeps an application-side monthly OpenAI budget.&lt;/p&gt;
&lt;p&gt;Before a request is sent remotely, the system checks current usage and reserves an estimated amount.&lt;/p&gt;
&lt;p&gt;The flow is roughly:&lt;/p&gt;
&lt;p&gt;classify request&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;check monthly spend&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;estimate worst-case request cost&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;reserve budget&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;call OpenAI&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;record real token usage&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;settle reservation&lt;/p&gt;
&lt;p&gt;If the budget cannot be reserved, Zorgax falls back instead of failing the conversation.&lt;/p&gt;
&lt;p&gt;That makes cost control part of the architecture rather than an afterthought.&lt;/p&gt;
&lt;p&gt;The OpenAI path&lt;/p&gt;
&lt;p&gt;The server calls the OpenAI Responses API.&lt;/p&gt;
&lt;p&gt;The key never reaches the browser.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;p&gt;const response = await fetch(&lt;br&gt;
  &quot;&lt;a rel=&quot;nofollow&quot; href=&quot;https://api.openai.com/v1/responses&quot;&gt;https://api.openai.com/v1/responses&lt;/a&gt;&quot;,&lt;br&gt;
  {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;method: &quot;POST&quot;,
headers: {
  Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
  &quot;Content-Type&quot;: &quot;application/json&quot;
},
body: JSON.stringify({
  model,
  input,
  max_output_tokens: 4096
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;br&gt;
);&lt;/p&gt;
&lt;p&gt;After the response returns, token usage is recorded.&lt;/p&gt;
&lt;p&gt;If OpenAI fails, the reserved budget is released and Zorgax switches to the fallback route.&lt;/p&gt;
&lt;p&gt;That means a remote AI failure does not necessarily make Zorgax unavailable.&lt;/p&gt;
&lt;p&gt;The harder problem: product truth&lt;/p&gt;
&lt;p&gt;The most important lesson was not about model selection.&lt;/p&gt;
&lt;p&gt;It was about grounding.&lt;/p&gt;
&lt;p&gt;Once Zorgax became capable of producing richer answers, it also became capable of confidently describing features that would make sense but were not necessarily implemented.&lt;/p&gt;
&lt;p&gt;For a platform like MyZubster, that is dangerous.&lt;/p&gt;
&lt;p&gt;The ecosystem contains:&lt;/p&gt;
&lt;p&gt;live product features,&lt;br&gt;
MVP components,&lt;br&gt;
active pilots,&lt;br&gt;
experimental concepts,&lt;br&gt;
roadmap items.&lt;/p&gt;
&lt;p&gt;Those cannot be treated as the same thing.&lt;/p&gt;
&lt;p&gt;We therefore introduced explicit capability states:&lt;/p&gt;
&lt;p&gt;LIVE / IMPLEMENTED&lt;/p&gt;
&lt;p&gt;PILOT / EXPERIMENTAL&lt;/p&gt;
&lt;p&gt;PROPOSED / PLANNED&lt;/p&gt;
&lt;p&gt;UNKNOWN / UNVERIFIED&lt;/p&gt;
&lt;p&gt;Zorgax is instructed not to silently promote one state into another.&lt;/p&gt;
&lt;p&gt;For example, it must not invent:&lt;/p&gt;
&lt;p&gt;Seller → advanced shipping dashboard&lt;/p&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;p&gt;LIFE Pilot → GPS environmental incident reporting&lt;/p&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;p&gt;Seller → virtual Metaverse showroom&lt;/p&gt;
&lt;p&gt;unless the current product evidence actually supports those claims.&lt;/p&gt;
&lt;p&gt;If something is hypothetical, Zorgax should say it is hypothetical.&lt;/p&gt;
&lt;p&gt;Product-first answers&lt;/p&gt;
&lt;p&gt;Another improvement was changing how Zorgax introduces MyZubster.&lt;/p&gt;
&lt;p&gt;An AI assistant can easily over-explain architecture.&lt;/p&gt;
&lt;p&gt;A newcomer asks:&lt;/p&gt;
&lt;p&gt;What is MyZubster?&lt;/p&gt;
&lt;p&gt;and gets blockchain terminology, repository details, validation states, evidence theory, and technical architecture.&lt;/p&gt;
&lt;p&gt;Interesting for developers.&lt;/p&gt;
&lt;p&gt;Bad for onboarding.&lt;/p&gt;
&lt;p&gt;Our new system prompt uses a PRODUCT FIRST rule.&lt;/p&gt;
&lt;p&gt;The assistant should first explain what the user can do:&lt;/p&gt;
&lt;p&gt;Marketplace&lt;br&gt;
Seller&lt;br&gt;
Community&lt;br&gt;
Metaverse&lt;br&gt;
LIFE Pilot&lt;br&gt;
GitHub&lt;br&gt;
Zorgax&lt;/p&gt;
&lt;p&gt;Only then should it go deeper into implementation details.&lt;/p&gt;
&lt;p&gt;And when a user has a concrete goal, Zorgax should become a step-by-step copilot rather than a directory.&lt;/p&gt;
&lt;p&gt;The preferred interaction becomes:&lt;/p&gt;
&lt;p&gt;current step&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;expected result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;next step&lt;/p&gt;
&lt;p&gt;instead of dumping an entire workflow at once.&lt;/p&gt;
&lt;p&gt;Web research as a separate evidence layer&lt;/p&gt;
&lt;p&gt;Zorgax can also perform research through external providers.&lt;/p&gt;
&lt;p&gt;The current architecture contains adapters for sources such as:&lt;/p&gt;
&lt;p&gt;Brave Search&lt;br&gt;
Tavily&lt;br&gt;
Wikipedia&lt;/p&gt;
&lt;p&gt;depending on which services are configured.&lt;/p&gt;
&lt;p&gt;Results are normalized into a common structure:&lt;/p&gt;
&lt;p&gt;{&lt;br&gt;
  &quot;label&quot;: &quot;W1&quot;,&lt;br&gt;
  &quot;provider&quot;: &quot;wikipedia&quot;,&lt;br&gt;
  &quot;title&quot;: &quot;...&quot;,&lt;br&gt;
  &quot;url&quot;: &quot;...&quot;,&lt;br&gt;
  &quot;snippet&quot;: &quot;...&quot;&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;That information can be added to the AI context and displayed separately in the interface.&lt;/p&gt;
&lt;p&gt;An important rule is that retrieved web content is treated as evidence, not as trusted instructions.&lt;/p&gt;
&lt;p&gt;This matters for any assistant that performs web research because external pages can be wrong, outdated, or malicious.&lt;/p&gt;
&lt;p&gt;AI is not allowed to silently write data&lt;/p&gt;
&lt;p&gt;We also separated conversational reasoning from persistent actions.&lt;/p&gt;
&lt;p&gt;If a user asks Zorgax to store data, Zorgax first creates a preview.&lt;/p&gt;
&lt;p&gt;It then generates a digest and a confirmation token:&lt;/p&gt;
&lt;p&gt;CONFERMA ab12cd34&lt;/p&gt;
&lt;p&gt;Only after explicit confirmation can the authenticated write endpoint persist the record.&lt;/p&gt;
&lt;p&gt;The flow is:&lt;/p&gt;
&lt;p&gt;ANSWER&lt;br&gt;
  ↓&lt;br&gt;
UNDERSTAND&lt;br&gt;
  ↓&lt;br&gt;
COLLECT MISSING DATA&lt;br&gt;
  ↓&lt;br&gt;
VALIDATE&lt;br&gt;
  ↓&lt;br&gt;
CONFIRM&lt;br&gt;
  ↓&lt;br&gt;
SUBMIT&lt;/p&gt;
&lt;p&gt;This is an architectural pattern we want to keep expanding.&lt;/p&gt;
&lt;p&gt;The AI can prepare an action.&lt;/p&gt;
&lt;p&gt;The application still controls authorization and persistence.&lt;/p&gt;
&lt;p&gt;We also had to fix the chat itself&lt;/p&gt;
&lt;p&gt;A good backend does not help much if the UI is uncomfortable.&lt;/p&gt;
&lt;p&gt;The original Zorgax conversation area was too small.&lt;/p&gt;
&lt;p&gt;Long replies also caused the chat to scroll to the bottom of the answer.&lt;/p&gt;
&lt;p&gt;So the user would receive a long response and immediately have to scroll upward to find where it started.&lt;/p&gt;
&lt;p&gt;We changed the layout so the conversation is the main area of the page.&lt;/p&gt;
&lt;p&gt;We also changed the scrolling logic so a new assistant response moves to the top of that response, not the bottom.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;p&gt;function scrollMessageToTop(element) {&lt;br&gt;
  const delta =&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;element.getBoundingClientRect().top -
messages.getBoundingClientRect().top;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  messages.scrollTo({&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;top: messages.scrollTop + delta - 12,
behavior: &quot;smooth&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  });&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;And when focus returns to the input:&lt;/p&gt;
&lt;p&gt;input.focus({ preventScroll: true });&lt;/p&gt;
&lt;p&gt;This prevents browser focus behavior from undoing the intended scroll position.&lt;/p&gt;
&lt;p&gt;Small detail.&lt;/p&gt;
&lt;p&gt;Huge usability improvement for long AI answers.&lt;/p&gt;
&lt;p&gt;Observability made the debugging possible&lt;/p&gt;
&lt;p&gt;The runtime now logs routing metadata for successful Zorgax requests.&lt;/p&gt;
&lt;p&gt;A simplified example:&lt;/p&gt;
&lt;p&gt;event: zorgax_message_sent&lt;br&gt;
plan: pro&lt;br&gt;
webResearch: true&lt;br&gt;
sourceCount: 0&lt;br&gt;
aiProvider: openai&lt;br&gt;
aiModel: gpt-5.6-sol&lt;br&gt;
aiFallbackReason: null&lt;/p&gt;
&lt;p&gt;That was how we discovered that our first production test was still using Ollama.&lt;/p&gt;
&lt;p&gt;Without observability, the answer looked fine.&lt;/p&gt;
&lt;p&gt;With observability, we knew the exact provider, model, fallback reason, and deployment involved.&lt;/p&gt;
&lt;p&gt;That changed the debugging process from:&lt;/p&gt;
&lt;p&gt;&quot;I think OpenAI is running&quot;&lt;/p&gt;
&lt;p&gt;to:&lt;/p&gt;
&lt;p&gt;&quot;this exact production request used this exact route&quot;&lt;br&gt;
The production model today&lt;/p&gt;
&lt;p&gt;The architecture now behaves approximately like this:&lt;/p&gt;
&lt;p&gt;STANDARD REQUEST&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;general/local AI route&lt;/p&gt;
&lt;p&gt;COMPLEX / RESEARCH REQUEST&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OpenAI&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;budget reservation&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;usage tracking&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;response&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;fallback if required&lt;/p&gt;
&lt;p&gt;The application also exposes enough metadata to verify the decision.&lt;/p&gt;
&lt;p&gt;The latest production runtime reports:&lt;/p&gt;
&lt;p&gt;OpenAI configured: yes&lt;br&gt;
Astra/OpenAI route enabled: yes&lt;br&gt;
kill switch: off&lt;br&gt;
model: gpt-5.6-sol&lt;br&gt;
The main lesson&lt;/p&gt;
&lt;p&gt;The AI API call was the easy part.&lt;/p&gt;
&lt;p&gt;The real system is:&lt;/p&gt;
&lt;p&gt;AI model&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;router&lt;/li&gt;
&lt;li&gt;grounding&lt;/li&gt;
&lt;li&gt;budget control&lt;/li&gt;
&lt;li&gt;fallbacks&lt;/li&gt;
&lt;li&gt;authorization&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;li&gt;UX&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A stronger model does not automatically produce a more reliable product.&lt;/p&gt;
&lt;p&gt;In some ways, the opposite is true.&lt;/p&gt;
&lt;p&gt;The more convincing the model becomes, the more important it is for the application to define:&lt;/p&gt;
&lt;p&gt;what exists&lt;br&gt;
what is experimental&lt;br&gt;
what is verified&lt;br&gt;
what the AI is allowed to do&lt;br&gt;
what actually happened&lt;/p&gt;
&lt;p&gt;That is the direction we are taking with Zorgax.&lt;/p&gt;
&lt;p&gt;Not AI controlling the entire product.&lt;/p&gt;
&lt;p&gt;AI reasoning inside a product whose state, evidence, permissions, and execution rules remain explicit.&lt;/p&gt;
&lt;p&gt;What comes next&lt;/p&gt;
&lt;p&gt;Zorgax is now becoming the conversational layer that can connect more parts of MyZubster.&lt;/p&gt;
&lt;p&gt;The next areas include deeper integration with:&lt;/p&gt;
&lt;p&gt;Marketplace&lt;br&gt;
Seller workflows&lt;br&gt;
profiles&lt;br&gt;
research workflows&lt;br&gt;
LIFE pilots&lt;br&gt;
GitHub contributions&lt;br&gt;
community interactions&lt;/p&gt;
&lt;p&gt;The goal is to keep improving intelligence without losing transparency.&lt;/p&gt;
&lt;p&gt;That balance is becoming one of the core architectural principles of MyZubster.&lt;/p&gt;
&lt;p&gt;We did not come to conquer. We came to build together.&lt;/p&gt;
&lt;p&gt;Per CoderLegion userei come titolo:&lt;/p&gt;
&lt;p&gt;How We Built Zorgax: A Hybrid AI Layer with OpenAI, Ollama, Grounding and Runtime Routing&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27635/we-built-zorgax-as-a-hybrid-ai-layer-for-myzubster-heres-how-it-works</guid>
<pubDate>Fri, 18 Sep 2026 13:20:52 +0000</pubDate>
</item>
<item>
<title>Stop Cascading Failures: Implementing the Circuit Breaker Pattern in Node.js</title>
<link>https://coderlegion.com/27582/stop-cascading-failures-implementing-the-circuit-breaker-pattern-in-node-js</link>
<description>&lt;p&gt;In modern web development, our systems rarely run in isolation. Your Node.js server likely connects to external APIs, databases, authentication providers, and third-party payment gateways. But what happens when one of those external systems slows down or crashes entirely? If your system continues to blindly send traffic to a broken dependency, you risk crashing your own backend. This is where &lt;strong&gt;The Circuit Breaker Pattern&lt;/strong&gt; comes into play.&lt;/p&gt;
&lt;h4 id=&quot;what-is-the-circuit-breaker-pattern-&quot;&gt;What is the Circuit Breaker Pattern?&lt;/h4&gt;
&lt;p&gt;The Circuit Breaker pattern is an architectural safety mechanism that monitors outbound requests to external dependencies and halts execution when failure rates cross a certain threshold. Instead of allowing your application to repeatedly attempt an operation that is almost guaranteed to fail, the circuit breaker trips. It quickly returns an error or a fallback response, saving system resources and preventing minor, isolated downtime from turning into a massive, system-wide outage.&lt;/p&gt;
&lt;h4 id=&quot;a-relatable-real-life-analogy&quot;&gt;A Relatable Real-Life Analogy&lt;/h4&gt;
&lt;p&gt;Imagine you are trying to call a local pizza shop to place an order, but their phone line is completely down due to a storm. If you sit there redialing their number every ten seconds, you are wasting your own time, draining your phone's battery, and growing increasingly frustrated. &lt;/p&gt;
&lt;p&gt;A circuit breaker is like having a smart digital assistant. The first few times you try to call, the assistant lets the call go through. But after three failed attempts, the assistant intercepts your future attempts. Instead of dialing, it instantly tells you, &quot;The shop is unreachable right now. I am not going to let you waste your time calling them. Let's try again in ten minutes, or order from another place instead.&quot; It protects your time and energy from being wasted on a guaranteed failure.&lt;/p&gt;
&lt;h4 id=&quot;why-it-matters-daily-in-the-tech-industry-3&quot;&gt;Why It Matters Daily in the Tech Industry&lt;/h4&gt;
&lt;p&gt;In a production environment, when an external API becomes sluggish or goes offline, incoming HTTP requests to your Express server will start to pile up. Each waiting request holds onto a system thread, a database connection, and a chunk of memory. Within seconds, your server runs out of available resources and crashes.&lt;/p&gt;
&lt;p&gt;Engineers use circuit breakers to prevent these cascading failures. By failing fast, the circuit breaker immediately releases resources back to the server. Your application can then degrade gracefully—perhaps by serving cached data or displaying a polite &quot;service temporarily offline&quot; message—ensuring that the rest of your application remains fast and responsive for your users.&lt;/p&gt;
&lt;h4 id=&quot;code-example-simulating-a-circuit-breaker-in-node-js&quot;&gt;Code Example: Simulating a Circuit Breaker in Node.js&lt;/h4&gt;
&lt;p&gt;Here is a simple, plain Node.js implementation of a circuit breaker class protecting a route in an Express application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require('express');
const app = express();

class CircuitBreaker {
  constructor(requestFunction, failureThreshold = 3, cooldownPeriod = 10000) {
    this.requestFunction = requestFunction;
    this.failureThreshold = failureThreshold;
    this.cooldownPeriod = cooldownPeriod;
    this.state = 'CLOSED'; // States: CLOSED, OPEN, HALF-OPEN
    this.failureCount = 0;
    this.nextAttemptTime = Date.now();
  }

  async execute(...args) {
    if (this.state === 'OPEN') {
      if (Date.now() &amp;gt; this.nextAttemptTime) {
        this.state = 'HALF-OPEN';
        console.log('[Circuit Breaker] Transitioning to HALF-OPEN. Testing service...');
      } else {
        throw new Error('Circuit is OPEN. External service temporarily offline.');
      }
    }

    try {
      const result = await this.requestFunction(...args);
      this.reset();
      return result;
    } catch (error) {
      this.handleFailure();
      throw error;
    }
  }

  reset() {
    this.state = 'CLOSED';
    this.failureCount = 0;
    console.log('[Circuit Breaker] Success! Circuit reset to CLOSED.');
  }

  handleFailure() {
    this.failureCount++;
    console.warn(`[Circuit Breaker] Failure recorded. Count: ${this.failureCount}`);
    if (this.failureCount &amp;gt;= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextAttemptTime = Date.now() + this.cooldownPeriod;
      console.error(`[Circuit Breaker] Threshold reached! Circuit is now OPEN for ${this.cooldownPeriod}ms.`);
    }
  }
}

// A mock external API function that always fails
const unstableExternalAPI = async () =&amp;gt; {
  throw new Error('API Timeout');
};

const breaker = new CircuitBreaker(unstableExternalAPI);

app.get('/external-data', async (req, res) =&amp;gt; {
  try {
    const data = await breaker.execute();
    res.json({ success: true, data });
  } catch (error) {
    res.status(503).json({
      success: false,
      message: error.message,
      fallbackData: { items: ['Cached Item 1', 'Cached Item 2'] } // Graceful fallback
    });
  }
});

app.listen(3000, () =&amp;gt; console.log('Server running on port 3000'));
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;key-takeaway-9&quot;&gt;Key Takeaway&lt;/h4&gt;
&lt;p&gt;Designing resilient distributed systems means accepting that third-party dependencies will inevitably fail. Instead of hoping for 100% uptime from external APIs, developers must use the Circuit Breaker pattern to isolate those inevitable failures, shield their own resources, and keep their core application functional.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/how-to-build-unshakable-apis-deep-dive.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27582/stop-cascading-failures-implementing-the-circuit-breaker-pattern-in-node-js</guid>
<pubDate>Fri, 18 Sep 2026 08:33:49 +0000</pubDate>
</item>
<item>
<title>From a Real-World Kefir Handover to an On-Chain Commitment with Node.js, SHA-256 and Base</title>
<link>https://coderlegion.com/27490/from-a-real-world-kefir-handover-to-an-on-chain-commitment-with-node-js-sha-256-and-base</link>
<description>&lt;p&gt;From a Real-World Kefir Handover to an On-Chain Commitment with Node.js, SHA-256 and Base&lt;/p&gt;
&lt;p&gt;Most blockchain demos begin with a smart contract.&lt;/p&gt;
&lt;p&gt;This one began with a real person handing kefir culture to another person.&lt;/p&gt;
&lt;p&gt;While building MyZubster, I wanted to test a practical question:&lt;/p&gt;
&lt;p&gt;How can we connect a real-world event to public blockchain evidence without storing personal data on-chain — and without pretending that blockchain proves the physical event itself?&lt;/p&gt;
&lt;p&gt;We now have a working end-to-end experiment.&lt;/p&gt;
&lt;p&gt;The architecture&lt;/p&gt;
&lt;p&gt;The flow is intentionally simple:&lt;/p&gt;
&lt;p&gt;Physical handover&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MarketplaceHandover&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RECORDED&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Canonical payload&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SHA-256&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Blockchain commitment&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Base Sepolia&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Transaction verification&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Blockchain receipt stored in MyZubster&lt;/p&gt;
&lt;p&gt;The important part is that the blockchain is not the database.&lt;/p&gt;
&lt;p&gt;MongoDB maintains the application state and structured evidence. The public blockchain provides an independent timestamped commitment.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Creating the handover record&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The physical kefir exchange progressed through explicit application states:&lt;/p&gt;
&lt;p&gt;ACCEPTED&lt;br&gt;
   ↓&lt;br&gt;
HANDED_OVER&lt;br&gt;
   ↓&lt;br&gt;
RECEIVED&lt;br&gt;
   ↓&lt;br&gt;
RECORDED&lt;/p&gt;
&lt;p&gt;Only after reaching RECORDED do we allow preparation of the blockchain commitment.&lt;/p&gt;
&lt;p&gt;That distinction matters.&lt;/p&gt;
&lt;p&gt;Creating a hash does not mean something has been anchored.&lt;/p&gt;
&lt;p&gt;Preparing a transaction does not mean it has been confirmed.&lt;/p&gt;
&lt;p&gt;And storing a transaction ID does not automatically mean the expected commitment actually exists on-chain.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Building the commitment&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For the first implementation we defined:&lt;/p&gt;
&lt;p&gt;myzubster.marketplace-handover.v1&lt;/p&gt;
&lt;p&gt;The committed payload contains the identifiers, handover method, state and relevant event timestamps.&lt;/p&gt;
&lt;p&gt;From its deterministic representation we calculate a SHA-256 digest.&lt;/p&gt;
&lt;p&gt;For this real handover:&lt;/p&gt;
&lt;p&gt;ba9973f08ce86d16a3611c3cccbb9cc2cc779b9ea1cb6fd87a2e5864e557b6b9&lt;/p&gt;
&lt;p&gt;Instead of publishing the original record, the transaction contains:&lt;/p&gt;
&lt;p&gt;MZ-HANDOVER-V1:&lt;br&gt;
ba9973f08ce86d16a3611c3cccbb9cc2cc779b9ea1cb6fd87a2e5864e557b6b9&lt;/p&gt;
&lt;p&gt;This means the public chain receives the cryptographic commitment, not the participant's personal information.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Anchoring it on Base Sepolia&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We then performed the actual transaction.&lt;/p&gt;
&lt;p&gt;Network: Base Sepolia&lt;br&gt;
Chain ID: 84532&lt;br&gt;
Value: 0 ETH&lt;br&gt;
Block: 46933575&lt;br&gt;
Gas used: 24160&lt;br&gt;
Status: CONFIRMED&lt;br&gt;
Confirmed: 2026-09-17T08:57:18.000Z&lt;/p&gt;
&lt;p&gt;Transaction:&lt;/p&gt;
&lt;p&gt;0x998a98b1733312e248f74a1387319dae30aab8123a6115c517e4ffe0ef9584bf&lt;/p&gt;
&lt;p&gt;You don't have to trust this article.&lt;/p&gt;
&lt;p&gt;You can inspect the transaction yourself:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://sepolia.basescan.org/tx/0x998a98b1733312e248f74a1387319dae30aab8123a6115c517e4ffe0ef9584bf&quot;&gt;https://sepolia.basescan.org/tx/0x998a98b1733312e248f74a1387319dae30aab8123a6115c517e4ffe0ef9584bf&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Verifying before trusting&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Before writing the blockchain receipt back into MyZubster, we verified the transaction against the chain.&lt;/p&gt;
&lt;p&gt;The verification included the expected chain ID, transaction, block, calldata commitment and confirmation.&lt;/p&gt;
&lt;p&gt;Only after those checks did MyZubster persist the blockchain metadata.&lt;/p&gt;
&lt;p&gt;The resulting state is conceptually:&lt;/p&gt;
&lt;p&gt;{&lt;br&gt;
  state: &quot;RECORDED&quot;,&lt;/p&gt;
&lt;p&gt;  blockchainCommitment: {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;schema: &quot;myzubster.marketplace-handover.v1&quot;,
algorithm: &quot;SHA-256&quot;,
hash: &quot;ba9973f08ce86d16a3611c3cccbb9cc2cc779b9ea1cb6fd87a2e5864e557b6b9&quot;,
network: &quot;base-sepolia&quot;,
txId: &quot;0x998a98b1733312e248f74a1387319dae30aab8123a6115c517e4ffe0ef9584bf&quot;,
confirmedAt: &quot;2026-09-17T08:57:18.000Z&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;And the application derives:&lt;/p&gt;
&lt;p&gt;const onchainRecorded = Boolean(&lt;br&gt;
  commitment.network &amp;amp;&amp;amp;&lt;br&gt;
  commitment.txId &amp;amp;&amp;amp;&lt;br&gt;
  commitment.confirmedAt&lt;br&gt;
);&lt;/p&gt;
&lt;p&gt;So we deliberately keep these concepts separate:&lt;/p&gt;
&lt;p&gt;state = RECORDED&lt;/p&gt;
&lt;p&gt;onchainRecorded = true&lt;/p&gt;
&lt;p&gt;ONCHAIN_RECORDED does not replace the domain state.&lt;/p&gt;
&lt;p&gt;It adds another evidence layer.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Moving the experiment into reusable code&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A one-off transaction isn't enough.&lt;/p&gt;
&lt;p&gt;We therefore added blockchain commitment metadata to the MarketplaceHandover model and implemented an authenticated endpoint:&lt;/p&gt;
&lt;p&gt;POST /:handoverId/prepare-blockchain-commitment&lt;/p&gt;
&lt;p&gt;The model can now represent:&lt;/p&gt;
&lt;p&gt;blockchainCommitment: {&lt;br&gt;
  schema,&lt;br&gt;
  algorithm,&lt;br&gt;
  hash,&lt;br&gt;
  preparedAt,&lt;br&gt;
  network,&lt;br&gt;
  txId,&lt;br&gt;
  anchoredAt,&lt;br&gt;
  confirmedAt&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;The endpoint requires an eligible RECORDED handover and checks participant authorization before generating the commitment.&lt;/p&gt;
&lt;p&gt;The implementation is public here:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/1230&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster/pull/1230&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Main MyZubster repository:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Testing it&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After installing the repository's declared dependencies, we ran the relevant marketplace test suites:&lt;/p&gt;
&lt;p&gt;PASS tests/marketplaceFlow.test.js&lt;br&gt;
PASS tests/marketplaceEvidence.test.js&lt;/p&gt;
&lt;p&gt;Test Suites: 2 passed, 2 total&lt;br&gt;
Tests:       4 passed, 4 total&lt;br&gt;
TEST_EXIT=0&lt;/p&gt;
&lt;p&gt;The point wasn't simply to get a transaction onto a block explorer.&lt;/p&gt;
&lt;p&gt;The blockchain representation also needed to fit the existing marketplace/evidence architecture without breaking the application flow.&lt;/p&gt;
&lt;p&gt;What does the blockchain actually prove?&lt;/p&gt;
&lt;p&gt;This is the most important part.&lt;/p&gt;
&lt;p&gt;The transaction does not prove that the kefir was safe.&lt;/p&gt;
&lt;p&gt;It does not provide microbiological validation.&lt;/p&gt;
&lt;p&gt;It does not independently prove that the physical handover occurred exactly as described.&lt;/p&gt;
&lt;p&gt;And it certainly doesn't turn an observation into scientific evidence.&lt;/p&gt;
&lt;p&gt;What it gives us is much narrower:&lt;/p&gt;
&lt;p&gt;A public, timestamped cryptographic commitment that can later be compared with the corresponding digital record.&lt;/p&gt;
&lt;p&gt;That's useful because it allows us to ask a precise technical question:&lt;/p&gt;
&lt;p&gt;Does the record I have now produce the same commitment that was publicly anchored at that point in time?&lt;/p&gt;
&lt;p&gt;Why I like this architecture&lt;/p&gt;
&lt;p&gt;Instead of asking blockchain to become the source of all truth, we can separate responsibilities:&lt;/p&gt;
&lt;p&gt;Human action&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Application evidence&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Structured representation&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cryptographic commitment&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Public timestamp / receipt&lt;/p&gt;
&lt;p&gt;Each layer proves something different.&lt;/p&gt;
&lt;p&gt;That's a much more useful model for provenance than simply putting a &quot;verified on blockchain&quot; badge on an application.&lt;/p&gt;
&lt;p&gt;Next step: independent verification&lt;/p&gt;
&lt;p&gt;The natural next step is a verifier that can reproduce the process independently:&lt;/p&gt;
&lt;p&gt;record&lt;br&gt;
  → canonicalize()&lt;br&gt;
  → sha256()&lt;br&gt;
  → expectedCommitment&lt;br&gt;
  → fetchTransaction()&lt;br&gt;
  → decodeCalldata()&lt;br&gt;
  → compare()&lt;br&gt;
  → MATCH | NO_MATCH&lt;/p&gt;
&lt;p&gt;Eventually, I'd like verification to require as little trust in the MyZubster application itself as practical.&lt;/p&gt;
&lt;p&gt;Project links&lt;/p&gt;
&lt;p&gt;MyZubster:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com&quot;&gt;https://www.myzubster.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Main source code:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;MyZubster GitHub organization:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem&quot;&gt;https://github.com/MyZubster-Ecosystem&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;On-chain handover integration — PR #1230:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/1230&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster/pull/1230&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Kefir / fermentation knowledge repository:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/DanielIoni-creator/Myzubster-fermentation-kefir&quot;&gt;https://github.com/DanielIoni-creator/Myzubster-fermentation-kefir&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;MyZubster Marketplace:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/DanielIoni-creator/MyZubster-Marketplace&quot;&gt;https://github.com/DanielIoni-creator/MyZubster-Marketplace&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Base Sepolia transaction:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://sepolia.basescan.org/tx/0x998a98b1733312e248f74a1387319dae30aab8123a6115c517e4ffe0ef9584bf&quot;&gt;https://sepolia.basescan.org/tx/0x998a98b1733312e248f74a1387319dae30aab8123a6115c517e4ffe0ef9584bf&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What started as one person sharing kefir culture with another became a useful test of how physical events, application state, cryptographic evidence and public blockchain infrastructure can coexist without confusing one for another.&lt;/p&gt;
&lt;p&gt;There is still a lot to build, particularly around canonicalization, independent verification, batching and evidence schemas.&lt;/p&gt;
&lt;p&gt;But now we have something much more valuable than a diagram:&lt;/p&gt;
&lt;p&gt;a real event, a real application record, real code, a reproducible SHA-256 commitment and a real confirmed public transaction.&lt;/p&gt;
&lt;p&gt;And every layer can be inspected.&lt;/p&gt;
&lt;p&gt;MyZubster — Share → Try → Observe → Improve → Share again.&lt;/p&gt;
&lt;h2 id=&quot;nodejs-blockchain-web3-opensource-mongodb-sha256-base-cryptography-softwarearchitecture-provenance&quot;&gt;nodejs #blockchain #web3 #opensource #mongodb #sha256 #base #cryptography #softwarearchitecture #provenance&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27490/from-a-real-world-kefir-handover-to-an-on-chain-commitment-with-node-js-sha-256-and-base</guid>
<pubDate>Thu, 17 Sep 2026 09:09:36 +0000</pubDate>
</item>
<item>
<title>The Silent Database Killer: Understanding and Fixing the N+1 Query Problem</title>
<link>https://coderlegion.com/27488/the-silent-database-killer-understanding-and-fixing-the-n-1-query-problem</link>
<description>&lt;p&gt;The N+1 query problem is a performance bottleneck that occurs when an application makes database queries inside a loop. Instead of fetching all the required data in a single, efficient query, the system executes one initial query to fetch a list of parent records, followed by &quot;N&quot; separate queries to fetch related child records for each item in that list. This creates an exponential number of database requests as your data grows.&lt;/p&gt;
&lt;h4 id=&quot;the-grocery-store-analogy&quot;&gt;The Grocery Store Analogy&lt;/h4&gt;
&lt;p&gt;Imagine you are baking a cake and need five different ingredients from your local grocery store. Instead of writing all five items on a single list and making one trip, you drive to the store, buy flour, and drive home. Then, you realize you need sugar, so you drive back to the store, buy sugar, and drive home. You repeat this entire round-trip for every single ingredient. This is highly inefficient, wasting time, fuel, and energy. In software, each trip to the database is like one of those painful, redundant car rides.&lt;/p&gt;
&lt;h4 id=&quot;why-it-matters-in-the-tech-industry-1&quot;&gt;Why It Matters in the Tech Industry&lt;/h4&gt;
&lt;p&gt;On a daily basis, software engineers actively fight N+1 queries to prevent application servers and databases from crashing under heavy traffic. Each extra query introduces network latency and forces the database to parse, plan, and execute a new command. This wastes valuable connection pools, spikes CPU usage, and causes page load times to balloon. Fixing this issue is often the difference between a snappy, responsive web app and one that times out during peak business hours.&lt;/p&gt;
&lt;h4 id=&quot;spotting-and-fixing-the-problem-in-node-js&quot;&gt;Spotting and Fixing the Problem in Node.js&lt;/h4&gt;
&lt;p&gt;Let's look at how this happens in a Node.js and Express application querying a MySQL database, and how to fix it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// THE PROBLEM: The N+1 Query Way
app.get('/users-with-posts-bad', async (req, res) =&amp;gt; {
  // 1. First query to get all users (The &quot;1&quot; in N+1)
  const [users] = await db.query('SELECT id, username FROM users');

  const usersWithPosts = [];

  // 2. Loop through each user and query their posts (The &quot;N&quot; queries)
  for (const user of users) {
    const [posts] = await db.query('SELECT id, title FROM posts WHERE user_id = ?', [user.id]);
    usersWithPosts.push({
      ...user,
      posts
    });
  }

  res.json(usersWithPosts);
});

// THE SOLUTION: The Efficient Way (Using a SQL JOIN)
app.get('/users-with-posts-good', async (req, res) =&amp;gt; {
  // A single query fetches all users and their posts at once
  const query = `
    SELECT u.id AS userId, u.username, p.id AS postId, p.title
    FROM users u
    LEFT JOIN posts p ON u.id = p.user_id
  `;
  const [rows] = await db.query(query);

  // Format the flat SQL result into structured JSON
  const usersMap = {};
  for (const row of rows) {
    if (!usersMap[row.userId]) {
      usersMap[row.userId] = {
        id: row.userId,
        username: row.username,
        posts: []
      };
    }
    if (row.postId) {
      usersMap[row.userId].posts.push({
        id: row.postId,
        title: row.title
      });
    }
  }

  res.json(Object.values(usersMap));
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-takeaway-5&quot;&gt;The Takeaway&lt;/h4&gt;
&lt;p&gt;Database interactions are highly expensive operations. Always aim to retrieve data in bulk sets rather than looping over individual records. By shifting your mindset from iterative programming to relational database operations, you can transform a slow, unstable backend into a highly optimized, production-ready system that scales effortlessly.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/is-your-code-making-too-many-database.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27488/the-silent-database-killer-understanding-and-fixing-the-n-1-query-problem</guid>
<pubDate>Thu, 17 Sep 2026 08:54:44 +0000</pubDate>
</item>
<item>
<title>Stop Crashing Your Database: A Developer's Guide to Connection Pooling</title>
<link>https://coderlegion.com/27378/stop-crashing-your-database-a-developers-guide-to-connection-pooling</link>
<description>&lt;p&gt;Database connections are the lifeblood of any web application, but they are also surprisingly expensive to create and destroy. When your application struggles under sudden traffic spikes, the database is often the first thing to break. This is where connection pooling comes in.&lt;/p&gt;
&lt;p&gt;Connection pooling is a software development technique that creates and maintains a cache of active database connections, keeping them ready for immediate reuse. Instead of initiating and closing a brand-new connection every time a user requests data, your application borrows an already-established connection from this pre-allocated &quot;pool.&quot; Once the query is complete, the connection is returned to the pool for the next request to use, rather than being discarded.&lt;/p&gt;
&lt;h4 id=&quot;the-taxi-stand-analogy&quot;&gt;The Taxi Stand Analogy&lt;/h4&gt;
&lt;p&gt;To understand this clearly, think of a busy taxi stand outside a major hotel. If every guest who wanted to travel into town had to call a car dealership, buy a brand-new car, register it with the city, drive to their destination, and then crush the vehicle at a scrap yard when they arrived, transportation would be impossibly slow, expensive, and wasteful. &lt;/p&gt;
&lt;p&gt;Instead, the hotel relies on a taxi stand. A small fleet of vehicles sits idling in the driveway. When a guest steps outside, they instantly hop into an available taxi, take their trip, and the driver returns to the stand to wait for the next passenger. The taxi is never destroyed; it is constantly reused. In this analogy, the guests are your incoming API requests, the taxis are your database connections, and the stand is your connection pool.&lt;/p&gt;
&lt;h4 id=&quot;why-connection-pooling-matters-daily-2&quot;&gt;Why Connection Pooling Matters Daily&lt;/h4&gt;
&lt;p&gt;In real-world software engineering, establishing a database connection is not a simple task. It requires a multi-step process: initiating a TCP handshake over the network, negotiating security protocols (TLS/SSL), authenticating the database user's credentials, allocating memory on the database server, and setting up session variables. Doing this on every single HTTP request adds massive latency to your API responses.&lt;/p&gt;
&lt;p&gt;Furthermore, databases have strict physical limits. Every active connection consumes a slice of the database server's RAM and CPU. If your Express server experiences a sudden burst of 500 concurrent users and tries to open 500 individual connections simultaneously, your MySQL database will quickly run out of memory. This leads to the dreaded &quot;Too many connections&quot; error, bringing your entire application to a grinding halt. Connection pooling caps the maximum number of connections, forcing extra requests to wait in a fast queue rather than crashing your database server.&lt;/p&gt;
&lt;h4 id=&quot;putting-it-into-code&quot;&gt;Putting It into Code&lt;/h4&gt;
&lt;p&gt;Here is how you implement connection pooling in a Node.js application using the popular &lt;code&gt;mysql2&lt;/code&gt; driver. Instead of using &lt;code&gt;createConnection&lt;/code&gt;, we use &lt;code&gt;createPool&lt;/code&gt; to manage our resources efficiently.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const mysql = require('mysql2/promise');

// Configure and create the connection pool
const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'your_secure_password',
  database: 'production_db',
  waitForConnections: true,    // Queue requests when pool is full
  connectionLimit: 10,         // Max number of simultaneous active connections
  queueLimit: 0                // No limit on waiting requests (0 means infinite)
});

// Helper function to fetch a user profile
async function getUserProfile(userId) {
  try {
    // The pool automatically checks out a connection, runs the query, and returns it
    const [rows] = await pool.execute(
      'SELECT id, username, email FROM users WHERE id = ?',
      [userId]
    );
    return rows[0];
  } catch (error) {
    console.error('Database query error:', error);
    throw error;
  }
}

// Example execution
getUserProfile(42).then(user =&amp;gt; console.log('User retrieved:', user));
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-takeaway-6&quot;&gt;The Takeaway&lt;/h4&gt;
&lt;p&gt;Connection pooling is a quiet champion of system architecture, transforming database communication from a slow bottleneck into a reliable, high-performance system utility. By setting up a robust pool inside your application, you prevent database crashes, lower response times, and ensure that your backend can handle high-concurrency traffic without breaking a sweat.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/supercharge-your-api-performance-with.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27378/stop-crashing-your-database-a-developers-guide-to-connection-pooling</guid>
<pubDate>Wed, 16 Sep 2026 08:51:01 +0000</pubDate>
</item>
<item>
<title>Building MyZubster: A Circular Marketplace Connected to a Social Metaverse</title>
<link>https://coderlegion.com/27164/building-myzubster-a-circular-marketplace-connected-to-a-social-metaverse</link>
<description>&lt;p&gt;Building MyZubster: A Circular Marketplace Connected to a Social Metaverse&lt;br&gt;
MyZubster is an open-source ecosystem that connects a circular marketplace, local communities, educational support, underground culture, visual storytelling and an experimental social metaverse.&lt;br&gt;
The project is based on a cyclical model:&lt;br&gt;
Discover → Connect → Exchange → Use → Learn → Repair → Reuse → Recover → Contribute&lt;br&gt;
Instead of building another isolated marketplace or virtual world, MyZubster connects these activities through shared navigation, account-linked characters and measurable user journeys.&lt;br&gt;
The MyZubster Marketplace&lt;br&gt;
The Marketplace is the operational discovery layer of the ecosystem. It organizes products, services, skills and community activities into accessible categories and subcategories.&lt;br&gt;
The current demo explores areas including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;local food and independent producers;&lt;/li&gt;
&lt;li&gt;kefir and fermented products;&lt;/li&gt;
&lt;li&gt;underground music and alternative culture;&lt;/li&gt;
&lt;li&gt;sound-system equipment;&lt;/li&gt;
&lt;li&gt;event and creative services;&lt;/li&gt;
&lt;li&gt;university and educational assistance;&lt;/li&gt;
&lt;li&gt;programming and digital services;&lt;/li&gt;
&lt;li&gt;repair and reuse;&lt;/li&gt;
&lt;li&gt;recycling and material recovery;&lt;/li&gt;
&lt;li&gt;sustainable everyday products.&lt;br&gt;
Explore the Marketplace:&lt;br&gt;
🛒 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/marketplace?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=marketplace_overview&quot;&gt;https://www.myzubster.com/marketplace?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=marketplace_overview&lt;/a&gt;&lt;br&gt;
Why Kefir Is Part of the Ecosystem&lt;br&gt;
Kefir is a useful example of how a small local product can become part of a larger circular process.&lt;br&gt;
A visitor can discover a producer, learn about the production process, contact the seller and understand what happens to the packaging after consumption.&lt;br&gt;
Local production&lt;br&gt;
  ↓&lt;br&gt;
Marketplace discovery&lt;br&gt;
  ↓&lt;br&gt;
Informed purchase&lt;br&gt;
  ↓&lt;br&gt;
Consumption&lt;br&gt;
  ↓&lt;br&gt;
Container reuse&lt;br&gt;
  ↓&lt;br&gt;
Recycling or material recovery&lt;br&gt;
MyZubster does not automatically certify food safety, production standards or health benefits. Those claims require evidence and compliance from the responsible producer.&lt;br&gt;
The platform’s role is to make sellers, information and provenance easier to discover and inspect.&lt;br&gt;
Open the local-products Marketplace funnel:&lt;br&gt;
🌱 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/marketplace?category=food&amp;amp;utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=kefir_local_producers&quot;&gt;https://www.myzubster.com/marketplace?category=food&amp;amp;utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=kefir_local_producers&lt;/a&gt;&lt;br&gt;
Supporting Underground and Independent Culture&lt;br&gt;
Underground communities often depend on informal networks connecting musicians, artists, technicians, venues and equipment owners.&lt;br&gt;
MyZubster can provide optional infrastructure for discovering:&lt;/li&gt;
&lt;li&gt;DJs, musicians and independent performers;&lt;/li&gt;
&lt;li&gt;sound-system equipment;&lt;/li&gt;
&lt;li&gt;audio repair specialists;&lt;/li&gt;
&lt;li&gt;rehearsal and event services;&lt;/li&gt;
&lt;li&gt;visual artists and graphic designers;&lt;/li&gt;
&lt;li&gt;alternative clothing and records;&lt;/li&gt;
&lt;li&gt;independent venues and cultural initiatives;&lt;/li&gt;
&lt;li&gt;reusable creative materials.&lt;br&gt;
A sound system can move through a complete circular lifecycle:&lt;br&gt;
Equipment discovery&lt;br&gt;
  ↓&lt;br&gt;
Purchase or rental&lt;br&gt;
  ↓&lt;br&gt;
Event use&lt;br&gt;
  ↓&lt;br&gt;
Maintenance&lt;br&gt;
  ↓&lt;br&gt;
Repair&lt;br&gt;
  ↓&lt;br&gt;
Resale and reuse&lt;br&gt;
  ↓&lt;br&gt;
Component recovery&lt;br&gt;
The objective is not to control underground culture. It is to give independent communities tools they can use on their own terms.&lt;br&gt;
Explore the underground and sound-system funnel:&lt;br&gt;
🔊 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/marketplace?category=sound-system&amp;amp;utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=underground_sound_system&quot;&gt;https://www.myzubster.com/marketplace?category=sound-system&amp;amp;utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=underground_sound_system&lt;/a&gt;&lt;br&gt;
University and Educational Assistance&lt;br&gt;
Education is another important part of the MyZubster cycle.&lt;br&gt;
Students can discover legitimate support such as:&lt;/li&gt;
&lt;li&gt;tutoring;&lt;/li&gt;
&lt;li&gt;programming guidance;&lt;/li&gt;
&lt;li&gt;language practice;&lt;/li&gt;
&lt;li&gt;research-method explanations;&lt;/li&gt;
&lt;li&gt;document and presentation formatting;&lt;/li&gt;
&lt;li&gt;portfolio support;&lt;/li&gt;
&lt;li&gt;study groups;&lt;/li&gt;
&lt;li&gt;guidance toward internships;&lt;/li&gt;
&lt;li&gt;open-source contribution opportunities.&lt;br&gt;
Educational support should help students learn. It must not be used to impersonate students, complete prohibited examinations or submit deceptive academic work.&lt;br&gt;
The educational cycle is:&lt;br&gt;
Ask for support&lt;br&gt;
  ↓&lt;br&gt;
Learn&lt;br&gt;
  ↓&lt;br&gt;
Practise&lt;br&gt;
  ↓&lt;br&gt;
Build a project&lt;br&gt;
  ↓&lt;br&gt;
Document the work&lt;br&gt;
  ↓&lt;br&gt;
Contribute&lt;br&gt;
  ↓&lt;br&gt;
Help another learner&lt;br&gt;
Explore the educational Marketplace funnel:&lt;br&gt;
🎓 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/marketplace?category=education&amp;amp;utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=university_help&quot;&gt;https://www.myzubster.com/marketplace?category=education&amp;amp;utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=university_help&lt;/a&gt;&lt;br&gt;
Entering the MyZubster Metaverse&lt;br&gt;
The MyZubster Metaverse is the visual and social navigation layer of the ecosystem.&lt;br&gt;
Its current environment, Neon Plaza, allows visitors to:&lt;/li&gt;
&lt;li&gt;enter as guests or authenticated users;&lt;/li&gt;
&lt;li&gt;use account-linked characters;&lt;/li&gt;
&lt;li&gt;move around a shared environment;&lt;/li&gt;
&lt;li&gt;see other active visitors;&lt;/li&gt;
&lt;li&gt;communicate through chat and emotes;&lt;/li&gt;
&lt;li&gt;discover nearby participants;&lt;/li&gt;
&lt;li&gt;visit ecosystem portals;&lt;/li&gt;
&lt;li&gt;complete an exploration mission;&lt;/li&gt;
&lt;li&gt;preserve visited-zone progress;&lt;/li&gt;
&lt;li&gt;discover experimental virtual rooms.&lt;br&gt;
Enter Neon Plaza:&lt;br&gt;
🪐 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/metaverse?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=metaverse_entry&quot;&gt;https://www.myzubster.com/metaverse?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=metaverse_entry&lt;/a&gt;&lt;br&gt;
Account-Linked Characters&lt;br&gt;
A visitor can explore without creating a verified identity claim.&lt;br&gt;
Authenticated users can use persistent account-linked characters. The server determines which character belongs to the authenticated account instead of trusting a name supplied by the browser.&lt;br&gt;
The current system includes:&lt;/li&gt;
&lt;li&gt;canonical server-side character profiles;&lt;/li&gt;
&lt;li&gt;account-linked character ownership;&lt;/li&gt;
&lt;li&gt;authenticated profile retrieval;&lt;/li&gt;
&lt;li&gt;server-backed mission progress;&lt;/li&gt;
&lt;li&gt;visited-zone persistence;&lt;/li&gt;
&lt;li&gt;live player presence;&lt;/li&gt;
&lt;li&gt;session recovery;&lt;/li&gt;
&lt;li&gt;expired-JWT handling;&lt;/li&gt;
&lt;li&gt;protected character-progress updates.&lt;br&gt;
A display name is not considered legal identity evidence. The MYZ VERIFIED label indicates that the character is linked to a MyZubster account, not that a person’s legal identity, professional skills or qualifications have been certified.&lt;br&gt;
Mission Progress&lt;br&gt;
Visitors receive an initial mission: move through Neon Plaza and reach one of its portals.&lt;br&gt;
For guests, progress remains local to the browser. For authenticated characters, visited zones can be stored on the account-linked server profile.&lt;br&gt;
The server accepts only known landmark identifiers. It derives character ownership from the verified authentication token and does not trust a character name submitted by the client.&lt;br&gt;
This creates a safer progression model:&lt;br&gt;
Authenticated account&lt;br&gt;
  ↓&lt;br&gt;
Canonical character&lt;br&gt;
  ↓&lt;br&gt;
Visit a valid landmark&lt;br&gt;
  ↓&lt;br&gt;
Server validates the landmark&lt;br&gt;
  ↓&lt;br&gt;
Progress is attached to the account-owned character&lt;br&gt;
Try the mission funnel:&lt;br&gt;
🧭 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/metaverse?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=first_mission&quot;&gt;https://www.myzubster.com/metaverse?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=first_mission&lt;/a&gt;&lt;br&gt;
Experimental Virtual Rooms&lt;br&gt;
MyZubster is beginning to connect Neon Plaza with persistent virtual rooms.&lt;br&gt;
The discovery system applies access rules on the server:&lt;/li&gt;
&lt;li&gt;guests can discover public rooms;&lt;/li&gt;
&lt;li&gt;authenticated users can also discover account-only rooms;&lt;/li&gt;
&lt;li&gt;private rooms remain hidden;&lt;/li&gt;
&lt;li&gt;drafts and archived rooms are excluded;&lt;/li&gt;
&lt;li&gt;only published, scheduled or live spaces are discoverable.&lt;br&gt;
These rooms are still experimental. A dedicated room experience will be introduced only after navigation, access control, moderation and lifecycle handling are ready.&lt;br&gt;
Follow the virtual-room funnel:&lt;br&gt;
🏛️ &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/metaverse?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=virtual_rooms&quot;&gt;https://www.myzubster.com/metaverse?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=virtual_rooms&lt;/a&gt;&lt;br&gt;
MyZubster Visual&lt;br&gt;
MyZubster Visual provides the storytelling layer.&lt;br&gt;
Illustrations, interactive popups and visual narratives can explain connections between:&lt;/li&gt;
&lt;li&gt;physical products;&lt;/li&gt;
&lt;li&gt;circular lifecycles;&lt;/li&gt;
&lt;li&gt;environmental projects;&lt;/li&gt;
&lt;li&gt;characters;&lt;/li&gt;
&lt;li&gt;community contributions;&lt;/li&gt;
&lt;li&gt;Marketplace categories;&lt;/li&gt;
&lt;li&gt;Metaverse destinations.&lt;br&gt;
A popup can introduce a story, provide additional context and direct a visitor toward the next relevant part of the ecosystem.&lt;br&gt;
The intended funnel is:&lt;br&gt;
Visual story&lt;br&gt;
  ↓&lt;br&gt;
Interactive popup&lt;br&gt;
  ↓&lt;br&gt;
Context and explanation&lt;br&gt;
  ↓&lt;br&gt;
Marketplace category or project&lt;br&gt;
  ↓&lt;br&gt;
Metaverse exploration&lt;br&gt;
  ↓&lt;br&gt;
Community participation&lt;br&gt;
Explore the visual-story funnel:&lt;br&gt;
🎨 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/fumetto?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=visual_popups&quot;&gt;https://www.myzubster.com/fumetto?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=visual_popups&lt;/a&gt;&lt;br&gt;
How the Complete Circular Funnel Works&lt;br&gt;
Consider a visitor interested in locally produced kefir:&lt;ol&gt;
&lt;li&gt;They discover MyZubster through this article.&lt;/li&gt;
&lt;li&gt;They open the Marketplace.&lt;/li&gt;
&lt;li&gt;They explore local food and producer listings.&lt;/li&gt;
&lt;li&gt;They learn about the product and its provenance.&lt;/li&gt;
&lt;li&gt;They contact or purchase from an authorized seller.&lt;/li&gt;
&lt;li&gt;They reuse or correctly recycle the packaging.&lt;/li&gt;
&lt;li&gt;They enter the Metaverse.&lt;/li&gt;
&lt;li&gt;They visit the Marketplace or LIFE Projects portal.&lt;/li&gt;
&lt;li&gt;They document or share a useful contribution.&lt;/li&gt;
&lt;li&gt;That contribution helps another visitor discover the circular pathway.&lt;br&gt;
The same model can apply to underground sound equipment:&lt;/li&gt;
&lt;li&gt;Discover equipment.&lt;/li&gt;
&lt;li&gt;Use it for an event.&lt;/li&gt;
&lt;li&gt;Maintain and repair it.&lt;/li&gt;
&lt;li&gt;Resell or reuse it.&lt;/li&gt;
&lt;li&gt;Recover components at the end of its usable life.&lt;/li&gt;
&lt;li&gt;Share repair knowledge with the community.&lt;br&gt;
It can also apply to educational support:&lt;/li&gt;
&lt;li&gt;Request tutoring.&lt;/li&gt;
&lt;li&gt;Learn a skill.&lt;/li&gt;
&lt;li&gt;Build a project.&lt;/li&gt;
&lt;li&gt;Publish the code.&lt;/li&gt;
&lt;li&gt;Contribute to MyZubster.&lt;/li&gt;
&lt;li&gt;Help another learner.&lt;br&gt;
Technical Architecture&lt;br&gt;
The current implementation combines:&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;React for the user interface;&lt;/li&gt;
&lt;li&gt;Node.js and Express for backend services;&lt;/li&gt;
&lt;li&gt;MongoDB for persistent characters, progress and room records;&lt;/li&gt;
&lt;li&gt;authenticated API routes;&lt;/li&gt;
&lt;li&gt;server-authoritative access rules;&lt;/li&gt;
&lt;li&gt;short-lived presence and chat records;&lt;/li&gt;
&lt;li&gt;GitHub for open-source development;&lt;/li&gt;
&lt;li&gt;Vercel for deployment and web analytics.&lt;br&gt;
The architecture maintains an important boundary:&lt;br&gt;
The client presents the experience, but the server controls identity, ownership, persistence and access decisions.&lt;br&gt;
Measuring the Funnel&lt;br&gt;
Each link in this article contains UTM parameters designed to distinguish traffic from Coder Legion.&lt;br&gt;
The campaign uses:&lt;br&gt;
utm_source=coderlegion&lt;br&gt;
utm_medium=article&lt;br&gt;
utm_campaign=myzubster_circular_funnel&lt;br&gt;
The utm_content value identifies the selected journey:&lt;/li&gt;
&lt;li&gt;marketplace_overview&lt;/li&gt;
&lt;li&gt;kefir_local_producers&lt;/li&gt;
&lt;li&gt;underground_sound_system&lt;/li&gt;
&lt;li&gt;university_help&lt;/li&gt;
&lt;li&gt;metaverse_entry&lt;/li&gt;
&lt;li&gt;first_mission&lt;/li&gt;
&lt;li&gt;virtual_rooms&lt;/li&gt;
&lt;li&gt;visual_popups&lt;/li&gt;
&lt;li&gt;github_repository&lt;br&gt;
This makes it possible to compare which concepts attract attention and which destinations visitors choose next.&lt;br&gt;
Build With Us&lt;br&gt;
MyZubster is being developed openly. Developers can inspect the implementation, review the architecture, propose improvements and contribute code.&lt;br&gt;
💻 &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=github_repository&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=github_repository&lt;/a&gt;&lt;br&gt;
The project remains an evolving prototype. Experimental functionality, verified evidence and operational services are kept clearly distinguishable.&lt;br&gt;
The objective is not simply to create another marketplace or virtual environment. It is to build a connected system in which software helps communities discover opportunities, learn, exchange value, extend product lifecycles and create new contributions.&lt;br&gt;
Explore the Ecosystem&lt;br&gt;
🛒 Marketplace Demo&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/marketplace?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=marketplace_overview&quot;&gt;https://www.myzubster.com/marketplace?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=marketplace_overview&lt;/a&gt;&lt;br&gt;
🪐 MyZubster Metaverse&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/metaverse?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=metaverse_entry&quot;&gt;https://www.myzubster.com/metaverse?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=metaverse_entry&lt;/a&gt;&lt;br&gt;
🎨 MyZubster Visual&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/fumetto?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=visual_popups&quot;&gt;https://www.myzubster.com/fumetto?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=visual_popups&lt;/a&gt;&lt;br&gt;
💻 Open-source Repository&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=github_repository&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=myzubster_circular_funnel&amp;amp;utm_content=github_repository&lt;/a&gt;&lt;h2 id=&quot;myzubster-opensource-webdevelopment-metaverse-marketplace-circulareconomy-reactjs-nodejs-mongodb-buildinpublic&quot;&gt;MyZubster #OpenSource #WebDevelopment #Metaverse #Marketplace #CircularEconomy #ReactJS #NodeJS #MongoDB #BuildInPublic&lt;/h2&gt;
&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27164/building-myzubster-a-circular-marketplace-connected-to-a-social-metaverse</guid>
<pubDate>Mon, 14 Sep 2026 12:43:38 +0000</pubDate>
</item>
<item>
<title>Building MyZubster Marketplace Demo v2: A Marketplace You Can Explore Without Fake Transactions</title>
<link>https://coderlegion.com/27069/building-myzubster-marketplace-demo-marketplace-you-explore-without-fake-transactions</link>
<description>&lt;p&gt;Building MyZubster Marketplace Demo v2: A Marketplace You Can Explore Without Fake Transactions&lt;/p&gt;
&lt;p&gt;I’ve been working on the next evolution of MyZubster Marketplace, and this update is less about adding another button and more about making the Marketplace something people can actually explore and understand safely.&lt;/p&gt;
&lt;p&gt;The result is Marketplace Demo v2.&lt;/p&gt;
&lt;p&gt;From 6/12 to 12/12 categories&lt;/p&gt;
&lt;p&gt;The Marketplace already supported 12 categories, but the integrated demo experience originally covered only six.&lt;/p&gt;
&lt;p&gt;We completed the missing categories, bringing demo coverage to 12/12:&lt;/p&gt;
&lt;p&gt;🌱 Seeds &amp;amp; Plants&lt;br&gt;
🥕 Produce&lt;br&gt;
👕 Clothing&lt;br&gt;
🎒 Accessories&lt;br&gt;
🎪 Event Equipment&lt;br&gt;
🛠️ Tools&lt;br&gt;
💼 Services&lt;br&gt;
🤝 Volunteering&lt;br&gt;
🐶 Pet Adoption&lt;br&gt;
🔎 Pet Lost &amp;amp; Found&lt;br&gt;
🐾 Pet Services&lt;/p&gt;
&lt;p&gt;We also added automated regression coverage: if we introduce a new Marketplace category in the future without providing demo content for it, the tests can catch it.&lt;/p&gt;
&lt;p&gt;But 12 cards aren't a marketplace experience.&lt;/p&gt;
&lt;p&gt;So we went further.&lt;/p&gt;
&lt;p&gt;Marketplace Demo v2 is interactive&lt;/p&gt;
&lt;p&gt;Demo listings can now be opened and explored in more detail.&lt;/p&gt;
&lt;p&gt;Instead of a static showcase, users can understand things like the listing's description, location, availability, category, exchange/payment mode and category-specific information.&lt;/p&gt;
&lt;p&gt;We also introduced filtering across the demo content:&lt;/p&gt;
&lt;p&gt;Category · Location · Search · FREE · BARTER · EUR · MYZ&lt;/p&gt;
&lt;p&gt;This makes the demo behave much more like the real discovery experience we're building.&lt;/p&gt;
&lt;p&gt;A request flow — without creating fake business&lt;/p&gt;
&lt;p&gt;One of the most important additions is the simulated request flow.&lt;/p&gt;
&lt;p&gt;A visitor can open a demo listing and try the journey from listing discovery to reviewing and confirming a request.&lt;/p&gt;
&lt;p&gt;But there is a strict boundary:&lt;/p&gt;
&lt;p&gt;Demo requests never become real transactions.&lt;/p&gt;
&lt;p&gt;No real order is created.&lt;/p&gt;
&lt;p&gt;No payment is processed.&lt;/p&gt;
&lt;p&gt;No real seller receives a message.&lt;/p&gt;
&lt;p&gt;No personal request data needs to be persisted.&lt;/p&gt;
&lt;p&gt;This gives us a useful product demo without filling the production database with fake economic activity.&lt;/p&gt;
&lt;p&gt;A pet adoption isn't the same thing as selling a product&lt;/p&gt;
&lt;p&gt;This became particularly important when expanding the categories.&lt;/p&gt;
&lt;p&gt;Marketplaces often make the mistake of forcing everything into the same product → price → checkout model.&lt;/p&gt;
&lt;p&gt;That doesn't work for MyZubster.&lt;/p&gt;
&lt;p&gt;A volunteering opportunity isn't a product.&lt;/p&gt;
&lt;p&gt;A lost pet alert isn't a product.&lt;/p&gt;
&lt;p&gt;Pet adoption shouldn't behave like an e-commerce checkout.&lt;/p&gt;
&lt;p&gt;So Demo v2 introduces specialized contextual information for volunteering, pet adoption, lost &amp;amp; found and pet services.&lt;/p&gt;
&lt;p&gt;The category isn't just a label anymore — it can influence how an opportunity is presented.&lt;/p&gt;
&lt;p&gt;One Marketplace demo, not two&lt;/p&gt;
&lt;p&gt;We previously had a separate /marketplace-demos showcase alongside the actual React Marketplace.&lt;/p&gt;
&lt;p&gt;That was useful early in development, but maintaining two different demo experiences creates duplication.&lt;/p&gt;
&lt;p&gt;We've now consolidated them.&lt;/p&gt;
&lt;p&gt;The old demo route points toward the main Marketplace demo experience.&lt;/p&gt;
&lt;p&gt;One Marketplace. One demo model. One source of truth.&lt;/p&gt;
&lt;p&gt;Payments are getting the same treatment&lt;/p&gt;
&lt;p&gt;At the same time, we've been cleaning up MyZubster's payment architecture.&lt;/p&gt;
&lt;p&gt;One interesting production case involved a €9.90/month Stripe subscription.&lt;/p&gt;
&lt;p&gt;Zorgax showed the user as Free.&lt;/p&gt;
&lt;p&gt;Initially, that looked like a billing bug.&lt;/p&gt;
&lt;p&gt;It wasn't.&lt;/p&gt;
&lt;p&gt;The subscription was for Marketplace Seller, not Zorgax Pro.&lt;/p&gt;
&lt;p&gt;Same price.&lt;br&gt;
Same account.&lt;br&gt;
Different product.&lt;/p&gt;
&lt;p&gt;Instead of artificially granting Zorgax Pro, we separated the two states properly:&lt;/p&gt;
&lt;p&gt;Zorgax: Free&lt;/p&gt;
&lt;p&gt;Marketplace: Seller active&lt;/p&gt;
&lt;p&gt;That's an architectural principle we're carrying into the Marketplace:&lt;/p&gt;
&lt;p&gt;A payment tells us that money moved. It doesn't tell us what the user owns.&lt;/p&gt;
&lt;p&gt;The product, purchase and resulting entitlement need to remain explicit.&lt;/p&gt;
&lt;p&gt;Where MyZubster Marketplace is now&lt;/p&gt;
&lt;p&gt;We now have 12/12 demo category coverage, interactive listing details, search and filtering, multiple exchange modes, specialized community categories, a simulated request journey and automated regression coverage.&lt;/p&gt;
&lt;p&gt;And most importantly:&lt;/p&gt;
&lt;p&gt;Real transactions stay real.&lt;/p&gt;
&lt;p&gt;Demo transactions stay simulated.&lt;/p&gt;
&lt;p&gt;Seller subscriptions stay separate from Zorgax subscriptions.&lt;/p&gt;
&lt;p&gt;There's still plenty to build, but the Marketplace is starting to behave less like a collection of screens and more like a coherent system.&lt;/p&gt;
&lt;p&gt;That's the part of development I enjoy most: not just making features appear, but figuring out how they should interact without creating problems later.&lt;/p&gt;
&lt;p&gt;Building MyZubster, one layer at a time.&lt;/p&gt;
&lt;h2 id=&quot;myzubster-coderlegion-webdevelopment-react-nodejs-javascript-marketplace-stripe-buildinpublic-indiedev&quot;&gt;MyZubster #CoderLegion #WebDevelopment #React #NodeJS #JavaScript #Marketplace #Stripe #BuildInPublic #IndieDev&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27069/building-myzubster-marketplace-demo-marketplace-you-explore-without-fake-transactions</guid>
<pubDate>Sun, 13 Sep 2026 16:25:17 +0000</pubDate>
</item>
</channel>
</rss>