<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Akash Pal</title>
    <description>The latest articles on DEV Community by Akash Pal (@akashpal).</description>
    <link>https://dev.to/akashpal</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F141600%2F85e73546-83c9-443b-b965-725add6daea2.png</url>
      <title>DEV Community: Akash Pal</title>
      <link>https://dev.to/akashpal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akashpal"/>
    <language>en</language>
    <item>
      <title>Ownership, and Making This Template Your Own (Part 5)</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Sun, 30 Aug 2026 18:46:01 +0000</pubDate>
      <link>https://dev.to/akashpal/ownership-and-making-this-template-your-own-part-5-1m5l</link>
      <guid>https://dev.to/akashpal/ownership-and-making-this-template-your-own-part-5-1m5l</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 4 covered how this platform actually ships — scaffolding, CI/CD, and the two deployment shapes. This closing part is the two things every one of the last four parts has assumed: who actually owns each piece of this, and what it takes to make this whole template yours.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Who owns what
&lt;/h2&gt;

&lt;p&gt;Every piece of this platform belongs to exactly one team, and that split is what makes independent deploys survive contact with a real organization, not just a single-team demo:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Owned by&lt;/th&gt;
&lt;th&gt;Depends on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Host / Shell&lt;/td&gt;
&lt;td&gt;Platform team&lt;/td&gt;
&lt;td&gt;Store, Components, the manifest, the identity provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Components MFE&lt;/td&gt;
&lt;td&gt;Platform / design-systems team&lt;/td&gt;
&lt;td&gt;Nothing (a leaf)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Store MFE&lt;/td&gt;
&lt;td&gt;Platform team&lt;/td&gt;
&lt;td&gt;The identity provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Utilities MFE&lt;/td&gt;
&lt;td&gt;Platform team&lt;/td&gt;
&lt;td&gt;Nothing (a leaf)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain MFE (×N)&lt;/td&gt;
&lt;td&gt;Domain team&lt;/td&gt;
&lt;td&gt;Components, Store, Utilities only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manifest Registry&lt;/td&gt;
&lt;td&gt;Platform team&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity provider(s)&lt;/td&gt;
&lt;td&gt;Outside the platform — whichever the deployment configures&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend / BFF&lt;/td&gt;
&lt;td&gt;Domain team, or a shared gateway (Part 2)&lt;/td&gt;
&lt;td&gt;Each team's own data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The rule underneath the table:&lt;/strong&gt; domain teams never import from each other, only from the shared platform layer. That keeps the dependency graph a strict two-level tree — Host → platform layer → domain leaves — instead of a mesh, which is what keeps independent deployability tractable once there's more than a handful of domain teams. It's the same rule that made every part of this series possible to write in isolation: Part 3's auth flow doesn't need to know Part 4's deploy pipeline exists, and neither needs to know how many domain teams there eventually are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making this template your own
&lt;/h2&gt;

&lt;p&gt;Everything organization-specific in this platform — branding, which identity provider(s) it trusts, where the manifest lives — has lived in one file across this entire series, on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;platform.config.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orgName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acme-corp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"branding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"primaryColor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#0B5FFF"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"logoUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"idp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"issuers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"primary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://issuer.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"clientId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"issuerResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifestUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cdn.acme.com/platform.manifest.json"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Host, the three platform MFEs, and the CI/CD pipeline ship as a GitHub &lt;strong&gt;template repository&lt;/strong&gt;, not a library to install. Adopting it for a new org, a different business unit, or an entirely different kind of product — internal tool, customer-facing product, partner portal, or any mix — is meant to be: fork → edit &lt;code&gt;platform.config.json&lt;/code&gt; → point DNS and CDN at your own infrastructure → done. No fork-and-diverge of Host's or Store's own logic required, which is exactly what keeps a fork upgradable from the upstream template later instead of quietly drifting into its own incompatible thing.&lt;/p&gt;

&lt;p&gt;That's what the versioned contracts from Part 2 — &lt;code&gt;StoreContract&lt;/code&gt;, &lt;code&gt;DomainMFEExport&lt;/code&gt;, the manifest schema — are actually for, beyond just keeping one deployment internally consistent: they're the seam a fork can pull upstream changes across without breaking whatever a domain team already built on top of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this series leaves you
&lt;/h2&gt;

&lt;p&gt;Five parts: the decision behind Module Federation and why the two credible alternatives lost (Part 1), the topology it produces — Host, the three platform MFEs, and the domain-MFE contract they all render into (Part 2), the login flow that unifies every mounted page regardless of which identity provider issued it (Part 3), and how a new domain team actually scaffolds, builds, and ships through a real CI/CD pipeline (Part 4). This part is the frame around all four: who owns which piece, and how the whole thing is meant to be forked, not just run as-is.&lt;/p&gt;

&lt;p&gt;The code for all five parts — real, and the actual source every snippet in this series was pulled from — is on GitHub: &lt;a href="https://github.com/akash-pal/enterprise-mfe-platform" rel="noopener noreferrer"&gt;github.com/akash-pal/enterprise-mfe-platform&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>opensource</category>
      <category>github</category>
      <category>microfrontends</category>
    </item>
    <item>
      <title>Shipping It — Scaffolding &amp; CI/CD (Part 4)</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Sun, 30 Aug 2026 18:46:00 +0000</pubDate>
      <link>https://dev.to/akashpal/shipping-it-scaffolding-cicd-part-4-54g4</link>
      <guid>https://dev.to/akashpal/shipping-it-scaffolding-cicd-part-4-54g4</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 3 covered how login is unified across every mounted page. This part is how the platform actually ships: scaffolding a new domain team by leveraging the domain template, where the resulting bundles actually live, and the CI/CD pipeline that carries a team's page from a pull request to production.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Decisions locked before writing code
&lt;/h2&gt;

&lt;p&gt;This platform resolves five open questions up front, specifically so they don't cause churn mid-scaffolding:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Options on the table&lt;/th&gt;
&lt;th&gt;What was chosen&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monorepo tool&lt;/td&gt;
&lt;td&gt;Nx vs Turborepo vs polyrepo&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Turborepo&lt;/strong&gt; — lighter config, sufficient for this repo's size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Issuer topology&lt;/td&gt;
&lt;td&gt;Single issuer vs multi-issuer-ready from day one&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Multi-issuer-ready&lt;/strong&gt; (config array) — cheap now, expensive to retrofit (Part 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Issuer resolution mechanism&lt;/td&gt;
&lt;td&gt;default / email-domain / subdomain / selector&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;default&lt;/code&gt;&lt;/strong&gt; ships built; the other three are documented config swaps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend topology&lt;/td&gt;
&lt;td&gt;Single shared gateway vs per-team backends&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Left to whoever adopts the platform&lt;/strong&gt; — the template doesn't take a position&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rendering&lt;/td&gt;
&lt;td&gt;Client-rendered only vs Next.js Multi-Zones vs edge rendering&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Client-rendered only, for now&lt;/strong&gt; — matches Part 1's default choice; the Host and CI/CD are the documented swap points if that changes later&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Scaffolding a new domain team
&lt;/h2&gt;

&lt;p&gt;The goal from Part 2 — a new team scaffolds, builds, and deploys an authenticated page in under an hour — isn't aspirational; it's something worth actually timing, with someone unfamiliar with the repo. The mechanism for getting there is leveraging a template: &lt;code&gt;apps/domain-template&lt;/code&gt;, a folder to copy, rather than a bespoke CLI — chosen deliberately as the cheaper thing to maintain at this stage (an &lt;code&gt;npx create-org-mfe &amp;lt;name&amp;gt;&lt;/code&gt; CLI is a natural upgrade path later, once the copy-paste steps below are well-worn enough to automate):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# demo/README.md&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Copy apps/domain-template to demo/&lt;span class="nt"&gt;&amp;lt;your-domain&amp;gt;&lt;/span&gt;.
&lt;span class="p"&gt;2.&lt;/span&gt; Rename the package in package.json and update webpack.common.js's
   federation.name to match. Give it its own dev port.
&lt;span class="p"&gt;3.&lt;/span&gt; Replace DomainApp.tsx with your own UI; keep exporting DomainMFEExport
   ({ routes, Component }) from src/index.tsx.
&lt;span class="p"&gt;4.&lt;/span&gt; Add an entry to platform.manifest.dev.json with the new remote's
   remoteEntry URL and route — no Host rebuild required.
&lt;span class="p"&gt;5.&lt;/span&gt; Add a dev line for it to the root package.json's dev script.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;apps/domain-template&lt;/code&gt; stays deliberately minimal — auth and shared-state wiring only, no business content — precisely so it stays a clean copy source every new team starts from. &lt;code&gt;demo/orders&lt;/code&gt; is the proof this actually works: &lt;code&gt;domain-template&lt;/code&gt; copied, renamed, filled in with real (if sample) content, running on its own port with its own federation name, wired in by one manifest entry. Host's own &lt;code&gt;remotes: {}&lt;/code&gt; from Part 1 never had to know &lt;code&gt;orders&lt;/code&gt; existed until it was already running in a browser tab — and the exact same copy-and-rename steps are how a third, fourth, or fiftieth domain team gets a page running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two deployment shapes, one running Host
&lt;/h2&gt;

&lt;p&gt;Host, Components, Store, and Utilities don't deploy the same way a domain team's page does — and that difference isn't incidental, it's the same one Part 1 built the whole platform around:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr6vmn3ri7eso7vroupm2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr6vmn3ri7eso7vroupm2.png" alt="Two deployment shapes converge on the running Host: platform bundles are npm-installed and version-pinned at build time, then ship together as one release that rebuilds Host. Domain bundles are uploaded to their own versioned CDN path and are only ever discovered through a manifest entry, fetched fresh at runtime, so Host never rebuilds to pick one up." width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The left path is ordinary dependency management: Host's &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;webpack.common.js&lt;/code&gt; declare which version of Components, Store, and Utilities it expects, npm installs them, and shipping a change to any of the four means rebuilding and redeploying Host itself — the four are versioned and released together. The right path is the one this whole series keeps coming back to: a domain team's bundle is uploaded to its own versioned path, and the &lt;em&gt;only&lt;/em&gt; thing that connects it to Host is one entry in the manifest, fetched fresh at runtime. Same running application, two genuinely different deployment mechanisms feeding it — which is exactly why scaffolding a domain page (above) never touches Host's own build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CI/CD pipeline
&lt;/h2&gt;

&lt;p&gt;Three stages, run by GitHub Actions: build and test the code into a deployable artifact, promote that artifact to dev automatically on every merge to main, then promote the same artifact to production once someone approves it. In the workflow file, that's &lt;code&gt;build-test → deploy-dev → promote-prod&lt;/code&gt;, with &lt;code&gt;promote-prod&lt;/code&gt; gated behind a GitHub Environment with required reviewers — no custom approval code needed, just a platform feature configured correctly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F02s6kwiscd47ll8srw1f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F02s6kwiscd47ll8srw1f.png" alt="The CI/CD pipeline: a pull request or push to main runs build-test — lint, typecheck, build, test, and the shared-dependency contract check. A pull request stops at that gate. A push to main continues to deploy-dev, which builds the bundle, uploads it, and calls update-manifest.js to point the dev manifest at it. Promotion to production waits for a human reviewer's approval, then repeats the same upload-and-update-manifest step against production." width="799" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the same three stages as the actual workflow file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/mfe-ci-cd.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run lint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run typecheck&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run test&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run test:scripts&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify shared-dependency contracts&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run check:contract&lt;/span&gt;

  &lt;span class="na"&gt;deploy-dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-test&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main' &amp;amp;&amp;amp; github.event_name == 'push'&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload build output to dev hosting&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "TODO -- upload apps/*/dist and demo/*/dist-remote to your dev CDN"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Point the dev manifest at the new build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;npm run manifest:update -- \&lt;/span&gt;
            &lt;span class="s"&gt;--route /orders --name orders \&lt;/span&gt;
            &lt;span class="s"&gt;--version "$(node -p "require('./demo/orders/package.json').version")" \&lt;/span&gt;
            &lt;span class="s"&gt;--remote-entry "${{ vars.DEV_CDN_BASE_URL }}/orders/remoteEntry.js"&lt;/span&gt;

  &lt;span class="na"&gt;promote-prod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy-dev&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'workflow_dispatch' &amp;amp;&amp;amp; github.event.inputs.environment == 'prod'&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# same shape as deploy-dev, against PROD_* vars&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; the "Point the dev manifest at the new build" step is what actually deploys a domain MFE. The Host isn't rebuilt — it just reads a new &lt;code&gt;remoteEntry&lt;/code&gt; URL out of the manifest on its next load.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating the manifest registry is the actual deploy step, so it has to be safe under concurrency
&lt;/h3&gt;

&lt;p&gt;Say that plainly: updating &lt;code&gt;platform.manifest.json&lt;/code&gt; &lt;strong&gt;is&lt;/strong&gt; deploying a domain MFE — not part of deploying it. Upload a beautiful new &lt;code&gt;remoteEntry.js&lt;/code&gt; to a CDN and never touch the manifest, and Host keeps loading the old one forever, with no error, because nothing told it anything changed. So the deploy script's whole job is updating one JSON array safely when more than one CI run might be doing it at the same time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/update-manifest.js&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;upsertManifestEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifestPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lockPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;acquireLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifestPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readManifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifestPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nf"&gt;validateEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeManifestAtomic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifestPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextManifest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;nextEntry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;releaseLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lockPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two properties earn their keep: &lt;strong&gt;locking&lt;/strong&gt; — an exclusive-create lockfile with retry/backoff — because if two domain teams' pipelines finish around the same moment (the normal case once there's more than one team, not an edge case), a naive read-modify-write is a lost-update race waiting to happen. &lt;strong&gt;Atomic write&lt;/strong&gt; — write to a temp file, then rename over the real one — because a same-directory rename is atomic on POSIX filesystems, so a crash mid-write can never leave the manifest half-written; the real file has either the old complete content or the new complete content, never something in between. A test proves the property that actually matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;serializes concurrent updates to different routes — neither update is lost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;manifestPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;withTempManifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;upsertManifestEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifestPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;upsertManifestEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifestPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;upsertManifestEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifestPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/billing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readManifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifestPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deepEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/billing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three "deploys" fired at the exact same instant, on purpose — exactly what happens the moment the pipeline generalizes past one domain MFE, which a GitHub Actions matrix would drive in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's real vs. what's still a placeholder, stated honestly:&lt;/strong&gt; locking and atomicity, the shared-contract check, and the workflow's job graph and gating are real and tested. The CDN upload steps are explicit &lt;code&gt;echo "TODO"&lt;/code&gt; placeholders — deliberately, since this pipeline is written target-agnostic (no CDN or cloud provider chosen), and canary-rollout percentages and automatic rollback on regression can't be built honestly without a real target to build against; a rollback script that's never watched real error-rate metrics isn't tested, it's decorative.&lt;/p&gt;

&lt;p&gt;Each environment — dev/staging/prod — gets an isolated manifest and its own &lt;code&gt;platform.config.json&lt;/code&gt; issuer configuration, so a bad manifest update, or a bad auth config change, can never cross environments. Nothing above names a specific cloud on purpose: the mechanism — build once, upload, point the manifest at it — is what this template actually commits to, and it maps onto whichever object storage, CDN, and CI runner an adopter already runs.&lt;/p&gt;

&lt;p&gt;The last part closes the series: who actually owns each piece of this, and what it takes to make this whole template yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next: &lt;a href="//./05-ownership-and-reuse.md"&gt;Part 5 — Ownership, and Making This Template Your Own&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>github</category>
      <category>architecture</category>
    </item>
    <item>
      <title>One Login, Every Microfrontend — The Auth Architecture (Part 3)</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Sun, 30 Aug 2026 18:45:58 +0000</pubDate>
      <link>https://dev.to/akashpal/one-login-every-microfrontend-the-auth-architecture-part-3-4oon</link>
      <guid>https://dev.to/akashpal/one-login-every-microfrontend-the-auth-architecture-part-3-4oon</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 2 covered the platform's topology and the three shared microfrontends every domain team builds against. This part is authentication — the piece every other part of the platform depends on, which is why it's built early rather than left until the end.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A few terms, defined once
&lt;/h2&gt;

&lt;p&gt;This part leans on some standard identity terms — worth pinning down before diving in, since they build on each other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IdP (Identity Provider)&lt;/strong&gt; — the actual service that owns usernames, passwords, and roles, and does the real work of checking whether a login is valid. Okta, Google, Microsoft Entra ID, Auth0, or a company's own login system are all examples. Host never becomes one of these itself — it always hands the real login off to one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth 2.0&lt;/strong&gt; — an industry-standard way for one app (Host) to get limited, provable confirmation from an IdP that a user is logged in, without Host ever seeing or handling that user's password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OIDC (OpenID Connect)&lt;/strong&gt; — OAuth 2.0, plus a standard way to also learn &lt;em&gt;who&lt;/em&gt; that user is (their id, their roles), not just "yes, they logged in." This platform speaks OIDC specifically because it needs that identity information, not just a bare confirmation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issuer&lt;/strong&gt; — the specific IdP a deployment has decided to trust. In practice, just a URL Host redirects the browser to, and later asks "did this login really happen?" A deployment can trust one issuer, or several — more on why below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With those in hand, here's what actually happens when someone logs in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Population-agnostic by design
&lt;/h2&gt;

&lt;p&gt;This platform is explicit about scope here: the Host and Store MFE don't model "employees" vs. "partners" vs. "customers" as distinct concepts. They know one thing — a standard OIDC contract. &lt;em&gt;Which&lt;/em&gt; population authenticates, and &lt;em&gt;which&lt;/em&gt; provider issues their tokens, is a deployment-time config decision, not an architectural one. That's what lets the identical template back an internal admin tool, a customer-facing product, or a B2B partner portal with zero Host or Store code changes between them — the choice of IdP (Okta, Microsoft Entra ID, Auth0, Keycloak, Cognito, or a homegrown one) is explicitly out of scope for the platform itself.&lt;/p&gt;

&lt;p&gt;One rule follows from this everywhere in the codebase: &lt;strong&gt;a domain MFE never triggers login, logout, or a token exchange.&lt;/strong&gt; It reads the &lt;em&gt;result&lt;/em&gt; of auth (&lt;code&gt;useAuth()&lt;/code&gt;, the federated singleton from Part 2) but never causes an auth &lt;em&gt;event&lt;/em&gt;. Host owns the entire flow, alone, end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow: Authorization Code + PKCE
&lt;/h2&gt;

&lt;p&gt;Host never sees a password. It redirects the browser to the IdP, the IdP authenticates the user, and it redirects back with proof. That handoff is called the &lt;strong&gt;Authorization Code flow with PKCE&lt;/strong&gt; ("pixy" — Proof Key for Code Exchange), and it's a specific sequence of redirects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Host generates a random secret (the code verifier), hashes it into a code challenge, and sends the browser to the IdP's login page with that challenge.&lt;/li&gt;
&lt;li&gt;The user authenticates at the IdP — whatever that means for the real IdP in use.&lt;/li&gt;
&lt;li&gt;The IdP redirects back to Host's &lt;code&gt;/callback&lt;/code&gt; with a short-lived authorization code.&lt;/li&gt;
&lt;li&gt;Host exchanges that code — with the &lt;em&gt;original&lt;/em&gt;, un-hashed verifier — for the actual tokens.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why the extra verifier/challenge step, specifically?&lt;/strong&gt; A traditional server-side app can hold a permanent secret of its own — proof that "I'm really the app I claim to be" — when it trades a login code for real tokens. Host can't do that safely: it's JavaScript running inside the user's own browser, and anything shipped to a browser can be read by anyone who opens dev tools. PKCE solves this without needing any permanent secret at all. Right before redirecting to the IdP, Host invents a random, one-time value (the verifier) just for this one login attempt, and sends the IdP a scrambled version of it (the challenge). When Host later comes back to redeem the authorization code for real tokens, it presents the original, un-scrambled verifier. Only whoever actually started this specific login attempt could have that value — so the IdP can trust the exchange, without either side ever storing a password-like secret.&lt;/p&gt;

&lt;p&gt;Here's that exact handshake as a diagram — steps 1 through 4 above, continued through to what Host does once it actually has a session:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhvsrnh3jtghg4p6romf2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhvsrnh3jtghg4p6romf2.png" alt="Sequence diagram: the Host normalizes tokens from the IdP into one shape, then on navigation resolves the route against the Manifest Registry. If the route is authorized, the Host fetches the domain bundle from the CDN and mounts it into the shared singleton scope. If not authorized, the Host renders a 403 fallback and the CDN is never contacted." width="799" height="631"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here's step 1 as real code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/runtime-module-federation/src/oidc-client.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;redirectToAuthorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issuerConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IssuerConfig&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;discovery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;discover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issuerConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateCodeVerifier&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;challenge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateCodeChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PKCE_VERIFIER_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STATE_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;discovery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization_endpoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;response_type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;issuerConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redirect_uri&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;redirectUri&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scope&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code_challenge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code_challenge_method&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;S256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;state&lt;/code&gt; value is a separate, second safeguard — it guards against CSRF (a class of attack where a malicious site tries to trick a browser into completing part of this handshake without the user actually intending it). The callback rejects any response whose &lt;code&gt;state&lt;/code&gt; doesn't match what was stored right before redirecting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/runtime-module-federation/src/oidc-client.ts&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;returnedState&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;returnedState&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;expectedState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;State mismatch on callback — possible CSRF, aborting token exchange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One or more issuers — a config array from line one
&lt;/h2&gt;

&lt;p&gt;Recall an issuer is just the specific IdP a deployment trusts. &lt;code&gt;platform.config.json&lt;/code&gt; lists every issuer a deployment trusts, as an array. A deployment serving one population declares one issuer; a deployment serving many tenants might declare several, or point at a single identity provider that itself manages logins for many upstream organizations — Host's own code doesn't need to change either way, only the config.&lt;/p&gt;

&lt;p&gt;This platform makes a specific, deliberate choice here: &lt;strong&gt;multi-issuer support is a config array from the first line of code&lt;/strong&gt;, even with only one issuer configured — retrofitting an array shape onto a single-issuer assumption later would touch every piece of code that reads a user's claims.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;platform.config.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orgName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acme-demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"branding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"primaryColor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#2F5CE0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"logoUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/logo.svg"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"idp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"issuers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"local-dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:9000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"clientId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"host-dev-client"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"issuerResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifestUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/platform.manifest.json"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;issuerResolution&lt;/code&gt; — &lt;code&gt;default&lt;/code&gt; / &lt;code&gt;email-domain&lt;/code&gt; / &lt;code&gt;subdomain&lt;/code&gt; / &lt;code&gt;selector&lt;/code&gt; — decides &lt;em&gt;which&lt;/em&gt; issuer applies when more than one is configured. Only &lt;code&gt;default&lt;/code&gt; ships built with a UI; the other three are documented config swaps, not something built upfront on spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  One normalized session, regardless of which IdP produced it
&lt;/h2&gt;

&lt;p&gt;Here's the problem this solves: two different IdPs describing the exact same logged-in user are free to shape that description differently — some send &lt;code&gt;roles&lt;/code&gt; as an array, some send a single &lt;code&gt;role&lt;/code&gt;; some send &lt;code&gt;org_id&lt;/code&gt;, some &lt;code&gt;orgId&lt;/code&gt;. Without a fix, every single piece of downstream code — every domain team's page, every route guard — would need to know which specific IdP produced whatever token it's looking at, forever. Instead, one function collapses whatever comes back into one fixed shape, immediately after the token exchange:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/runtime-module-federation/src/normalize.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeTokenResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RawTokenResponse&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;AuthState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id_token&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id_token&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;claimsSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;idToken&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;decodeJwtPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claimsSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;claimsSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;claimsSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;claimsSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orgId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;claimsSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;org_id&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;claimsSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;idToken&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;orgId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;orgId&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claimsSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expires_in&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything downstream — every domain MFE, every route guard, &lt;code&gt;StoreContract&lt;/code&gt; itself — reads only this normalized &lt;code&gt;AuthState&lt;/code&gt; shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/types/src/auth.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AuthState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// kept specifically as id_token_hint for RP-Initiated Logout, below&lt;/span&gt;
  &lt;span class="nl"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AuthClaims&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire payoff: every team writes its authorization checks once, against one shape, and never needs to know or care which real IdP a given deployment actually uses. It's also the seam worth its own unit tests — asserting the normalized shape stays identical across at least two mock issuer response formats — since swapping one IdP for another later should be a change contained entirely to this one function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session persistence and cross-tab logout
&lt;/h2&gt;

&lt;p&gt;The resulting &lt;code&gt;AuthState&lt;/code&gt; is handed to the Store MFE's &lt;code&gt;AuthProvider&lt;/code&gt; from Part 2 — every domain MFE reading &lt;code&gt;useAuth()&lt;/code&gt; gets &lt;em&gt;this exact&lt;/em&gt; session object. It's also deliberately persisted to &lt;code&gt;sessionStorage&lt;/code&gt;, hydrated back on mount, so a page reload doesn't force a full round trip back to the IdP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/store-mfe/src/auth/AuthContext.tsx&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadPersistedSession&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;AuthState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SESSION_STORAGE_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;EMPTY_AUTH_STATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;EMPTY_AUTH_STATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;EMPTY_AUTH_STATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sessionStorage&lt;/code&gt;, deliberately, not &lt;code&gt;localStorage&lt;/code&gt; — a session shouldn't outlive the browser tab it belongs to. Cross-tab logout is handled separately, via &lt;code&gt;BroadcastChannel('auth-events')&lt;/code&gt;: when one tab logs out, every other open tab hears a &lt;code&gt;LOGOUT&lt;/code&gt; message, clears its own session, and — because that clear runs through the same state-setting path — the persisted &lt;code&gt;sessionStorage&lt;/code&gt; copy gets wiped too, in every tab, automatically. That message is &lt;code&gt;{ type: 'LOGOUT' | 'TOKEN_REFRESHED' }&lt;/code&gt;, and it's treated as a must-have from day one, not a nice-to-have: a domain MFE silently holding a stale session after a Host logout is the kind of bug that's invisible until a real incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logout means telling the IdP, not just forgetting locally
&lt;/h2&gt;

&lt;p&gt;This is easy to get half-right: "logging out" has to mean more than clearing Host's own memory. If Host only resets its own state, the IdP's own session cookie is untouched — the very next login attempt can silently re-authenticate against that still-live IdP session without ever showing a sign-in prompt. That's single sign-on working exactly as designed; it's just not "log out" if only the local half of the relationship gets torn down.&lt;/p&gt;

&lt;p&gt;The correct mechanism is &lt;strong&gt;RP-Initiated Logout&lt;/strong&gt; — a named part of the OIDC spec — redirecting the browser to the IdP's &lt;code&gt;end_session_endpoint&lt;/code&gt; so the IdP itself ends the session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/runtime-module-federation/src/oidc-client.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;redirectToEndSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;issuerConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IssuerConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;idTokenHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;discovery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;discover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issuerConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;discovery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end_session_endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;postLogoutRedirectUri&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;discovery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end_session_endpoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idTokenHint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id_token_hint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idTokenHint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;issuerConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post_logout_redirect_uri&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;postLogoutRedirectUri&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;id_token_hint&lt;/code&gt; is the raw ID token from earlier — the IdP uses it to confirm which session it's being asked to end, which is exactly why &lt;code&gt;AuthState&lt;/code&gt; keeps the raw token around instead of discarding it once its claims are decoded. And the IdP has to allow-list &lt;code&gt;post_logout_redirect_uris&lt;/code&gt;, the same way it already allow-lists login redirect URIs, so it knows it's safe to send the browser back afterward. With all three pieces together, logging out genuinely ends the session at the IdP — the next "Log in" click shows a real sign-in prompt, not an instant, invisible re-authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authorization is claims-driven, never issuer-driven
&lt;/h2&gt;

&lt;p&gt;Role/claim data — never &lt;em&gt;which&lt;/em&gt; IdP authenticated the user — drives every enforcement point: route guards in Host's manifest-driven router, conditional rendering inside domain MFEs, and enforcement at the backend layer, which is a hard requirement, not a suggestion — &lt;strong&gt;client-side checks are never sufficient alone.&lt;/strong&gt; This decoupling is what lets the same authorization code work whether a "role" came from a workforce group, a partner-org assignment, or a subscription tier; the platform doesn't distinguish, and doesn't need to. Feature flags layer on top of roles for progressive rollout, sourced from the Store MFE's &lt;code&gt;FeatureFlagProvider&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  From login to a mounted page: the whole sequence
&lt;/h2&gt;

&lt;p&gt;Putting the pieces above in order, this is the complete path from "user opens the app" to "a team's page is on screen" — the diagram earlier in this part covers steps 1 through 6:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User loads the platform → Host checks for a valid in-memory token.&lt;/li&gt;
&lt;li&gt;If absent, redirect to the IdP (Authorization Code + PKCE) → exchange code for tokens.&lt;/li&gt;
&lt;li&gt;Host normalizes the response and initializes the Store MFE's &lt;code&gt;AuthProvider&lt;/code&gt; with the session.&lt;/li&gt;
&lt;li&gt;User navigates to a domain route → Host resolves it against the manifest.&lt;/li&gt;
&lt;li&gt;Host checks the route's required role against the user's claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If authorized:&lt;/strong&gt; fetch the domain's bundle from the CDN, mount it into the shared singleton scope — no re-fetch of React/Store/Components, no duplicate provider instance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If not authorized:&lt;/strong&gt; Host renders a 403 fallback. The CDN is never contacted — the domain's bundle is never fetched at all.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Modeling roles in the local dev IdP
&lt;/h2&gt;

&lt;p&gt;Exercising role-gating locally needs at least two distinct roles without a real IdP account. The local IdP — a thin, dev-only wrapper around the &lt;code&gt;oidc-provider&lt;/code&gt; library, never meant to be deployed — models this with two named accounts and a generic fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tools/mock-idp/src/server.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;NAMED_ACCOUNTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DEMO_ACCOUNT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ADMIN_ACCOUNT&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NAMED_ACCOUNTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;acme-demo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;org_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any login typed into the local sign-in form works: &lt;code&gt;admin-user-1&lt;/code&gt; resolves to the &lt;code&gt;admin&lt;/code&gt; role, &lt;code&gt;demo-user-1&lt;/code&gt; — or literally anything else — resolves to &lt;code&gt;user&lt;/code&gt;. It accepts any password, too. This is exclusively a local stand-in for exercising the flow end to end, never a real identity source.&lt;/p&gt;

&lt;p&gt;Part 4 is how this all actually ships: scaffolding a new domain team from the template, and the CI/CD pipeline that carries a page from a pull request to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next: &lt;a href="//./04-shipping-it.md"&gt;Part 4 — Shipping It: Scaffolding &amp;amp; CI/CD&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>oidc</category>
      <category>react</category>
      <category>security</category>
    </item>
    <item>
      <title>Inside the Platform — Topology, Host Shell &amp; Platform MFEs (Part 2)</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Sun, 30 Aug 2026 18:45:56 +0000</pubDate>
      <link>https://dev.to/akashpal/inside-the-platform-topology-host-shell-platform-mfes-part-2-35a4</link>
      <guid>https://dev.to/akashpal/inside-the-platform-topology-host-shell-platform-mfes-part-2-35a4</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 1 covered why this platform uses Module Federation instead of Next.js Multi-Zones or single-spa. This part covers the platform's design goals, its topology, what the Host shell actually owns, and the three shared microfrontends every domain team builds against — and, at the end, how the Host actually wires all three together.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Design goals, stated as constraints
&lt;/h2&gt;

&lt;p&gt;Four goals shape every decision in this series, not just the composition strategy from Part 1:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each domain team deploys independently, without redeploying the Host.&lt;/li&gt;
&lt;li&gt;One design system, one state/auth source of truth, one utilities layer — no duplication across MFEs.&lt;/li&gt;
&lt;li&gt;A new domain team can scaffold, build, and deploy an authenticated page in under an hour.&lt;/li&gt;
&lt;li&gt;The whole platform is config-driven enough to be forked and re-branded for a different business unit or organization without touching core code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Goal 4 is the one easy to underrate reading this series linearly — it's the subject of the last part. Goals 1–3 are what the rest of this part is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  High-level topology
&lt;/h2&gt;

&lt;p&gt;The Host dynamically loads three shared platform microfrontends as federated singletons, which every domain microfrontend consumes. The Host resolves routes against the manifest at runtime, and a domain's bundle is fetched from the CDN only if the route is authorized:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faay9zyvcy6tjzc2vs6vv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faay9zyvcy6tjzc2vs6vv.png" alt="Platform topology: the Host shell loads Components and Store MFEs two ways — as npm packages at build time and as federated singletons at runtime — while Utilities MFE is npm-only. The Host resolves routes against the Manifest Registry at runtime, and a domain's bundle is fetched from the CDN only if the route is authorized. Domain MFEs call their own backend or BFF, whose topology is left to each adopter." width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few things worth reading directly off that diagram. The platform MFEs sit in one &lt;em&gt;shared singleton scope&lt;/em&gt; — one instance, not one per domain MFE. The CDN is only ever contacted after the manifest has already said a route is authorized. Components and Store each reach the Host two different ways at once — solid line, dashed line — which the next section explains; Utilities only ever takes the dashed one. And below the domain layer, a dashed "Backend / BFF" box marks a boundary on purpose: this platform has an opinion about how a page gets &lt;em&gt;found&lt;/em&gt; and &lt;em&gt;mounted&lt;/em&gt;, and deliberately no opinion at all about where that page gets its &lt;em&gt;data&lt;/em&gt; from — that line is for each domain team to draw themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Host: what it owns, and what it deliberately doesn't
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Responsibilities:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App shell — Header, Side Navigation, Footer, layout regions&lt;/li&gt;
&lt;li&gt;Route registry that resolves domain MFEs &lt;em&gt;dynamically from a manifest&lt;/em&gt;, not hardcoded at build time — this is what makes independent deploys possible&lt;/li&gt;
&lt;li&gt;Auth bootstrap — acquires the session once, hands it to the Store MFE's &lt;code&gt;AuthProvider&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Global error boundary + fallback UI per mounted remote, so one MFE crashing doesn't take the shell down with it&lt;/li&gt;
&lt;li&gt;Theme/design-token injection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it explicitly doesn't own: any domain's business logic, any domain's routes beyond the catch-all that hands off to the manifest, and — per Part 1 — any compiled-in knowledge of which domains exist.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/host/webpack.common.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createModuleFederationConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;federation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;remotes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="c1"&gt;// resolved at runtime — see manifest-loader.ts below&lt;/span&gt;
    &lt;span class="na"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;react&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^18.2.0 || ^19.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^18.2.0 || ^19.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@org/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`^&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;storePkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@org/components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`^&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;componentsPkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the route resolution it hands off to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/runtime-module-federation/src/manifest-loader.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ManifestEntry&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ManifestEntry&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The three platform microfrontends
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Components MFE — the design system
&lt;/h3&gt;

&lt;p&gt;Exposes the shared component library — buttons, form primitives, modals, header/side-nav chrome, a &lt;code&gt;Forbidden&lt;/code&gt; view, and so on — built on design tokens as the actual contract (colors, spacing, typography), so visual consistency survives independent deploys rather than drifting per team.&lt;/p&gt;

&lt;p&gt;The mechanism that makes "one design system" true rather than aspirational is &lt;strong&gt;dual-publishing&lt;/strong&gt;: a real &lt;code&gt;tsc&lt;/code&gt; build producing an npm-installable package, and a separate webpack build producing a &lt;code&gt;remoteEntry.js&lt;/code&gt;, from the same source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/components-mfe/webpack.common.cjs&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createModuleFederationConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/dev-bootstrap.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;outputDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dist-remote&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;federation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;remoteEntry.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;exposes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/index.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;react&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^18.2.0 || ^19.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^18.2.0 || ^19.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@org/components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`^&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;apps/components-mfe/package.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(trimmed)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@org/components"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.d.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc -p tsconfig.json &amp;amp;&amp;amp; node scripts/copy-css.mjs &amp;amp;&amp;amp; webpack build --config webpack.prod.cjs"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tsc&lt;/code&gt; gives compile-time type safety and tree-shaking for anyone importing &lt;code&gt;@org/components&lt;/code&gt; as a normal npm dependency; the webpack build gives runtime singleton sharing for anyone consuming it as a Module Federation remote. Same source, two consumers, and — because it's federated as a singleton — every domain MFE gets the &lt;em&gt;exact same&lt;/em&gt; rendered instance, not N slightly-drifted copies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Store MFE — everything that has to be one shared instance
&lt;/h3&gt;

&lt;p&gt;This is the platform's other non-negotiable singleton. Its public surface, &lt;code&gt;StoreContract&lt;/code&gt;, is one interface covering everything that has to be shared across every mounted page: the login session, state shared for the current tab, state that survives a reload, feature flags, analytics, local-storage-backed state, and logging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/types/src/store.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;StoreContract&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;AuthState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;useGlobalState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nx"&gt;useGlobalStatePersist&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nf"&gt;useFeatureFlag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flagKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;useAnalytics&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;AnalyticsClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;useLocalStorage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nf"&gt;useLogger&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven hooks, one underlying mechanism: each is backed by exactly one React context, created once, that every consumer across the whole platform reads and writes through. &lt;code&gt;useGlobalState&lt;/code&gt; is a good one to look at up close, because it's small enough to show that whole mechanism in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/store-mfe/src/state/GlobalStateContext.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GlobalStateContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GlobalStateContextValue&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * One in-memory map behind one context — every useGlobalState() consumer
 * across the whole federated singleton reads/writes the same object.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GlobalStateProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValues&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StateMap&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setValues&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GlobalStateContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;GlobalStateContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every other hook on &lt;code&gt;StoreContract&lt;/code&gt; follows this same shape — its own context, its own provider, exposed as a hook. What turns seven separate providers into the one thing the Host actually mounts is &lt;code&gt;StoreProvider&lt;/code&gt;, which composes all of them into a single tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/store-mfe/src/StoreProvider.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;StoreProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;FeatureFlag&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AuthProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GlobalStateProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GlobalStatePersistProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FeatureFlagProvider&lt;/span&gt; &lt;span class="na"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AnalyticsProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AnalyticsProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;FeatureFlagProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;GlobalStatePersistProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;GlobalStateProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AuthProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;singleton: true&lt;/code&gt; on &lt;code&gt;@org/store&lt;/code&gt; in every consumer's webpack config, none of this holds — each MFE would mount its own copy of &lt;code&gt;StoreProvider&lt;/code&gt;, and two pages' &lt;code&gt;useGlobalState('cart')&lt;/code&gt; calls would quietly diverge instead of sharing state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One more thing lives inside that same tree: a shared cache for data fetched from a backend.&lt;/strong&gt; &lt;code&gt;StoreProvider&lt;/code&gt; also mounts exactly one &lt;code&gt;QueryClientProvider&lt;/code&gt;, from &lt;a href="https://tanstack.com/query" rel="noopener noreferrer"&gt;TanStack Query&lt;/a&gt; — same idea as everything else on this page: one instance, not one per domain MFE, so if two different pages both fetch the same resource, the second call reads the first one's cache instead of firing a duplicate request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/store-mfe/src/StoreProvider.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;StoreProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;FeatureFlag&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;QueryClientProvider&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AuthProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GlobalStateProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ...the rest, unchanged... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;GlobalStateProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AuthProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;QueryClientProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@org/store&lt;/code&gt; re-exports &lt;code&gt;useQuery&lt;/code&gt;, &lt;code&gt;useMutation&lt;/code&gt;, and &lt;code&gt;useQueryClient&lt;/code&gt; directly — a domain team never installs the library itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/store-mfe/src/index.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useQueryClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is exactly the kind of thing that's easy to get subtly wrong the same way singleton context always is: TanStack Query keeps its cache behind its own React context, so &lt;code&gt;@tanstack/react-query&lt;/code&gt; has to be declared &lt;code&gt;singleton: true&lt;/code&gt; in every consuming app's webpack config too — the same failure mode as &lt;code&gt;@org/store&lt;/code&gt; itself, one layer further down. Here it is actually used, in the &lt;code&gt;orders&lt;/code&gt; domain MFE:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// demo/orders/src/DomainApp.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@org/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchOrders&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3005/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Failed to load orders: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DomainApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;basePath&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;basePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fetchOrders&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// ...renders a loading state, an error state, or the list&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;http://localhost:3005&lt;/code&gt; is &lt;code&gt;tools/mock-bff&lt;/code&gt; — a small dev-only backend, real and running, that this one domain team owns and calls directly. Nothing about that URL is known to Host, Store, or any other domain MFE; it's &lt;code&gt;orders&lt;/code&gt;' own business, the same way its UI is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Utilities MFE — deliberately &lt;em&gt;not&lt;/em&gt; federated
&lt;/h3&gt;

&lt;p&gt;Formatters, validators, generators, convertors — pure, side-effect-free functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/utilities-mfe/src/index.ts&lt;/span&gt;
&lt;span class="c1"&gt;// Pure, side-effect-free, tree-shakeable — npm package only. Federating this&lt;/span&gt;
&lt;span class="c1"&gt;// one is optional; skipped for v1 until a real duplication problem shows up.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./formatters/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./validators/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./generators/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./convertors/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pure functions don't hold state, so there's no correctness reason to dedupe them at runtime — just ship it as a plain npm dependency and skip the Module Federation machinery entirely. This is worth calling out precisely because it cuts against the instinct to federate everything on principle: federate what has to be one instance, and nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it together: how the Host actually uses all three
&lt;/h2&gt;

&lt;p&gt;Components, Store, and Utilities read as three separate pieces above — here's what tying them into the Host actually looks like. The Host's entry point wraps its entire app in exactly &lt;strong&gt;one&lt;/strong&gt; &lt;code&gt;StoreProvider&lt;/code&gt;, mounted once, nowhere else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/host/src/bootstrap.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createRoot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StoreProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@org/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;loadPlatformConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@org/runtime-module-federation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./App&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;platformConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadPlatformConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StoreProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="na"&gt;platformConfig&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;platformConfig&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;StoreProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single &lt;code&gt;&amp;lt;StoreProvider&amp;gt;&lt;/code&gt; is what makes &lt;code&gt;useAuth()&lt;/code&gt; — or any other &lt;code&gt;StoreContract&lt;/code&gt; hook — return the same value no matter which mounted page calls it. Inside &lt;code&gt;App.tsx&lt;/code&gt;, the Host then pulls from all three platform MFEs directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/host/src/App.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useAuth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@org/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;formatDate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@org/utilities&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SideNav&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Footer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HeaderActions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@org/components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;platformConfig&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;platformConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PlatformConfig&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BrowserRouter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;platformConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HeaderActions&lt;/span&gt; &lt;span class="na"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* domain routes resolved from the manifest */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Footer&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;platformConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; — Enterprise MFE Platform`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;BrowserRouter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;useAuth()&lt;/code&gt; reads the session &lt;code&gt;StoreProvider&lt;/code&gt; set up above it. &lt;code&gt;Header&lt;/code&gt;, &lt;code&gt;SideNav&lt;/code&gt;, and &lt;code&gt;Footer&lt;/code&gt; are the Components MFE's design system, rendering the shell chrome from the design goals at the top of this part. &lt;code&gt;formatDate&lt;/code&gt;, elsewhere in the same file, is a plain function import from Utilities — no provider, no context, just a function call, exactly because Utilities isn't a singleton the way Store and Components are. One file, all three platform MFEs, doing exactly what each was described as doing above.&lt;/p&gt;

&lt;p&gt;The next part covers exactly how that session got there in the first place — from a redirect to an identity provider through to the normalized claims &lt;code&gt;useAuth()&lt;/code&gt; returns here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain MFEs — the ×N layer
&lt;/h2&gt;

&lt;p&gt;One per business capability, owned by an independent team. A domain MFE registers itself by adding one entry to the manifest — name, remote URL, route, required role — with no PR against the Host repo required, and it consumes Components/Store/Utilities as shared dependencies, never bundling its own copy of React, the design system, or the state layer.&lt;/p&gt;

&lt;p&gt;What a domain MFE actually hands back to Host is fixed by one small contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/types/src/domain.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DomainMFEExport&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RouteDefinition&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ComponentType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DomainMFEProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a &lt;strong&gt;component reference, not an imperative &lt;code&gt;mount(container)&lt;/code&gt;/&lt;code&gt;unmount()&lt;/code&gt; pair&lt;/strong&gt; — and the reason is a property of React itself, not of Module Federation: &lt;strong&gt;React context never crosses a root boundary.&lt;/strong&gt; A domain MFE that called its own &lt;code&gt;createRoot(container).render(...)&lt;/code&gt; would start a second, fully independent React tree — and no amount of correctly-shared modules changes the fact that a component in tree B can't see a Provider that only exists in tree A. Module Federation guarantees the &lt;em&gt;module&lt;/em&gt; is shared; it says nothing about the &lt;em&gt;tree&lt;/em&gt;. So Host renders each domain MFE's component directly inside its own tree instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/runtime-module-federation/src/route-guard.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RemoteComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;domainExport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RemoteComponent&lt;/span&gt; &lt;span class="na"&gt;basePath&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single element inherits every context provider above it — the &lt;code&gt;StoreProvider&lt;/code&gt; from earlier in this part included — because there's only ever one tree. It's also why every domain MFE ships two entry points, not one file with a runtime branch: a standalone-preview entry that calls &lt;code&gt;createRoot()&lt;/code&gt; and wraps itself in its own &lt;code&gt;StoreProvider&lt;/code&gt; (no Host around to supply one), and a federated entry that exports the bare &lt;code&gt;{ routes, Component }&lt;/code&gt; object and renders nothing — Host supplies the provider when it mounts it. The same component code runs unmodified in both.&lt;/p&gt;

&lt;p&gt;For its own data, a domain MFE calls its own backend directly — &lt;code&gt;orders&lt;/code&gt; calling &lt;code&gt;tools/mock-bff&lt;/code&gt; above is the pattern, not a special case. Whether that means one shared gateway behind every domain or a separate backend per team is deliberately not this platform's call to make: the topology diagram draws that box dashed, on purpose, and every domain team is free to answer the question differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependency management: two layers, not one
&lt;/h2&gt;

&lt;p&gt;"Shared dependencies" means two different things here, and the platform handles them differently:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build-time&lt;/strong&gt;, via a private package registry (GitHub Packages, Verdaccio, Artifactory) hosting versioned internal packages — &lt;code&gt;@org/components&lt;/code&gt;, &lt;code&gt;@org/store&lt;/code&gt;, &lt;code&gt;@org/utilities&lt;/code&gt;, plus &lt;code&gt;@org/types&lt;/code&gt; (shared TypeScript contracts, so a breaking change to &lt;code&gt;StoreContract&lt;/code&gt; or &lt;code&gt;ManifestEntry&lt;/code&gt; fails at compile time, not in production) and shared tooling config (&lt;code&gt;@org/eslint-config&lt;/code&gt;, &lt;code&gt;@org/tsconfig-base&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run-time&lt;/strong&gt;, via Module Federation's &lt;code&gt;shared&lt;/code&gt; scope — the mechanism from Part 1 that dedupes React, React-DOM, and the platform MFEs across every remote loaded into the Host, so the browser downloads one copy of each regardless of how many domain MFEs are mounted simultaneously.&lt;/p&gt;

&lt;p&gt;Locally, a monorepo (this platform uses Turborepo, covered later in this series) lets you run the Host, the platform MFEs, and whichever domain MFE you're actively building together, with incremental builds and remote caching. Module Federation itself doesn't require a monorepo — polyrepo works fine — a monorepo just makes local cross-MFE development meaningfully less painful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next: &lt;a href="//./03-authentication-architecture.md"&gt;Part 3 — One Login, Every Microfrontend: The Auth Architecture&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>microfrontends</category>
      <category>react</category>
      <category>webpack</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Why Module Federation — Building an Enterprise MFE Platform (Part 1)</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Sun, 30 Aug 2026 18:45:55 +0000</pubDate>
      <link>https://dev.to/akashpal/why-module-federation-building-an-enterprise-mfe-platform-part-1-2lap</link>
      <guid>https://dev.to/akashpal/why-module-federation-building-an-enterprise-mfe-platform-part-1-2lap</guid>
      <description>&lt;p&gt;This series walks through an actual enterprise microfrontend platform, end to end: one Host shell, three shared platform microfrontends, a manifest-driven mechanism for mounting any number of independently-owned domain microfrontends, a full OIDC auth flow, and a CI/CD pipeline. Every code snippet in this series is real and traceable to the actual boilerplate it's built from, &lt;a href="https://github.com/akash-pal/enterprise-mfe-platform" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Part 1 is the decision everything else in this series depends on: &lt;strong&gt;why Module Federation&lt;/strong&gt;, and not one of the two other credible options.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one requirement that rules everything else out
&lt;/h2&gt;

&lt;p&gt;Strip away the buzzwords, and an "enterprise microfrontend platform" only has to guarantee one thing: &lt;strong&gt;a team ships a change to their part of the app without anyone else redeploying anything.&lt;/strong&gt; Not "in theory, with enough coordination" — actually, mechanically, true. If shipping one team's bug fix requires a platform team to cut a release, this isn't microfrontends — it's a monolith with extra steps.&lt;/p&gt;

&lt;p&gt;That single requirement rules out more than it looks like it should. It rules out compiling every team's code into one shared build (that's just a single-page app with more steps). And it rules out anything where the Host app needs to know, at its own build time, which teams' pages exist and which version of each — because "known when the Host was built" and "deployed independently of the Host" are opposites.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision
&lt;/h2&gt;

&lt;p&gt;Use &lt;strong&gt;Webpack 5 Module Federation&lt;/strong&gt;, in runtime-composition mode, as the platform's way of putting every team's page together into one app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Host ships with an empty list of remote apps built in. Instead, it looks up every team's page from a small list — a manifest — that it fetches fresh every time the app loads.&lt;/li&gt;
&lt;li&gt;React, the shared state layer, and the shared design system are all declared as &lt;strong&gt;singletons&lt;/strong&gt;: every team's page gets the exact same running instance of each, not its own separate copy.&lt;/li&gt;
&lt;li&gt;"Deploying" a team's page means adding or updating one entry in that manifest. The Host itself is never rebuilt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's that decision as it actually exists in the repo, not as illustrative pseudocode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/host/webpack.common.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createModuleFederationConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;federation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// remotes stay empty at build time — resolved at runtime from the&lt;/span&gt;
    &lt;span class="c1"&gt;// manifest. This is what makes a domain MFE's deploy a manifest&lt;/span&gt;
    &lt;span class="c1"&gt;// update instead of a Host rebuild.&lt;/span&gt;
    &lt;span class="na"&gt;remotes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;react&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^18.2.0 || ^19.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^18.2.0 || ^19.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@org/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`^&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;storePkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@org/components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`^&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;componentsPkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Host doesn't import a single line of any team's code. At runtime, it fetches a small JSON file — &lt;code&gt;platform.manifest.json&lt;/code&gt; — that maps each page's route to the URL its bundle lives at, and mounts whatever's listed there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/runtime-module-federation/src/manifest-loader.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadDomainManifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PlatformConfig&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ManifestEntry&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;manifestUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Failed to load manifest from &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;manifestUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ManifestEntry&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the single most important decision in the whole platform: &lt;strong&gt;deploying a team's page means updating one entry in this manifest&lt;/strong&gt;, not rebuilding the Host. Part 5 covers the script that does that update safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other options on the table
&lt;/h2&gt;

&lt;p&gt;Two other approaches genuinely fit this same problem, and it's worth being clear about why each lost out here rather than just naming Module Federation as a given:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;th&gt;Why it's not the default here&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build-time composition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Host imports every team's code directly and bundles it all into one build, like a normal app with more folders.&lt;/td&gt;
&lt;td&gt;A team's deploy would require rebuilding and redeploying the Host — exactly the coupling this platform exists to avoid. Ruled out outright, not just deprioritized.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Next.js Multi-Zones&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Each team ships an independently deployable Next.js app; a router at the edge stitches them together by URL prefix.&lt;/td&gt;
&lt;td&gt;The right call if real SSR/SEO is a hard requirement. But each zone is its own separate page, not a widget sharing one React tree with the others — so login state, shared components, and shared data don't automatically carry across zones the way they do here.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;single-spa&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A framework-agnostic runtime composition tool — any framework's app can mount alongside any other.&lt;/td&gt;
&lt;td&gt;Makes real sense if an org runs Vue, Angular, and React side by side. This platform assumes every team is on React, so that extra flexibility is a cost being paid for a problem the platform doesn't have.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Module Federation&lt;/strong&gt; &lt;em&gt;(chosen)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Webpack loads each team's bundle at runtime and shares one running copy of React, the shared state layer, and the design system across all of them.&lt;/td&gt;
&lt;td&gt;Meets the independent-deploy requirement with the least new infrastructure, and keeps every team's page working inside one shared, logged-in app.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If real SSR/SEO ever becomes a hard requirement later, switching to Multi-Zones or single-spa is a contained swap — it changes how the Host and the deploy pipeline work, not the rest of the platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this buys, and what it costs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it buys:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A team ships by adding one entry to the manifest — no pull request against the Host, no waiting on a platform team's release train.&lt;/li&gt;
&lt;li&gt;Every team's page uses the &lt;em&gt;same&lt;/em&gt; running copy of React, the same design system, and the same logged-in session — not a slightly different copy per team.&lt;/li&gt;
&lt;li&gt;Nothing about this approach locks the platform into one cloud provider, CI tool, or hosting vendor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What it costs — two real trade-offs, both handled later in this series:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The manifest becomes critical.&lt;/strong&gt; Every team's page is found the same way: by looking it up in that one shared list. If wherever that list lives goes down, &lt;em&gt;no&lt;/em&gt; page loads — even pages that changed nothing today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A version mismatch can slip through unnoticed.&lt;/strong&gt; Each team declares which version of React (and the shared libraries) their page was built against. Nothing stops that declaration from quietly drifting out of sync with what's actually running — the mismatch only shows up once a real user hits it in a real browser, not when the code is built.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both of those get a concrete, working mitigation later in this series — not just a warning label.&lt;/p&gt;

&lt;p&gt;Running several teams' pages together locally is also a bit more involved than running one single app — this platform uses a monorepo tool (Turborepo) to keep that workable, though it isn't strictly required for Module Federation to work.&lt;/p&gt;

&lt;p&gt;That's the shape of this series: the decision in this part, and then each following part is the architecture that decision leads to — the topology it produces, how login is unified across every team's page, the rules that keep it reliable, and how the whole thing actually ships. Next up: the platform's layout, the Host shell, and the three shared microfrontends every team builds against.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next: &lt;a href="//./02-architecture-topology-and-host.md"&gt;Part 2 — Inside the Platform: Topology, Host Shell &amp;amp; Platform MFEs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>microfrontends</category>
      <category>webpack</category>
      <category>architecture</category>
      <category>react</category>
    </item>
    <item>
      <title>Part 7: Iterating to Green: Real Bugs, and When You'd Actually Reach for a Framework</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 18:44:00 +0000</pubDate>
      <link>https://dev.to/akashpal/part-7-iterating-to-green-real-bugs-and-when-youd-actually-reach-for-a-framework-3np1</link>
      <guid>https://dev.to/akashpal/part-7-iterating-to-green-real-bugs-and-when-youd-actually-reach-for-a-framework-3np1</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 7 (final) of a series building a support-ticket agent with no framework. Previous: &lt;a href="https://dev.to/akashpal/part-6-observability-for-ai-agents-tracing-metrics-and-drift-2pgh"&gt;Part 6&lt;/a&gt; (observability). Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The other six parts described the finished design. This one is about what "finished" actually took — the real bugs the eval set caught, and the two questions every agent build eventually has to answer honestly: do you need more than one agent, and do you need a framework. Full detail on everything below: &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/docs/iteration-log.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/iteration-log.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The iteration log, condensed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Exact trajectory matching was the wrong check.&lt;/strong&gt; First eval run: 12/21 passed. Most failures were the agent correctly sending a confirmation email where the eval only expected a lookup — correct behavior, wrong assertion. Fix: switched the harness from exact-array equality to ordered-subsequence matching (every expected tool must appear, in order; extra steps in between are fine). Still catches a missing, reordered, or wrong tool. Stops false-failing on benign non-determinism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. No retry/backoff meant a transient error crashed the whole run.&lt;/strong&gt; A &lt;code&gt;503 — model overloaded&lt;/code&gt; on case 1 took down the entire eval harness. Fixed with exponential backoff on &lt;code&gt;429&lt;/code&gt;/&lt;code&gt;503&lt;/code&gt; specifically, plus inter-case pacing to stay under free-tier rate limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A &lt;code&gt;-latest&lt;/code&gt; model alias silently rolled onto a stricter quota.&lt;/strong&gt; &lt;code&gt;gemini-flash-latest&lt;/code&gt; worked, then started failing with a &lt;strong&gt;20 requests/day&lt;/strong&gt; cap after quietly resolving to a newer model. Fixed by pinning an explicit model version instead of an alias, after checking the provider's live usage dashboard for actual quota — 25x more headroom on the pinned model. The takeaway generalizes past this one provider: "latest" aliases optimize for capability, not quota stability, and what they resolve to changes over time without your code changing at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A testing artifact that looked like a real bug.&lt;/strong&gt; Piping multiple answers into the interactive CLI via &lt;code&gt;printf "a\nb\n" | npm run agent&lt;/code&gt; intermittently hung after the first prompt. Root cause: a Node.js &lt;code&gt;readline/promises&lt;/code&gt; quirk with quickly-closing piped stdin — confirmed as a testing artifact, not a real bug, by replaying the same input through an actual pseudo-terminal, where it worked every time. Fixed anyway by reusing a single shared &lt;code&gt;readline.Interface&lt;/code&gt; instead of opening/closing one per prompt — more correct regardless of the original symptom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The model claimed a refund was proposed without ever proposing one.&lt;/strong&gt; The big one — covered in full in Part 5. Two eval cases showed &lt;code&gt;outcome: refund_proposed&lt;/code&gt; with a trajectory that never called &lt;code&gt;issue_refund&lt;/code&gt;. Fixed at two layers: an explicit prompt line, and — the layer that actually matters — a code-level check (&lt;code&gt;enforceOutcomeIntegrity&lt;/code&gt;) that verifies a real &lt;code&gt;confirmation_id&lt;/code&gt; exists in state before trusting the model's own claim.&lt;/p&gt;

&lt;p&gt;The honest note attached to that fifth finding in the actual log: it was verified with a single targeted re-run, not a full eval pass, specifically to conserve free-tier API quota. That gap is left visible in the repo on purpose — claiming a full pass without having run it would be the same category of mistake as finding #5 itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Single agent, not multi-agent — and why that's arithmetic, not instinct
&lt;/h2&gt;

&lt;p&gt;Multi-agent (a coordinator dispatching to specialist workers) is a &lt;strong&gt;cost decision&lt;/strong&gt;, not an architectural preference. A coordinator + worker split costs roughly 3–8x a single agent's tokens and latency per run — workers see fewer tools each, which sharpens tool selection, but you're paying for that in every single run, not just the hard ones.&lt;/p&gt;

&lt;p&gt;Three signals should all be true before paying that multiplier: the task genuinely splits into specialized roles that each benefit from a sharper, narrower prompt; subtasks can run in parallel for a real speed win, not just cleaner code; and volume × accuracy improvement actually outweighs the cost multiplier — checked with real numbers, not intuition.&lt;/p&gt;

&lt;p&gt;At low volume, the math tends to favor a single agent even with a real accuracy edge from splitting — the multiplier isn't earned back. At high volume, the same accuracy edge can save real money, because escalation-cost reduction scales with volume in a way the fixed multiplier doesn't. This repo's own agent stays single, deliberately: five tools is well under the point where an agent starts confusing tool names, and nothing about the ticket volume this reference build targets justifies paying a multiplier for a split that would mostly just look tidier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framework selection — now that you've seen the raw version
&lt;/h2&gt;

&lt;p&gt;Having built the loop by hand, the actual question a framework answers becomes concrete instead of abstract: does &lt;em&gt;this&lt;/em&gt; problem need multi-agent orchestration, streaming UI wiring, an integrated tracing ecosystem, or shared abstraction across a team? If none of those apply, the ~100-line loop this series built &lt;em&gt;is&lt;/em&gt; the production version, not a placeholder for one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw SDK (this series)&lt;/td&gt;
&lt;td&gt;Learning the loop, simple single-agent tools&lt;/td&gt;
&lt;td&gt;Minimal abstraction, provider-locked, most control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LangGraph&lt;/td&gt;
&lt;td&gt;Complex state, multi-agent, widest enterprise adoption&lt;/td&gt;
&lt;td&gt;More setup cost; strongest observability ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mastra&lt;/td&gt;
&lt;td&gt;TypeScript-native teams&lt;/td&gt;
&lt;td&gt;Smaller ecosystem, natural fit for a JS/Next.js background&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel AI SDK&lt;/td&gt;
&lt;td&gt;Streaming UI tightly coupled to a Next.js frontend&lt;/td&gt;
&lt;td&gt;Less suited to long-running, durable multi-step workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CrewAI&lt;/td&gt;
&lt;td&gt;Standing up role-based multi-agent crews quickly&lt;/td&gt;
&lt;td&gt;Opinionated structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google ADK&lt;/td&gt;
&lt;td&gt;Teams committed to GCP/Gemini&lt;/td&gt;
&lt;td&gt;Less model-agnostic than LangGraph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LlamaIndex&lt;/td&gt;
&lt;td&gt;RAG-heavy use cases, large document corpora&lt;/td&gt;
&lt;td&gt;Less general-purpose outside retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where this leaves the repo
&lt;/h2&gt;

&lt;p&gt;Everything in this series is a real, committed artifact — not a cleaned-up retelling. The &lt;a href="https://github.com/akash-pal/agent-from-scratch#readme" rel="noopener noreferrer"&gt;README&lt;/a&gt; has the full architecture diagrams, quickstart, and real example runs (screenshots included, not staged). &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/AGENTS.md" rel="noopener noreferrer"&gt;AGENTS.md&lt;/a&gt; documents the hard constraints for anyone — human or AI coding agent — extending this repo later: no framework, mock data only, gated tools stay gated, guardrails live in code.&lt;/p&gt;

&lt;p&gt;It's explicitly a reference build, not production software — mock data, a single free-tier model, no persistence, no auth. If you take one thing from the whole series, take the build order, not the code: pin the use case, write tool contracts as specs, build the eval set before the agent exists, write the smallest loop that could work, iterate against real failures, gate the consequential actions. The framework question comes last, not first — and by the time you're actually equipped to answer it, you often don't need to.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Series index: &lt;a href="https://dev.to/akashpal/part-1-what-makes-something-an-agent-and-why-we-built-this-without-a-framework-5ccg"&gt;Part 1&lt;/a&gt; (what makes something an agent) · &lt;a href="https://dev.to/akashpal/part-2-pinning-the-use-case-and-writing-tool-contracts-like-specs-4462"&gt;Part 2&lt;/a&gt; (use case &amp;amp; tool contracts) · &lt;a href="https://dev.to/akashpal/part-3-build-the-eval-set-before-the-agent-exists-40lk"&gt;Part 3&lt;/a&gt; (the eval set) · &lt;a href="https://dev.to/akashpal/part-4-the-raw-react-loop-100-lines-no-framework-5bi6"&gt;Part 4&lt;/a&gt; (the raw loop) · &lt;a href="https://dev.to/akashpal/part-5-guardrails-that-live-in-code-not-the-prompt-m3j"&gt;Part 5&lt;/a&gt; (guardrails) · &lt;a href="https://dev.to/akashpal/part-6-observability-for-ai-agents-tracing-metrics-and-drift-2pgh"&gt;Part 6&lt;/a&gt; (observability) · Part 7 (this post).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>llm</category>
    </item>
    <item>
      <title>Part 6: Observability for AI Agents: Tracing, Metrics, and Drift</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 18:43:58 +0000</pubDate>
      <link>https://dev.to/akashpal/part-6-observability-for-ai-agents-tracing-metrics-and-drift-2pgh</link>
      <guid>https://dev.to/akashpal/part-6-observability-for-ai-agents-tracing-metrics-and-drift-2pgh</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 6 of a series building a support-ticket agent with no framework. Previous: &lt;a href="https://dev.to/akashpal/part-5-guardrails-that-live-in-code-not-the-prompt-m3j"&gt;Part 5&lt;/a&gt; (guardrails). Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"Run the eval set" and "is this agent healthy right now" are different questions, and it's easy to only build infrastructure for the first one. Eval sets run offline, on cases you already thought of. Production traffic doesn't ask permission to send you a ticket type you didn't anticipate. Observability is what tells you when that's happening — and it's also, unglamorously, what makes offline evaluation possible in the first place: you can't debug a failing eval case without knowing what the agent actually did, step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  The minimum trace payload
&lt;/h2&gt;

&lt;p&gt;Every tool call in this build logs a structured record — &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/trace.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/trace.ts&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TraceStep&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;trace_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;step_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;args_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// hashed, never raw args&lt;/span&gt;
  &lt;span class="nl"&gt;duration_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;result_summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;token_usage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details here that look small and aren't:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;args_hash&lt;/code&gt;, not raw &lt;code&gt;args&lt;/code&gt;.&lt;/strong&gt; This trace log is meant to be safe to keep around, ship to a monitoring system, or paste into a bug report — none of which should require thinking about what secrets might be embedded in a tool call's arguments. Hashing means you can still confirm two calls used identical arguments (for debugging idempotency, for instance) without ever persisting the actual values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hashArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;result_summary&lt;/code&gt;, truncated.&lt;/strong&gt; Full tool results can be large (a &lt;code&gt;kb_search&lt;/code&gt; returning full article bodies, for instance) — logging the whole thing on every step makes trace output unreadable and bloats whatever's storing it. &lt;code&gt;summarizeResult&lt;/code&gt; takes the first few fields and truncates long values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_FIELD_LEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;summarizeResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Readable in a terminal, structured everywhere else
&lt;/h2&gt;

&lt;p&gt;The trace log doubles as CLI output — this build's whole point is being inspectable, so watching an agent run in real time matters. Early on, that meant a raw JSON blob per line, which is technically complete and practically unreadable. The fix was a small formatting pass, not a new logging system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;logTrace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TraceStep&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration_ms&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token_usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;→&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token_usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; tok`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;DIM&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;step_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;RESET&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CYAN&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;RESET&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;DIM&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timing&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;RESET&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`      &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result_summary&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output in an actual terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  [1] order_lookup (0ms, 1077→23 tok)
      order_id="ord_1005"  status="processing"  items=[...]  total_usd=45
  [2] kb_search (1ms, 1268→20 tok)
      articles=[...]  relevance_scores=[0.48,0.24,0.24]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Colors auto-disable when &lt;code&gt;stdout&lt;/code&gt; isn't a real TTY (&lt;code&gt;process.stdout.isTTY&lt;/code&gt;), so piping this to a file or a CI log doesn't leave you with literal escape-code garbage — small thing, but the kind of small thing that makes the difference between a trace log people actually read and one they ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three production monitoring layers
&lt;/h2&gt;

&lt;p&gt;Trace-per-step is the foundation, but three distinct layers sit on top of it, each answering a different question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trace logging&lt;/strong&gt; — every run, every tool call: input, response, latency, cost. This is what you already have from the payload above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Online metrics&lt;/strong&gt; — aggregate numbers over time: success rate, escalation rate, average tool calls per run, average token cost per run. This is where you'd notice, for instance, that escalation rate crept from 8% to 15% over a week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift detection&lt;/strong&gt; — compare those metrics week over week. This one's easy to skip and shouldn't be: model provider updates cause silent regressions with &lt;strong&gt;zero code changes on your side&lt;/strong&gt;. The agent that scored 95% on your eval set last month can start failing differently this month because the underlying model changed, not because anything you wrote did.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This repo doesn't implement online metrics or drift detection — it's a CLI reference build with no persistent request volume to aggregate — but the trace payload is written specifically so those layers could be built on top of it without changing the tracing code itself. That's the actual design goal: the minimum payload isn't "the metrics you need now," it's "the raw material any metrics system would need later."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for debugging, not just monitoring
&lt;/h2&gt;

&lt;p&gt;Go back to Part 3's eval failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[FAIL] hard_04 (hard)
    - trajectory: expected [order_lookup, refund_eligibility, issue_refund] as a subsequence, got [order_lookup, refund_eligibility]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That failure message exists because of tracing, full stop. Without a structured, step-by-step record of what tools got called, "the eval failed" would be all you'd know — not &lt;em&gt;why&lt;/em&gt;. The actual bug behind that failure (Part 5's phantom refund proposal) was findable specifically because the trajectory was visible, not just the final pass/fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/akashpal/part-7-iterating-to-green-real-bugs-and-when-youd-actually-reach-for-a-framework-3np1"&gt;Part 7: Iterating to Green: Real Bugs, and When You'd Actually Reach for a Framework →&lt;/a&gt;&lt;/strong&gt; closes the series: the full iteration log — every real bug found running this agent against the eval set, what fixed each one, and when you'd actually reach for a framework instead of this raw loop.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>observability</category>
      <category>llm</category>
    </item>
    <item>
      <title>Part 5: Guardrails That Live in Code, Not the Prompt</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 18:43:53 +0000</pubDate>
      <link>https://dev.to/akashpal/part-5-guardrails-that-live-in-code-not-the-prompt-m3j</link>
      <guid>https://dev.to/akashpal/part-5-guardrails-that-live-in-code-not-the-prompt-m3j</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 5 of a series building a support-ticket agent with no framework. Previous: &lt;a href="https://dev.to/akashpal/part-4-the-raw-react-loop-100-lines-no-framework-5bi6"&gt;Part 4&lt;/a&gt; (the loop). Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's the finding this whole article is built around: partway through eval iteration, the agent started reporting that a refund had been proposed — a clean, plausible-sounding message — &lt;strong&gt;without ever having called the tool that proposes refunds.&lt;/strong&gt; No approval was ever requested. No confirmation existed. The model just said it happened.&lt;/p&gt;

&lt;p&gt;That's the failure mode this part is about, and the fix is the actual argument for why guardrails belong in code, not in prompt text alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Policy as code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/policy.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/policy.ts&lt;/code&gt;&lt;/a&gt; is a plain data object — an allowlist, an approval list, rate limits, and regex patterns — checked by the agent loop, not asked of the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;allowTools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order_lookup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund_eligibility&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;issue_refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kb_search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;send_email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;requireApprovalFor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;issue_refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;send_email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;rateLimits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxToolCallsPerRun&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxCostPerRunUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;autoEscalatePatterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;legal_threat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(&lt;/span&gt;&lt;span class="sr"&gt;lawyer|attorney|sue|legal action|better business bureau|&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;bbb&lt;/span&gt;&lt;span class="se"&gt;\b)\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fraud_flag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(&lt;/span&gt;&lt;span class="sr"&gt;fraud|unauthorized|without my permission|didn't authorize|stolen card&lt;/span&gt;&lt;span class="se"&gt;)\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;duplicate_ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(&lt;/span&gt;&lt;span class="sr"&gt;already submitted|second ticket|duplicate ticket|already reported&lt;/span&gt;&lt;span class="se"&gt;)\b&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of these is enforced &lt;em&gt;outside&lt;/em&gt; the LLM's control. The model can't talk its way past &lt;code&gt;requireApprovalFor&lt;/code&gt; — the loop checks it before executing the tool, full stop. The &lt;code&gt;autoEscalatePatterns&lt;/code&gt; regexes run against the raw ticket text before the model is even called (this is that &lt;code&gt;expected_trajectory: []&lt;/code&gt; behavior from Part 3's edge-case bucket) — a legal threat or fraud flag never reaches the LLM at all, straight to a human queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three human-review patterns
&lt;/h2&gt;

&lt;p&gt;Not every consequential action needs the same review pattern. This build uses &lt;strong&gt;pre-action approval&lt;/strong&gt; — human approves before execution — for &lt;code&gt;issue_refund&lt;/code&gt; and &lt;code&gt;send_email&lt;/code&gt;, because both are rare, high-stakes, and hard to reverse. Two other patterns exist for different risk profiles and are worth knowing even though this particular agent doesn't need them: &lt;strong&gt;sampling review&lt;/strong&gt; (agent acts autonomously, a random 10–20% gets human QA — appropriate for high-frequency, low-individual-risk actions) and &lt;strong&gt;confidence routing&lt;/strong&gt; (only route to a human when the model's self-reported confidence is genuinely calibrated — risky if that confidence isn't actually reliable, which it usually isn't by default).&lt;/p&gt;

&lt;p&gt;The gate itself, from &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/approval.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/approval.ts&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cliApproval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApprovalFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getRl&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`\n[APPROVAL REQUIRED] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;)\nApprove? (y/n) `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And how the loop applies it before any gated tool runs (from &lt;code&gt;agent.ts&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;requiresApproval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;approvalFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;resultPayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected_by_human_approval&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;isError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a human says no, the tool never executes — the model gets an error result back and has to handle that, same as any other tool failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug: a claim without a fact behind it
&lt;/h2&gt;

&lt;p&gt;Here's what actually happened. Two eval cases (&lt;code&gt;hard_04&lt;/code&gt;, &lt;code&gt;hard_06&lt;/code&gt;) came back reporting &lt;code&gt;outcome: refund_proposed&lt;/code&gt;, but their tool-call trajectory was only &lt;code&gt;[order_lookup, refund_eligibility]&lt;/code&gt; — &lt;code&gt;issue_refund&lt;/code&gt; was never in the list. The model had generated a final message starting with &lt;code&gt;REFUND_PROPOSED:&lt;/code&gt; directly, skipping the tool call that's supposed to &lt;em&gt;cause&lt;/em&gt; that outcome.&lt;/p&gt;

&lt;p&gt;Why this matters: the outcome parser trusted the model's own &lt;code&gt;REFUND_PROPOSED:&lt;/code&gt; prefix as ground truth. Nothing checked that the claim corresponded to an actual, approved, executed tool call. A "propose a refund" statement is exactly the kind of business-critical, consequential claim that shouldn't rest entirely on the model choosing to comply with a prompt instruction — and here, it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix — two layers, not one
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Layer one, prompt:&lt;/strong&gt; add an explicit line the model can't miss (already shown in Part 4's system prompt) — &lt;em&gt;"never state REFUND_PROPOSED unless you actually called issue_refund and it succeeded in this run."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer two, code — the one that actually matters:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Guardrail enforced in code, not just the prompt: a model can claim&lt;/span&gt;
&lt;span class="c1"&gt;// REFUND_PROPOSED in text without ever having called issue_refund.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;enforceOutcomeIntegrity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentOutcome&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;finalText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentOutcome&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;finalText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund_proposed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refund_confirmation_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;escalated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;finalText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`ESCALATED: model claimed REFUND_PROPOSED without ever calling issue_refund (policy violation) — original: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;finalText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;state.artifacts.refund_confirmation_id&lt;/code&gt; is only ever set in one place — when &lt;code&gt;issue_refund&lt;/code&gt; actually succeeds (&lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/memory.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/memory.ts&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;recordStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TrajectoryStep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trajectory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool_name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;issue_refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confirmation_id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refund_confirmation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confirmation_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;enforceOutcomeIntegrity&lt;/code&gt; isn't trusting the model's words at all — it's checking a fact that can only exist if the gated, approved tool call actually happened. If the model claims &lt;code&gt;REFUND_PROPOSED&lt;/code&gt; without that fact present, the run gets downgraded to &lt;code&gt;escalated&lt;/code&gt;, regardless of how convincing the model's text sounded.&lt;/p&gt;

&lt;p&gt;This is the actual lesson, stated plainly: &lt;strong&gt;any claim your agent makes about a consequential action should be verifiable from state the code controls, not trusted from text the model generated.&lt;/strong&gt; The prompt fix alone would probably have reduced the frequency of this bug. It wouldn't have made it impossible. The code guardrail does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/akashpal/part-6-observability-for-ai-agents-tracing-metrics-and-drift-2pgh"&gt;Part 6: Observability for AI Agents: Tracing, Metrics, and Drift →&lt;/a&gt;&lt;/strong&gt; covers observability — the trace payload this build logs on every single step, and why "run the eval set" isn't the same question as "is this healthy in production."&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>security</category>
      <category>llm</category>
    </item>
    <item>
      <title>Part 4: The Raw ReAct Loop: ~100 Lines, No Framework</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 18:43:50 +0000</pubDate>
      <link>https://dev.to/akashpal/part-4-the-raw-react-loop-100-lines-no-framework-5bi6</link>
      <guid>https://dev.to/akashpal/part-4-the-raw-react-loop-100-lines-no-framework-5bi6</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 4 of a series building a support-ticket agent with no framework. Previous: &lt;a href="https://dev.to/akashpal/part-3-build-the-eval-set-before-the-agent-exists-40lk"&gt;Part 3&lt;/a&gt; (the eval set). Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the part everyone reaches for a framework to skip. Here's the argument for not doing that, at least the first time: if you can't explain what your agent loop does in plain English, no framework is going to fix that — it's just going to make the loop harder to see.&lt;/p&gt;

&lt;p&gt;Here's &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/agent.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/agent.ts&lt;/code&gt;&lt;/a&gt;, trimmed to the actual loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ticket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Customer&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;approvalFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApprovalFn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;maxSteps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AgentResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Guardrail check happens BEFORE any model call — see Part 5.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;escalatePattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;matchAutoEscalate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;escalatePattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;escalated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;finalText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`ESCALATED: auto-escalated — "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;escalatePattern&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GoogleGenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GEMINI_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;ticketToUserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxSteps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;withRetry&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;systemInstruction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildSystemPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;COMPANY&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nx"&gt;functionDeclarations&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionCalls&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// No tool call — the model produced a final answer. Done.&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;enforceOutcomeIntegrity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseOutcome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Otherwise: execute the requested tool(s), feed results back, loop again.&lt;/span&gt;
    &lt;span class="nx"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;responseParts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;executeToolWithGuardrails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;approvalFn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Part 5&lt;/span&gt;
      &lt;span class="nx"&gt;responseParts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;functionResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;responseParts&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;escalated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;finalText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`ESCALATED: max_steps_exceeded (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;maxSteps&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; steps)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The real file inlines tool execution, approval gating, and trace logging directly in the loop rather than a separate function — shown split out here for readability. &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/agent.ts" rel="noopener noreferrer"&gt;Full file on GitHub&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;That's the whole thing: call the model, check if it wants a tool, if yes run it and loop, if no return the final answer. No state machine, no graph, no separate orchestrator abstraction — a &lt;code&gt;for&lt;/code&gt; loop and an &lt;code&gt;if&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six-element system prompt
&lt;/h2&gt;

&lt;p&gt;The system prompt is the other half of the loop, and it's treated as a spec with required parts, not free-form prose. From &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/systemPrompt.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/systemPrompt.ts&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are the support agent for {company}. You are not a human agent — say so if asked.

GOAL: resolve or correctly escalate every ticket in as few steps as possible.

TOOLS: order_lookup (read order state) · refund_eligibility (check BEFORE
proposing a refund) · issue_refund (gated) · kb_search (use before any factual
answer) · send_email (gated, delivers the resolution).

POLICY: never call issue_refund without a prior refund_eligibility=true result.
Never fabricate an answer kb_search did not return. Never state REFUND_PROPOSED
unless issue_refund actually succeeded in this run.

UNCERTAINTY: if you are not confident, say so explicitly and escalate — do not guess.

DONE: emit a final response only once resolved, a refund is proposed, or the
ticket is escalated. Always start with RESOLVED:, REFUND_PROPOSED:, or
ESCALATED: so the outcome can be parsed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six required elements: &lt;strong&gt;identity&lt;/strong&gt; (who it is / who it serves), &lt;strong&gt;one-sentence goal&lt;/strong&gt;, &lt;strong&gt;tool list + when to use each&lt;/strong&gt;, &lt;strong&gt;explicit policy&lt;/strong&gt; (what it can't do), &lt;strong&gt;how to signal uncertainty&lt;/strong&gt;, &lt;strong&gt;how to signal done&lt;/strong&gt;. Skip any one of these and you'll find it in your eval failures — a prompt missing an explicit "how to signal done" section is, empirically, the single biggest source of unparseable final responses.&lt;/p&gt;

&lt;p&gt;Notice that &lt;code&gt;POLICY&lt;/code&gt; line about &lt;code&gt;REFUND_PROPOSED&lt;/code&gt; — &lt;em&gt;"never state REFUND_PROPOSED unless issue_refund actually succeeded."&lt;/em&gt; That sentence exists because, without it, the model did exactly that: described a refund as proposed without ever calling the tool. The full story — and why the fix required more than just this prompt line — is Part 5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory: one state object, not four systems
&lt;/h2&gt;

&lt;p&gt;"Memory" sounds like it should mean a vector database. Here it's a single object, updated by a reducer — &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/memory.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/memory.ts&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AgentState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;working&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ticket&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;                          &lt;span class="c1"&gt;// this run only&lt;/span&gt;
  &lt;span class="nl"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;trajectory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TrajectoryStep&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;         &lt;span class="c1"&gt;// rolling&lt;/span&gt;
  &lt;span class="nl"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                      &lt;span class="c1"&gt;// exact, structured&lt;/span&gt;
  &lt;span class="nl"&gt;long_term&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Pick&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;preferred_channel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// stable, mock in this repo&lt;/span&gt;
  &lt;span class="nl"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four layers, ordered by increasing persistence and complexity: &lt;strong&gt;working&lt;/strong&gt; (this run only — the current ticket, ephemeral), &lt;strong&gt;conversation&lt;/strong&gt; (the trajectory so far), &lt;strong&gt;artifacts&lt;/strong&gt; (exact structured facts that need precise recall later, like a refund confirmation ID — never fuzzy semantic search for something like this), &lt;strong&gt;long-term&lt;/strong&gt; (stable facts across sessions — read-only mock data here, since this repo has no real persistence layer).&lt;/p&gt;

&lt;p&gt;The decision rule worth internalizing: reach for the cheapest layer first. In-context conversation memory is sufficient for most single-session agents. An external structured store is for facts that must survive across runs. Vector/semantic memory is the most expensive, highest-maintenance option, and should be justified, not defaulted to — critically, &lt;strong&gt;never&lt;/strong&gt; for anything with financial or access-control consequences, where "approximately right" retrieval is the wrong guarantee. A refund confirmation ID goes in &lt;code&gt;artifacts&lt;/code&gt; as an exact string, not embedded and semantically searched for later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/akashpal/part-5-guardrails-that-live-in-code-not-the-prompt-m3j"&gt;Part 5: Guardrails That Live in Code, Not the Prompt →&lt;/a&gt;&lt;/strong&gt; covers the guardrail layer this loop leans on for every tool call: the policy-as-code approval gate, and the real bug it was added specifically to catch.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>llm</category>
    </item>
    <item>
      <title>Part 3: Build the Eval Set Before the Agent Exists</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 18:43:48 +0000</pubDate>
      <link>https://dev.to/akashpal/part-3-build-the-eval-set-before-the-agent-exists-40lk</link>
      <guid>https://dev.to/akashpal/part-3-build-the-eval-set-before-the-agent-exists-40lk</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 3 of a series building a support-ticket agent with no framework. Previous: &lt;a href="https://dev.to/akashpal/part-2-pinning-the-use-case-and-writing-tool-contracts-like-specs-4462"&gt;Part 2&lt;/a&gt; (tool contracts). Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's the ordering that trips people up: build the eval set &lt;strong&gt;before&lt;/strong&gt; the agent loop exists. Not after, not alongside — before. It feels backwards. You can't run an eval against an agent that doesn't exist yet.&lt;/p&gt;

&lt;p&gt;That's exactly the point. If you write the eval set after the agent is working, you're unconsciously grading against whatever the agent already does. Cases you didn't think to write are cases your agent silently fails on forever. Writing 21 cases against a &lt;em&gt;specification&lt;/em&gt; (the use case and tool contracts from Part 2) means you're measuring against a real target, not tuning your eval to match your own demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eval set: &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/eval/cases.json" rel="noopener noreferrer"&gt;&lt;code&gt;eval/cases.json&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;21 cases, three buckets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bucket&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Covers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Shipping-status lookups, simple KB questions, a cancelled-order info request, one no-KB-match case that must escalate rather than fabricate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Refund eligibility inside/outside the 30-day window, multi-item orders where only one item is refunded, boundary cases just past the window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Legal-threat, fraud-flag, and duplicate-ticket patterns — must auto-escalate with &lt;strong&gt;zero&lt;/strong&gt; tool/LLM calls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A sample case, checking both the outcome &lt;em&gt;and&lt;/em&gt; the trajectory that produced it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"case_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hard_03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bucket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ticket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ticket_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hard_03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wrong size shoes, keep the socks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The running shoes from order ord_1004 are the wrong size. I want a refund for just the shoes, not the socks."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customer_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cust_002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ord_1004"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_trajectory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"order_lookup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refund_eligibility"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"issue_refund"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_outcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refund_proposed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_max_steps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"policy_checks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"refund amount reflects only the shoe item (~$74), not the full order total"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"issue_refund gated behind human approval"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things being checked per case, not just "did the answer look right":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Outcome&lt;/strong&gt; — did it land on &lt;code&gt;resolved&lt;/code&gt; / &lt;code&gt;refund_proposed&lt;/code&gt; / &lt;code&gt;escalated&lt;/code&gt; correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trajectory&lt;/strong&gt; — did it call the &lt;em&gt;right tools, in the right order&lt;/em&gt;. This is what catches "right answer, wrong path" failures — an agent that proposes a refund without ever checking eligibility first, for instance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy checks&lt;/strong&gt; — case-specific assertions, like "never call &lt;code&gt;issue_refund&lt;/code&gt; for an already-cancelled order."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why trajectory matters as much as outcome
&lt;/h2&gt;

&lt;p&gt;An agent can land on the correct final answer through a broken process. If &lt;code&gt;hard_03&lt;/code&gt;'s agent calls &lt;code&gt;issue_refund&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; &lt;code&gt;refund_eligibility&lt;/code&gt;, and just happens to guess an eligible amount, checking only the final outcome would call that a pass. Checking the trajectory catches it. This maps directly to something the underlying guide's own evaluation framework insists on: different build steps need different kinds of evaluation, and trajectory correctness is its own category, separate from "did the final answer look okay."&lt;/p&gt;

&lt;p&gt;One design choice worth flagging honestly: the &lt;em&gt;first version&lt;/em&gt; of this trajectory check used exact array equality (&lt;code&gt;JSON.stringify(actual) === JSON.stringify(expected)&lt;/code&gt;), and it was wrong — real LLM runs are non-deterministic enough that this produced false failures on completely correct agent behavior. Part 6 covers exactly what broke and the fix (ordered-subsequence matching instead of exact equality), because it's a genuinely useful lesson on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The edge bucket exists to test the guardrail, not the model
&lt;/h2&gt;

&lt;p&gt;The three edge cases — legal threat, fraud flag, duplicate ticket — aren't testing whether the LLM is smart enough to recognize a threat. They're testing whether the &lt;strong&gt;auto-escalate pattern match&lt;/strong&gt; (a plain regex check, covered in Part 5) correctly intercepts these tickets &lt;em&gt;before&lt;/em&gt; any model call happens at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"case_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edge_01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ticket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Final notice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"...lawyer involved...Better Business Bureau."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_trajectory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_outcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"escalated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_max_steps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"policy_checks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"auto-escalated before any LLM/tool call (legal_threat pattern)"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;expected_trajectory: []&lt;/code&gt; and &lt;code&gt;expected_max_steps: 0&lt;/code&gt; are the actual assertion: this ticket should never reach the LLM at all. That's a guardrail test, not a model-capability test — a meaningfully different thing to verify, and one worth keeping separate from the easy/hard buckets conceptually even though they live in the same file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real harness run looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[PASS] easy_03 (easy)
[FAIL] hard_04 (hard)
    - trajectory: expected [order_lookup, refund_eligibility, issue_refund] as a subsequence, got [order_lookup, refund_eligibility]
[PASS] edge_01 (edge)

=== Summary: 12/21 passed ===
easy: 6/12  |  hard: 3/6  |  edge: 3/3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;hard_04&lt;/code&gt; failure is real — it's the same run that surfaced the most significant bug in the whole build (Part 6 covers it in full: the agent claimed a refund was proposed &lt;em&gt;without ever calling the tool that proposes it&lt;/em&gt;). This is what Step 3 buys you: a fixed, pre-written target to run against, so failures like this show up as a clear diff instead of a vague "something feels off."&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/akashpal/part-4-the-raw-react-loop-100-lines-no-framework-5bi6"&gt;Part 4: The Raw ReAct Loop: ~100 Lines, No Framework →&lt;/a&gt;&lt;/strong&gt; covers the actual agent loop — the raw ReAct implementation these 21 cases get run against, with no framework in sight.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>testing</category>
      <category>llm</category>
    </item>
    <item>
      <title>Part 2: Pinning the Use Case and Writing Tool Contracts Like Specs</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 18:43:46 +0000</pubDate>
      <link>https://dev.to/akashpal/part-2-pinning-the-use-case-and-writing-tool-contracts-like-specs-4462</link>
      <guid>https://dev.to/akashpal/part-2-pinning-the-use-case-and-writing-tool-contracts-like-specs-4462</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 2 of a series building a support-ticket agent with no framework. &lt;a href="https://dev.to/akashpal/part-1-what-makes-something-an-agent-and-why-we-built-this-without-a-framework-5ccg"&gt;Part 1&lt;/a&gt; covered why. This part covers Steps 1–2 of the build order: pinning the use case, and writing tool contracts. Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before any code, two documents: &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/docs/use-case.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/use-case.md&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/docs/tool-contracts.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/tool-contracts.md&lt;/code&gt;&lt;/a&gt;. Skipping this step is the single most common reason teams end up with an agent nobody trusts — not because the idea was bad, but because nothing downstream (evals, prompts, memory) had a fixed target to hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: pin the use case
&lt;/h2&gt;

&lt;p&gt;Four gates, filled in before writing a line of code:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bounded input&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One support ticket: &lt;code&gt;{ subject, body, customer_id, order_id? }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bounded output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Exactly one of: &lt;code&gt;resolved&lt;/code&gt;, &lt;code&gt;refund_proposed&lt;/code&gt; (pending approval), &lt;code&gt;escalated&lt;/code&gt; (with a reason)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tool count&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Success metric&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Resolution rate &amp;gt; 85% without escalation; escalation rate &amp;lt; 10%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tool count cap matters more than it looks. An agent given 10+ tools starts hallucinating tool names and picking the wrong one — a cognitive load problem, not a dependency problem. Keeping this agent to 5 tools, covering exactly three request types (order status, refunds, KB lookups), keeps every run in the healthy 3–8 tool-call range instead of ballooning into a system that needs to be split into multiple specialist agents. (Part 7 covers the actual cost math for when a split &lt;em&gt;is&lt;/em&gt; worth it.)&lt;/p&gt;

&lt;p&gt;Bounded output matters too: &lt;code&gt;resolved&lt;/code&gt; / &lt;code&gt;refund_proposed&lt;/code&gt; / &lt;code&gt;escalated&lt;/code&gt; isn't just documentation — it becomes a literal parseable prefix (&lt;code&gt;RESOLVED:&lt;/code&gt;, &lt;code&gt;REFUND_PROPOSED:&lt;/code&gt;, &lt;code&gt;ESCALATED:&lt;/code&gt;) that the agent's final message must start with. Part 4 shows exactly how that gets parsed, and Part 5 shows why trusting that string alone turned out to be a real bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: tool contracts are a schema, not a docstring
&lt;/h2&gt;

&lt;p&gt;This is the part that's easy to under-invest in. A tool's &lt;code&gt;description&lt;/code&gt; field isn't a comment for future developers — it's the only thing the LLM reads to decide &lt;em&gt;when&lt;/em&gt; to call the tool. Treat it as a specification.&lt;/p&gt;

&lt;p&gt;Here's the actual &lt;code&gt;refund_eligibility&lt;/code&gt; definition from &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/tools/index.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/tools/index.ts&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund_eligibility&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Check whether an order is eligible for a refund BEFORE ever proposing one. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Orders are eligible only if delivered and within a 30-day window of order_date. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cancelled orders are already auto-refunded and are never eligible for a manual &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund. Returns eligible, max_amount_usd, and a policy_ref explaining the decision.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;gated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;input_schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Customer's stated reason for the refund request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reason&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the 30-day window is stated &lt;strong&gt;explicitly in the tool description&lt;/strong&gt;, not just mentioned once in the system prompt. That's not redundancy — it's a finding from later iteration (full story in Part 6): policy logic that lives only in the system prompt gets missed under load. It has to be load-bearing in the tool itself.&lt;/p&gt;

&lt;p&gt;The full contract table, matching each tool to its schema and failure mode:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;order_lookup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ order_id }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ status, items, total_usd, order_date }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;order not found → structured error, not a guess&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refund_eligibility&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ order_id, reason }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ eligible, max_amount_usd, policy_ref }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cancelled/undelivered → &lt;code&gt;eligible=false&lt;/code&gt; with a reason; ambiguous → escalate, never assume &lt;code&gt;true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;issue_refund&lt;/code&gt; &lt;em&gt;(gated)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ order_id, amount_usd, idempotency_key }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ confirmation_id }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;idempotency_key&lt;/code&gt; required — prevents double-refund on retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kb_search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ query }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ articles: [...], relevance_scores }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no match → explicit empty result; agent must not fabricate an answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;send_email&lt;/code&gt; &lt;em&gt;(gated)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ to, subject, body, ticket_id }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ sent, message_id }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;gated behind pre-action approval (Part 5)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three details worth calling out specifically:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency keys aren't optional.&lt;/strong&gt; &lt;code&gt;issue_refund&lt;/code&gt; requires one because retries happen — a flaky network call, a model retrying after a transient error — and a refund tool without idempotency protection means a retry can double-refund a customer. This is enforced in the tool executor itself, not assumed from good behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/tools/issueRefund.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;issuedRefunds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// idempotency_key -&amp;gt; confirmation_id&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;issueRefund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idempotency_key&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idempotency_key_required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;issuedRefunds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;confirmation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;replayed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="c1"&gt;// ... issue the refund, store it under idempotencyKey&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure modes are structured, not guessed.&lt;/strong&gt; &lt;code&gt;order_lookup&lt;/code&gt; on a missing order returns &lt;code&gt;{ error: "order_not_found", order_id }&lt;/code&gt;, not an empty object the model has to interpret. &lt;code&gt;kb_search&lt;/code&gt; on no matches returns an explicit empty &lt;code&gt;articles: []&lt;/code&gt;, specifically so the system prompt can say "if this comes back empty, escalate — don't fabricate an answer."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gated tools are marked at the contract level.&lt;/strong&gt; &lt;code&gt;issue_refund&lt;/code&gt; and &lt;code&gt;send_email&lt;/code&gt; carry &lt;code&gt;gated: true&lt;/code&gt; right in their definition. That flag is read by the policy layer (Part 5) to decide which tool calls need a human in the loop before they execute — the gating decision starts here, at the contract, not somewhere downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data source: mock, but shaped like something real
&lt;/h2&gt;

&lt;p&gt;All five tools read from an in-memory mock store — &lt;a href="https://github.com/akash-pal/agent-from-scratch/blob/main/src/data/mockData.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/data/mockData.ts&lt;/code&gt;&lt;/a&gt;: five customers, ten orders spanning inside/outside the refund window, five KB articles. It's deliberately fake data (this repo is a reference build, not a production system — more on that in the repo's own README), but the schemas are shaped to mirror a real commerce API's &lt;code&gt;order&lt;/code&gt;/&lt;code&gt;return&lt;/code&gt;/line-item structure, so swapping in a real backend later is a data-layer change, not a tool-contract rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/akashpal/part-3-build-the-eval-set-before-the-agent-exists-40lk"&gt;Part 3: Build the Eval Set Before the Agent Exists →&lt;/a&gt;&lt;/strong&gt; covers building the eval set — 21 cases written &lt;em&gt;before&lt;/em&gt; the agent loop existed, and why that ordering is the actual point, not a nice-to-have.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>llm</category>
    </item>
    <item>
      <title>Part 1: What Makes Something an Agent (and Why We Built This Without a Framework)</title>
      <dc:creator>Akash Pal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 18:43:44 +0000</pubDate>
      <link>https://dev.to/akashpal/part-1-what-makes-something-an-agent-and-why-we-built-this-without-a-framework-5ccg</link>
      <guid>https://dev.to/akashpal/part-1-what-makes-something-an-agent-and-why-we-built-this-without-a-framework-5ccg</guid>
      <description>&lt;p&gt;Most agent tutorials reach for a framework on line one — LangChain, LangGraph, CrewAI, pick one. This series does the opposite. Over seven parts, we build a real support-ticket agent with &lt;strong&gt;no agent framework at all&lt;/strong&gt;: a hand-written loop against a raw model SDK, explicit guardrails, and an eval set built &lt;em&gt;before&lt;/em&gt; the agent exists.&lt;/p&gt;

&lt;p&gt;The full code is on GitHub: &lt;strong&gt;&lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;akash-pal/agent-from-scratch&lt;/a&gt;&lt;/strong&gt;. Every article in this series links back to the specific files it's talking about — this isn't a toy snippet series, it's a walkthrough of one real (if intentionally small) build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chatbot vs. agent
&lt;/h2&gt;

&lt;p&gt;A chatbot answers once per turn. You send a message, it sends one back, done.&lt;/p&gt;

&lt;p&gt;An agent runs a &lt;strong&gt;loop&lt;/strong&gt;: it decides an action, executes a tool, observes the result, and decides again — repeating until the task is done or it gives up. This is the classic &lt;strong&gt;ReAct&lt;/strong&gt; pattern (Reason + Act):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhhiygje71ylzh6btk4yo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhhiygje71ylzh6btk4yo.png" alt="ReAct Pattern" width="477" height="257"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That loop is the entire reason the rest of this series exists. Because the agent calls tools autonomously and repeatedly, small errors compound across steps in a way a single chatbot turn never does. Propose a refund on the wrong order, and unlike a chatbot giving a wrong answer once, an agent might act on that mistake — call a tool, change state, send an email. The infrastructure this series builds — evals, tracing, guardrails, memory design — exists to &lt;strong&gt;bound and catch that compounding failure&lt;/strong&gt;, not because agents are inherently harder to prompt.&lt;/p&gt;

&lt;p&gt;Three components sit around the loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt; — each is a name + natural-language description + JSON input schema + an executor function. The description is what the LLM reads to decide when to call it — treat it as a specification, not documentation. (More on this in Part 2.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt; — what the agent knows within a run and across runs. (Part 4.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orchestrator&lt;/strong&gt; — the LLM (or a graph/state machine wrapping it) that decides which tool to call next based on the current state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why build it without a framework first
&lt;/h2&gt;

&lt;p&gt;Frameworks pay for themselves under specific conditions: multi-agent orchestration, streaming UI needs, integrated tracing, advanced retry/fallback logic, or a team of people who need a shared abstraction. None of those apply to a first agent built by one person to understand what's actually happening.&lt;/p&gt;

&lt;p&gt;If you can't tell whether LangGraph is adding value or hiding a bug, you don't yet know what LangGraph is doing for you. Writing the raw loop first — even a bad one — gives you a mental model to evaluate any framework against later. That's the actual thesis of this whole series: build the ~100-line version, understand every step of it, &lt;em&gt;then&lt;/em&gt; decide if you need more.&lt;/p&gt;

&lt;h2&gt;
  
  
  The build order this series follows
&lt;/h2&gt;

&lt;p&gt;Every part below produced a real, committed artifact in the repo, in this order — and the order is deliberate, not incidental:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pin the use case&lt;/strong&gt; — bounded input, bounded output, a hard cap on tool count, one measurable success metric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map tools &amp;amp; data sources&lt;/strong&gt; — write every tool contract before writing code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the eval set&lt;/strong&gt; — 21 cases, written before the agent existed, so it measures against a real target instead of tuning to whatever the agent happens to do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the minimal loop&lt;/strong&gt; — the raw ReAct loop, no framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the system prompt&lt;/strong&gt; — six required elements, versioned like code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterate to green&lt;/strong&gt; — one change per fix, and a log of what broke and why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add human review&lt;/strong&gt; — approval gating on consequential actions, decided as an architecture choice, not bolted on after launch.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The use case: an e-commerce support-ticket agent
&lt;/h2&gt;

&lt;p&gt;The concrete system this series builds triages and resolves inbound customer support tickets — order status questions, refund requests, and knowledge-base lookups — for a fictional e-commerce company. Five tools, one agent, real guardrails:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw6or53tlm6b0lpl46hdc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw6or53tlm6b0lpl46hdc.png" alt="Architecture: an inbound ticket first checks an auto-escalate guard (fraud/legal/duplicate patterns), routing to either immediate escalation with zero LLM calls, or the agent loop, which reads/writes memory state and calls five tools — three run freely, two (issue_refund, send_email) are gated behind human approval" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;order_lookup&lt;/code&gt; and &lt;code&gt;kb_search&lt;/code&gt; are read-only and run freely. &lt;code&gt;issue_refund&lt;/code&gt; and &lt;code&gt;send_email&lt;/code&gt; are &lt;strong&gt;gated&lt;/strong&gt; — they require human approval before they execute, because they're consequential and hard to reverse. That distinction — which actions can run autonomously and which need a human in the loop — turns out to be an architecture decision made in Part 1 of the design, not a safety feature bolted on at the end. Part 7 covers exactly how that gating works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/akashpal/part-2-pinning-the-use-case-and-writing-tool-contracts-like-specs-4462"&gt;Part 2: Pinning the Use Case and Writing Tool Contracts Like Specs →&lt;/a&gt;&lt;/strong&gt; covers pinning the use case and writing the five tool contracts — including the specific wording change that fixed a real eval failure later in the series.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/akash-pal/agent-from-scratch" rel="noopener noreferrer"&gt;github.com/akash-pal/agent-from-scratch&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
