<?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: Yaseen Khatib</title>
    <description>The latest articles on DEV Community by Yaseen Khatib (@yaseenyk04).</description>
    <link>https://dev.to/yaseenyk04</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%2F4049819%2F12dcf895-7397-40aa-8f24-b9d5d64f6736.jpg</url>
      <title>DEV Community: Yaseen Khatib</title>
      <link>https://dev.to/yaseenyk04</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yaseenyk04"/>
    <language>en</language>
    <item>
      <title>My Networking Runs Itself: A LinkedIn Pipeline</title>
      <dc:creator>Yaseen Khatib</dc:creator>
      <pubDate>Wed, 04 Nov 2026 12:25:13 +0000</pubDate>
      <link>https://dev.to/yaseenyk04/my-networking-runs-itself-a-linkedin-pipeline-39c8</link>
      <guid>https://dev.to/yaseenyk04/my-networking-runs-itself-a-linkedin-pipeline-39c8</guid>
      <description>&lt;p&gt;[ EXECUTIVE TEARDOWN // TL;DR ]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Consistency is a systems property: a cron-driven GitHub Actions pipeline publishes to LinkedIn and commits its own state ledger back to Git.&lt;/li&gt;
&lt;li&gt;  The human trust boundary is the queue — the machine automates showing up, never the judgment about what carries my name.&lt;/li&gt;
&lt;li&gt;  The pipeline kept publishing through heads-down building weeks, so recruiter conversations arrived on its schedule, not my energy level's.&lt;/li&gt;
&lt;li&gt;  The same queue → scheduled worker → auditable state pattern automates any valuable-but-skipped work inside a company, in days.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Networking is a numbers game played with hours you don't have. Writing a good LinkedIn post takes an evening; doing it consistently takes a discipline that competes with actually building things. So I treated my own visibility as an engineering problem and shipped a product for it: an autonomous pipeline that turns my real work into scheduled LinkedIn content, publishes it, and tracks its own state — entirely on free infrastructure. My network started growing while I slept.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you automate LinkedIn content for $0?
&lt;/h2&gt;

&lt;p&gt;The system is deliberately boring, which is the point. A GitHub Actions workflow wakes on a cron schedule. It reads a content queue, generates the post draft from the queued topic, publishes to LinkedIn, and then — the part most automation skips — commits its own state back to the repository in a &lt;code&gt;published.json&lt;/code&gt; ledger. The Git history is the database. There is no server to babysit, no dashboard subscription, no runtime cost. If it ever misfires, the ledger is a diff I can read and revert like any other commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automation with a quality gate
&lt;/h2&gt;

&lt;p&gt;The word "bot" earns its bad reputation from systems that publish without judgment. Mine has a human trust boundary in the same place all my products do: the queue. Nothing enters it that I haven't decided is worth saying — the pipeline automates the &lt;em&gt;showing up&lt;/em&gt;, not the &lt;em&gt;thinking&lt;/em&gt;. It is the same propose/confirm split that governs my finance agent: the machine handles cadence and delivery, the human owns the content decisions that carry my name.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Consistency is a systems property, not a personality trait. If showing up matters and humans are bad at it, that is not a self-improvement problem — that is a pipeline waiting to be built.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;The pipeline did what consistency always does: compounded. Posts went out on schedule through weeks when I was heads-down shipping Sable and streamerOS — precisely the weeks I would previously have gone silent. Profile visits, connection requests, and recruiter conversations arrived on the pipeline's schedule instead of my energy level's. The product's full architecture breakdown lives &lt;a href="https://yk.streamerosai.com/products/linkedin-pipeline/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;on its product page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transferable lesson for companies
&lt;/h2&gt;

&lt;p&gt;Every company has a version of this problem: work that is valuable, repetitive, and chronically skipped because it competes with "real" work — release notes, changelogs, status updates, onboarding emails, documentation refreshes. The pattern that fixed my networking fixes those too: a queue a human curates, a scheduled worker that executes, state committed somewhere auditable, and zero new infrastructure to own. I've now built that pattern three times (content, outreach, and blog publishing) — it takes days, not quarters, and it never calls in sick.&lt;/p&gt;

&lt;h2&gt;
  
  
  ~/keep-reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://yk.streamerosai.com/blog/zero-dollar-content-engine/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;6 min readThe $0 Content Engine That Runs ItselfA cron-scheduled pipeline drafts articles with Gemini, writes MDX, commits to Git, and redeploys the static site — no CMS, no server, no database, no bill. The architecture of refusing infrastructure.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yk.streamerosai.com/blog/shipped-5-products-solo-12-months/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;6 min readI Shipped 5 Products in 12 Months, SoloFive production systems — a realtime telemetry cockpit, a workflow engine, a local-first AI finance agent, and two autonomous pipelines — designed, built, and shipped by one engineer. Here's the operating model.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yk.streamerosai.com/blog/sable-ai-agent-never-touches-money/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;7 min readAn AI Money Agent That Can't Touch the MoneySable is a local-first AI financial agent: all data in on-device SQLite, no cloud backend, and a Review &amp;amp; Confirm boundary where the model proposes but only a human commits. The trust architecture, explained.&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;YK&lt;/p&gt;

&lt;p&gt;Yaseen Khatib · MERN + AI Architect&lt;/p&gt;

&lt;p&gt;Ships autonomous AI products solo — five in the last twelve months. &lt;a href="https://yk.streamerosai.com/about/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;More about Yaseen →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Want this built for your business?
&lt;/h2&gt;

&lt;p&gt;I build what this post describes for clients — RAG chatbots on your own documents, AI added to systems you already run, custom software — at fixed prices you see before we start. Hiring instead? I'm open to senior and lead roles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yk.streamerosai.com/solutions/business/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;See services and prices →&lt;/a&gt;&lt;a href="https://yk.streamerosai.com/hire/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;I'm hiring&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://yk.streamerosai.com/blog/linkedin-pipeline-job-search-runs-itself/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;yk.streamerosai.com/blog/linkedin-pipeline-job-search-runs-itself/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://yk.streamerosai.com/blog/linkedin-pipeline-job-search-runs-itself/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;my portfolio&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>automation</category>
      <category>linkedin</category>
    </item>
    <item>
      <title>WebSocket Telemetry at Scale</title>
      <dc:creator>Yaseen Khatib</dc:creator>
      <pubDate>Wed, 04 Nov 2026 12:25:12 +0000</pubDate>
      <link>https://dev.to/yaseenyk04/websocket-telemetry-at-scale-3f8o</link>
      <guid>https://dev.to/yaseenyk04/websocket-telemetry-at-scale-3f8o</guid>
      <description>&lt;p&gt;[ EXECUTIVE TEARDOWN // TL;DR ]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  In-memory broadcast only reaches the local process — multi-instance real-time needs a shared backplane (Redis pub/sub).&lt;/li&gt;
&lt;li&gt;  Scope subscriptions into rooms so each client receives only the streams it watches, collapsing bandwidth and CPU.&lt;/li&gt;
&lt;li&gt;  Coalesce high-frequency sources on the server to a renderable frame rate before broadcasting.&lt;/li&gt;
&lt;li&gt;  Handle backpressure at the source, not after it has already flooded the client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single WebSocket server streaming telemetry to a few clients is a weekend project. Feeding thousands across multiple instances is systems work. I learned that on streamerOS: the “one process holds every socket” sketch sailed on one pod and face-planted the moment we scaled out. Each instance owns its own connection table, so A cannot fan out to subscribers that landed on B; broadcasts die at the process boundary and your graphs freeze.&lt;/p&gt;

&lt;h2&gt;
  
  
  The horizontal-scaling problem
&lt;/h2&gt;

&lt;p&gt;Once you run more than one server process behind a load balancer, connections fan out across instances. An event that originates on one instance has to reach subscribers on all of them. In-memory broadcast only reaches the local process; you need a shared backplane so every instance hears every event. In the pattern I call Trinity Architecture, this cross-instance fanout lives in the orchestration tier — the UI stays dumb, render-only, and the adapter keeps payloads lean. I use a Serialization Adapter there too; on IntegrateX it stripped non-essential React Flow UI metadata before persistence and cut graph payloads 94%, and the same habit keeps telemetry frames tight.&lt;/p&gt;

&lt;p&gt;scale.ts&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;// Redis pub/sub backplane → broadcast across every instance&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;createAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@socket.io/redis-adapter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pubClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subClient&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// emit anywhere; every instance's subscribers receive it&lt;/span&gt;
&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tick&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;m&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;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;telemetry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;metric&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rooms, not broadcasts
&lt;/h2&gt;

&lt;p&gt;Do not push every metric to every client. Scope subscriptions into rooms — per dashboard, per device, per tenant — so a client receives only the streams it is watching. This collapses bandwidth and CPU from O(clients × events) toward O(interested clients), which is the difference between a system that scales and one that melts at a few thousand connections. On streamerOS, moving from a global channel to roomed feeds stopped NIC spikes, trimmed CPU interrupts, and cut render thrash when operators flipped between views; the UI joined/left rooms via the orchestrator in my Trinity split, never from components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coalesce at the source
&lt;/h2&gt;

&lt;p&gt;High-frequency sources emit faster than any client can render or any socket should carry. Sample or debounce on the server to a sane frame rate before broadcasting, so you push the latest truth at a rate the wire and the React reconciler can actually absorb. Budget is ~16ms at 60fps; a 200 Hz stream will starve the UI thread. Track last-value-wins per key and pre-aggregate counters server-side. Backpressure handled at the source beats backpressure discovered at the client.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Scaling real-time is not about a faster socket. It is about making sure each event travels exactly as far as it needs to — and no further.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For the single-server fundamentals and the React consumption side, see &lt;a href="https://yk.streamerosai.com/blog/real-time-telemetry-websockets-react/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Real-Time Telemetry&lt;/a&gt;; the live systems are &lt;a href="https://yk.streamerosai.com/#projects?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;streamerOS and the CMZ portal&lt;/a&gt;. The instinct I bring to a team is deciding how far each event should travel before you scale out, not after the graphs freeze.&lt;/p&gt;

&lt;h2&gt;
  
  
  ~/keep-reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://yk.streamerosai.com/blog/real-time-telemetry-websockets-react/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;8 min readReal-Time Telemetry: Why Polling LiesPolling dashboards lie between ticks — I learned that the hard way. Now I push telemetry over WebSockets for sub-second parity across every React client.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yk.streamerosai.com/blog/optimizing-mongodb-aggregation/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;7 min readMongoDB Aggregation Pipelines: Stage Order Is the WinProfiling a clinical API taught me MongoDB aggregation pipeline optimization lives or dies on stage order: $match first on indexes, $lookup join performance, then explain.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yk.streamerosai.com/blog/unifying-twitch-and-youtube-live-chat-ordered-de-duplicated/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;8 min readUnifying Twitch and YouTube live chatBuild a single, ordered, de-duplicated event stream from Twitch IRC and YouTube Live Chat with Hybrid Logical Clocks, watermarks, and idempotent delivery.&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;YK&lt;/p&gt;

&lt;p&gt;Yaseen Khatib · MERN + AI Architect&lt;/p&gt;

&lt;p&gt;Ships autonomous AI products solo — five in the last twelve months. &lt;a href="https://yk.streamerosai.com/about/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;More about Yaseen →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Want this built for your business?
&lt;/h2&gt;

&lt;p&gt;I build what this post describes for clients — RAG chatbots on your own documents, AI added to systems you already run, custom software — at fixed prices you see before we start. Hiring instead? I'm open to senior and lead roles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yk.streamerosai.com/solutions/business/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;See services and prices →&lt;/a&gt;&lt;a href="https://yk.streamerosai.com/hire/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;I'm hiring&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://yk.streamerosai.com/blog/websocket-telemetry-at-scale/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;yk.streamerosai.com/blog/websocket-telemetry-at-scale/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://yk.streamerosai.com/blog/websocket-telemetry-at-scale/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;my portfolio&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>websockets</category>
      <category>realtime</category>
      <category>redis</category>
    </item>
    <item>
      <title>MERN Stack Developer vs Full Stack Developer: What Changes</title>
      <dc:creator>Yaseen Khatib</dc:creator>
      <pubDate>Fri, 30 Oct 2026 10:59:52 +0000</pubDate>
      <link>https://dev.to/yaseenyk04/mern-stack-developer-vs-full-stack-developer-what-changes-1g8m</link>
      <guid>https://dev.to/yaseenyk04/mern-stack-developer-vs-full-stack-developer-what-changes-1g8m</guid>
      <description>&lt;p&gt;[ EXECUTIVE TEARDOWN // TL;DR ]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  A MERN stack developer is a full-stack developer who works in MongoDB, Express, React and Node; full stack is the range, MERN is one choice of tools.&lt;/li&gt;
&lt;li&gt;  MERN fixes one language end to end, a document database by default, and a single npm ecosystem, which is why it suits small teams and real-time products.&lt;/li&gt;
&lt;li&gt;  The titles diverge on relational database experience, real-time work, and server rendering; test for those rather than the acronym.&lt;/li&gt;
&lt;li&gt;  Hire a MERN developer when the product is already MERN; hire a broader full-stack developer when the stack is still a choice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A MERN stack developer is a full-stack developer who has chosen one specific set of tools: MongoDB for the database, Express for the server, React for the interface, and Node.js for the runtime. Every MERN developer is a full-stack developer. Not every full-stack developer works in MERN. That is the whole relationship, and the rest of this post is about what the choice means when you are hiring one or deciding to become one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "full stack" actually covers
&lt;/h2&gt;

&lt;p&gt;Full stack means one person can take a feature from the database schema to the pixels on screen and the deployment in between. The stack itself is open. A full-stack developer might work in Django and Vue, Rails and Hotwire, Laravel and Blade, Spring and Angular, or Next.js and Postgres. What makes them full stack is the range, not the tools.&lt;/p&gt;

&lt;p&gt;In practice the title hides a spread. Some full-stack developers are backend engineers who can write enough React to ship a form. Others are front-end specialists who can stand up an API. The useful question in an interview is never "are you full stack" but "which half would you reach for first, and how far does the other half go."&lt;/p&gt;

&lt;h2&gt;
  
  
  What MERN narrows down
&lt;/h2&gt;

&lt;p&gt;MERN fixes every layer to JavaScript or TypeScript. That has three consequences that matter more than the acronym.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;One language end to end.&lt;/strong&gt; The same types, the same validation logic and often the same utility code run in the browser and on the server. On a small team that removes a whole category of mismatch bugs between what the API sends and what the UI expects.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;A document database by default.&lt;/strong&gt; MongoDB stores JSON shaped documents, which maps cleanly onto the objects a React app already handles. It also means schema discipline is on the developer, not the database. Aggregation pipelines and index design become the performance work, as I covered in &lt;a href="https://yaseenkhatib.streamerosai.com/blog/optimizing-mongodb-aggregation/" rel="noopener noreferrer"&gt;optimising MongoDB aggregation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;A single ecosystem.&lt;/strong&gt; npm, the Node event loop, Express middleware and React's component model are one world. A MERN developer goes deep in it rather than wide across several.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where the two titles diverge in real work
&lt;/h2&gt;

&lt;p&gt;The difference shows up in three places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Databases.&lt;/strong&gt; A general full-stack developer usually has relational experience: joins, transactions, migrations, constraints enforced by the database. A MERN developer who has only worked with MongoDB may not. The reverse is also true: relational-first developers often model documents badly, embedding what should be referenced and referencing what should be embedded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-time and streaming.&lt;/strong&gt; Node's event loop makes WebSockets and streaming responses natural, which is why a lot of real-time product work ends up in MERN shops. I wrote up the pattern in &lt;a href="https://yaseenkhatib.streamerosai.com/blog/websocket-telemetry-at-scale/" rel="noopener noreferrer"&gt;WebSocket telemetry at scale&lt;/a&gt;. A full-stack developer from a request-response framework has to learn that model rather than inherit it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rendering.&lt;/strong&gt; Plain MERN is a client-rendered React app talking to an Express API. Modern full-stack JavaScript work has largely moved to frameworks like Next.js that render on the server or at build time, which changes how data loading, caching and SEO work. Many MERN developers have made that move; the acronym has not caught up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one to hire
&lt;/h2&gt;

&lt;p&gt;Hire for the problem, then check the stack.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  If your product is already in MongoDB, Express, React and Node, hire a MERN developer and test depth: schema design, aggregation, authentication, and how they structure a React app once it has more than twenty components.&lt;/li&gt;
&lt;li&gt;  If you are choosing a stack, hire a full-stack developer who has shipped in more than one and ask them to argue against MERN. The honest answer sometimes is Postgres.&lt;/li&gt;
&lt;li&gt;  If the work involves AI features, the stack matters less than whether the person has put a model into production behind a real interface: retrieval, evaluation, cost control, and the boundary between what the model proposes and what the system commits. My own path went MERN first and then into that, which I described in &lt;a href="https://yaseenkhatib.streamerosai.com/blog/my-journey-web-developer-to-ai-engineer/" rel="noopener noreferrer"&gt;from web developer to AI engineer&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which one to become
&lt;/h2&gt;

&lt;p&gt;Start with MERN or any single coherent stack, because depth in one place teaches you what the layers are for. Then deliberately break the acronym: learn a relational database, learn a server-rendering framework, and ship something with a model in it. At that point the title on your CV is full-stack developer, and MERN is the first line under it rather than the whole story.&lt;/p&gt;

&lt;p&gt;If you are hiring for either, the &lt;a href="https://yaseenkhatib.streamerosai.com/hire/" rel="noopener noreferrer"&gt;hire page&lt;/a&gt; describes what I build and how I work.&lt;/p&gt;

&lt;h2&gt;
  
  
  ~/keep-reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/ai-engineer-portfolio-examples/" rel="noopener noreferrer"&gt;7 min readAI Engineer Portfolio Examples: What Reviewers VerifyFour kinds of AI engineer portfolio pieces that let a reviewer verify production experience in minutes: a live grounded assistant, a product with a readable trust boundary, a measured result, and the operating model. With this site as the worked example and its failures included.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/my-journey-web-developer-to-ai-engineer/" rel="noopener noreferrer"&gt;6 min readHow I Went From My First Dev Job to Building AI SystemsFive years, honestly told: an ASP.NET healthcare backend in 2021, full-stack MERN through the ChatGPT shift, nights of embeddings homework, and finally shipping five AI products solo. The journey, without the gloss.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/hire-my-head-not-my-hands/" rel="noopener noreferrer"&gt;6 min readHire My Head, Not My HandsAI made hands abundant; heads stayed scarce. The closing argument of the Founder's Log series — the architectural judgment pattern across five shipped products, addressed to the person deciding whether to interview me.&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;YK&lt;/p&gt;

&lt;p&gt;Yaseen Khatib · Full-Stack AI Engineer&lt;/p&gt;

&lt;p&gt;Ships autonomous AI products solo — five in the last twelve months. &lt;a href="https://yaseenkhatib.streamerosai.com/about/" rel="noopener noreferrer"&gt;More about Yaseen →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Want this built for your business?
&lt;/h2&gt;

&lt;p&gt;I build what this post describes for clients — RAG chatbots on your own documents, AI added to systems you already run, custom software — at fixed prices you see before we start. Hiring instead? I'm open to senior and lead roles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/solutions/business/" rel="noopener noreferrer"&gt;See services and prices →&lt;/a&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/hire/" rel="noopener noreferrer"&gt;I'm hiring&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://yaseenkhatib.streamerosai.com/blog/mern-stack-developer-vs-full-stack-developer/" rel="noopener noreferrer"&gt;yaseenkhatib.streamerosai.com/blog/mern-stack-developer-vs-full-stack-developer/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mern</category>
      <category>full</category>
      <category>what</category>
    </item>
    <item>
      <title>AI Engineer Portfolio Examples: What Reviewers Verify</title>
      <dc:creator>Yaseen Khatib</dc:creator>
      <pubDate>Fri, 30 Oct 2026 10:59:48 +0000</pubDate>
      <link>https://dev.to/yaseenyk04/ai-engineer-portfolio-examples-what-reviewers-verify-4b80</link>
      <guid>https://dev.to/yaseenyk04/ai-engineer-portfolio-examples-what-reviewers-verify-4b80</guid>
      <description>&lt;p&gt;[ EXECUTIVE TEARDOWN // TL;DR ]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Reviewers check whether anything runs, whether you made the hard decisions, whether you can explain a trade-off, and whether anything is measured.&lt;/li&gt;
&lt;li&gt;  The strongest piece is a live retrieval assistant on the portfolio itself, with a visible refusal path and its index exposed as an MCP server.&lt;/li&gt;
&lt;li&gt;  A product with a readable trust boundary, such as a propose-and-confirm agent, gives reviewers something to disagree with, which is the point.&lt;/li&gt;
&lt;li&gt;  Include one measured before-and-after result and one plainly told failure; both raise credibility more than another success.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An AI engineer portfolio has one job: let a stranger verify, in a few minutes and without talking to you, that you have put a model into production and understood what happened. Most portfolios fail that test not because the work is weak but because the site shows a list of technologies instead of a system someone can inspect. This post is the checklist I used to build mine, with this site as the worked example, including the parts I got wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a hiring manager is actually checking
&lt;/h2&gt;

&lt;p&gt;Nobody reading an AI engineer portfolio is impressed by the words RAG, agents or fine-tuning any more. They are looking for four things, in roughly this order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Does anything here run?&lt;/strong&gt; A live system beats a screenshot, and a screenshot beats a bullet point.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Did you make the hard decisions, or follow a tutorial?&lt;/strong&gt; Retrieval thresholds, refusal behaviour, cost controls, the boundary between what the model proposes and what the system commits.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Can you explain a trade-off in writing?&lt;/strong&gt; One honest paragraph about a failure is worth more than ten about a success.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Is any of it measured?&lt;/strong&gt; Latency, cost per request, eval scores, a payload reduction with a before and after.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Example 1: a live, grounded assistant on the site itself
&lt;/h2&gt;

&lt;p&gt;The strongest example an AI engineer can put on a portfolio is a retrieval system that answers questions about the portfolio. It is self-demonstrating: the visitor asks, the system cites its source, and when asked something the site does not cover, it refuses instead of guessing.&lt;/p&gt;

&lt;p&gt;On this site that is the concierge on the home page. It runs on a Cloudflare Worker, embeds the question with Workers AI, retrieves from a Vectorize index built from the site's own content, and declines below a confidence threshold. The same index is exposed as an MCP server, so a reviewer can connect it from inside Claude and interrogate it there. The design decisions are written up in &lt;a href="https://yaseenkhatib.streamerosai.com/blog/semantic-caching-edge-rag/" rel="noopener noreferrer"&gt;semantic caching for edge RAG&lt;/a&gt; and the pipeline is re-indexed automatically after every deploy.&lt;/p&gt;

&lt;p&gt;What makes it a portfolio piece rather than a widget is that the refusal path is visible. Ask it something it cannot know and watch it say so. That one behaviour answers the "did you follow a tutorial" question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 2: a product with a trust boundary you can read
&lt;/h2&gt;

&lt;p&gt;The second example is a shipped product where the AI can do damage if it is wrong, and the architecture stops it. For me that is &lt;a href="https://yaseenkhatib.streamerosai.com/products/sable/" rel="noopener noreferrer"&gt;Sable&lt;/a&gt;, a local-first finance agent. Every record lives in on-device SQLite with no cloud backend, and every action the model wants to take is rendered as a review card a person confirms. A hallucination's blast radius is one dismissed card.&lt;/p&gt;

&lt;p&gt;The portfolio piece is not the app. It is the &lt;a href="https://yaseenkhatib.streamerosai.com/blog/sable-ai-agent-never-touches-money/" rel="noopener noreferrer"&gt;write-up of why the model never touches money&lt;/a&gt;. A reviewer can disagree with the design, and that is the point: it gives them something to disagree with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 3: a measured engineering result
&lt;/h2&gt;

&lt;p&gt;Every AI engineer portfolio should contain at least one number that was measured rather than estimated, with the before and the after. Mine is a &lt;a href="https://yaseenkhatib.streamerosai.com/blog/94-percent-payload-reduction-react-flow/" rel="noopener noreferrer"&gt;94% payload reduction&lt;/a&gt; on a workflow engine's graph serialisation. It is not an AI result. It is there because it shows the habit of measuring, which is the same habit that makes LLM cost control and evals possible. The FinOps side of that habit is in the &lt;a href="https://yaseenkhatib.streamerosai.com/blog/ai-finops-playbook-stop-burning-money/" rel="noopener noreferrer"&gt;AI FinOps playbook&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 4: the operating model, written down
&lt;/h2&gt;

&lt;p&gt;Senior roles are increasingly about how you work with AI tools, not only how you build AI products. A portfolio can show that directly: a description of the workflow, what is automated, what is reviewed, and what the constitution files say. Mine is in &lt;a href="https://yaseenkhatib.streamerosai.com/blog/one-architect-claude-mcp-full-squad/" rel="noopener noreferrer"&gt;one architect, Claude Code and MCP&lt;/a&gt;, with a 30-day plan a team could copy. Pipelines on this site, such as the weekly AI-drafted post that opens a pull request for review rather than publishing, are that model running in public.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got wrong, which is also an example
&lt;/h2&gt;

&lt;p&gt;For four months this site was invisible to Google because an entrance animation shipped its hidden state into the static HTML, so the crawler's snapshot was blank. A separate mistake before that was an auto-publishing pipeline that flooded the site with commodity explainer posts, which got the whole site treated as low value. Both are documented in the repository and both are on this page on purpose. An AI engineer portfolio that admits what broke is more credible than one that does not, because every reviewer knows something broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist you can apply to your own
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  One live system a visitor can poke, with a visible refusal or guardrail.&lt;/li&gt;
&lt;li&gt;  One product where the AI could do harm and the architecture prevents it, with the reasoning written down.&lt;/li&gt;
&lt;li&gt;  One measured result with a before and after, even if it is not about AI.&lt;/li&gt;
&lt;li&gt;  One description of how you work with AI tools, specific enough to copy.&lt;/li&gt;
&lt;li&gt;  One failure, told plainly.&lt;/li&gt;
&lt;li&gt;  Source code or a repository link for at least one of the above.&lt;/li&gt;
&lt;li&gt;  No fabricated metrics, testimonials or dates. Reviewers check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Templates and themes do not matter much. The list above does. If you would rather see the whole thing applied, the &lt;a href="https://yaseenkhatib.streamerosai.com/projects/" rel="noopener noreferrer"&gt;projects page&lt;/a&gt; is the index and the &lt;a href="https://yaseenkhatib.streamerosai.com/hire/" rel="noopener noreferrer"&gt;hire page&lt;/a&gt; is where it leads.&lt;/p&gt;

&lt;h2&gt;
  
  
  ~/keep-reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/mern-stack-developer-vs-full-stack-developer/" rel="noopener noreferrer"&gt;6 min readMERN Stack Developer vs Full Stack Developer: What ChangesEvery MERN developer is a full-stack developer, but not the other way round. What MongoDB, Express, React and Node fix in place, where the two titles diverge in real work, and which one to hire or become.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/ai-native-portfolio-landing-lead-roles/" rel="noopener noreferrer"&gt;6 min readThe AI-Native Portfolio: Landing Lead RolesA portfolio that lists 'used ChatGPT' reads junior. One that demonstrates systems architecture — RAG, agents, guardrails, latency budgets — reads like the lead they're trying to hire. Here's how to build the second one.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/whatsapp-chatbot-saas-vs-owning-it/" rel="noopener noreferrer"&gt;8 min readWhatsApp Chatbot for Business: Monthly SaaS vs Owning ItWhat a WhatsApp chatbot really costs in India: Meta's per-message fees, WATI and Interakt plans over three years, and when an owned AI bot is cheaper.&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;YK&lt;/p&gt;

&lt;p&gt;Yaseen Khatib · Full-Stack AI Engineer&lt;/p&gt;

&lt;p&gt;Ships autonomous AI products solo — five in the last twelve months. &lt;a href="https://yaseenkhatib.streamerosai.com/about/" rel="noopener noreferrer"&gt;More about Yaseen →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Want this built for your business?
&lt;/h2&gt;

&lt;p&gt;I build what this post describes for clients — RAG chatbots on your own documents, AI added to systems you already run, custom software — at fixed prices you see before we start. Hiring instead? I'm open to senior and lead roles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/solutions/business/" rel="noopener noreferrer"&gt;See services and prices →&lt;/a&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/hire/" rel="noopener noreferrer"&gt;I'm hiring&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://yaseenkhatib.streamerosai.com/blog/ai-engineer-portfolio-examples/" rel="noopener noreferrer"&gt;yaseenkhatib.streamerosai.com/blog/ai-engineer-portfolio-examples/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machine</category>
    </item>
    <item>
      <title>WhatsApp Chatbot for Business: Monthly SaaS vs Owning It</title>
      <dc:creator>Yaseen Khatib</dc:creator>
      <pubDate>Sat, 24 Oct 2026 09:01:42 +0000</pubDate>
      <link>https://dev.to/yaseenyk04/whatsapp-chatbot-for-business-monthly-saas-vs-owning-it-1mj1</link>
      <guid>https://dev.to/yaseenyk04/whatsapp-chatbot-for-business-monthly-saas-vs-owning-it-1mj1</guid>
      <description>&lt;p&gt;[ EXECUTIVE TEARDOWN // TL;DR ]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Every WhatsApp chatbot pays Meta per delivered message; service replies to customers who wrote first include 1,000 free a month per number from 1 October 2026.&lt;/li&gt;
&lt;li&gt;  Subscription platforms add a plan fee and often a per-message markup; an AI add-on can roughly double the plan fee.&lt;/li&gt;
&lt;li&gt;  Over three years, plan fees for an AI-answering setup run to around ₹1.9–2.1 lakh before GST on the plans compared here; a one-time build costs about one year of that.&lt;/li&gt;
&lt;li&gt;  Rent for broadcasts, team inbox and menu bots; own the bot when it must answer from your documents, refuse rather than guess, or act on your systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Short answer: rent a WhatsApp chatbot platform if what you need is broadcasts, a shared team inbox and menu-style flows. Own the bot if it has to answer questions from your own documents or act on your own systems.&lt;/strong&gt; Either way you pay Meta for messages. The decision is about what you pay on top of that, every month, for as long as the bot runs.&lt;/p&gt;

&lt;p&gt;Every page that ranks for “WhatsApp chatbot” in India is written by a company selling a subscription to one, so none of them works through the comparison below. I build these, so I have a bias too. I have tried to make it visible by saying where renting is the better call. All prices were checked on the vendors' own pricing pages on 23 October 2026, and they change, so check them again before you sign anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost nobody avoids: Meta's per-message fees
&lt;/h2&gt;

&lt;p&gt;A real WhatsApp chatbot runs on the WhatsApp Business Platform (the API), not the free WhatsApp Business app. Since 1 July 2025 Meta charges per delivered message, priced by category and by the recipient's country. Indian businesses can now be billed in rupees, and Meta requires accounts billed in India to move to INR by 31 December 2026.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Marketing&lt;/strong&gt; templates (offers, broadcasts) are the expensive category, a little under a rupee each on Meta's INR rate card.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Utility&lt;/strong&gt; (order updates) and &lt;strong&gt;authentication&lt;/strong&gt; (OTPs) cost a fraction of that.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Service&lt;/strong&gt; messages, your replies when a customer writes to you first, are what a support chatbot mostly sends. From 1 October 2026 each business number gets 1,000 free service messages a month, and the 1,001st is charged.&lt;/li&gt;
&lt;li&gt;  Conversations that start from a Click-to-WhatsApp ad open a free window of up to seven days.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A bot that mainly answers incoming questions sits largely inside the cheap or free categories. A bot that mainly sends promotions does not, and no platform choice changes that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the subscription platforms add
&lt;/h2&gt;

&lt;p&gt;Platforms such as WATI and Interakt are Business Solution Providers: they connect you to the API and give you a dashboard. You pay a plan fee, and on top of Meta's rate, often a per-message markup.&lt;/p&gt;

&lt;p&gt;Platform and plan&lt;/p&gt;

&lt;p&gt;Plan fee (INR, + GST)&lt;/p&gt;

&lt;p&gt;AI that answers questions&lt;/p&gt;

&lt;p&gt;WATI Growth&lt;/p&gt;

&lt;p&gt;₹2,699 / month, billed quarterly&lt;/p&gt;

&lt;p&gt;1 AI agent, 2,000 AI credits&lt;/p&gt;

&lt;p&gt;WATI Pro&lt;/p&gt;

&lt;p&gt;₹5,799 / month, billed quarterly&lt;/p&gt;

&lt;p&gt;2 AI agents, 5,000 AI credits&lt;/p&gt;

&lt;p&gt;WATI Business&lt;/p&gt;

&lt;p&gt;₹16,799 / month, billed quarterly&lt;/p&gt;

&lt;p&gt;Unlimited agents, 20,000 AI credits&lt;/p&gt;

&lt;p&gt;Interakt Growth&lt;/p&gt;

&lt;p&gt;₹2,799 / month&lt;/p&gt;

&lt;p&gt;Add-on: WhatsApp AI Agents, ₹2,499 / month for 100 AI messages, then ₹0.50 each&lt;/p&gt;

&lt;p&gt;Interakt Advanced&lt;/p&gt;

&lt;p&gt;₹3,799 / month&lt;/p&gt;

&lt;p&gt;Same add-on&lt;/p&gt;

&lt;p&gt;The markup is easiest to see on Interakt, which publishes its per-message rates: a service message is ₹0.128 on Growth, against ₹0.115, the utility and authentication rate on Meta's INR rate card for India. Only Interakt's Enterprise plan says it has no markup. WATI's page says per-message charges vary by category and links its rate card separately.&lt;/p&gt;

&lt;p&gt;None of this is a scandal. A team inbox, a broadcast scheduler, template management and a Shopify connector are real software, and building them yourself would cost far more than the subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three years of each, worked through
&lt;/h2&gt;

&lt;p&gt;An illustration, not a client: a business that wants an AI assistant answering customer questions on WhatsApp, with a couple of staff handing over to a person when the bot cannot help. Plan fees only, before GST and before Meta's charges, which are the same in every column.&lt;/p&gt;

&lt;p&gt;Interakt Growth + AI Agents&lt;/p&gt;

&lt;p&gt;WATI Pro&lt;/p&gt;

&lt;p&gt;Owned build&lt;/p&gt;

&lt;p&gt;Per month&lt;/p&gt;

&lt;p&gt;₹5,298, plus ₹0.50 per AI message after 100&lt;/p&gt;

&lt;p&gt;₹5,799&lt;/p&gt;

&lt;p&gt;Hosting and model usage only&lt;/p&gt;

&lt;p&gt;36 months of plan fees&lt;/p&gt;

&lt;p&gt;₹1,90,728&lt;/p&gt;

&lt;p&gt;₹2,08,764&lt;/p&gt;

&lt;p&gt;none&lt;/p&gt;

&lt;p&gt;Up front&lt;/p&gt;

&lt;p&gt;none&lt;/p&gt;

&lt;p&gt;none&lt;/p&gt;

&lt;p&gt;the build, from $800 at my published price&lt;/p&gt;

&lt;p&gt;At current exchange rates the build costs about what one year of either plan does, so the owned bot breaks even around the end of the first year and is ahead from then on. The Interakt column is also the optimistic one: 100 AI messages a month is a few a day, so any real volume adds the per-message AI charge on top. An owned bot's model usage depends on your volume and the model you choose; for short answers grounded in a few documents it is small, and it can be estimated before you commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  When renting is the right answer
&lt;/h2&gt;

&lt;p&gt;If the job is broadcasts to a customer list, abandoned-cart reminders from Shopify, catalogues and a shared inbox for five people, rent a platform. That is exactly what they are built for, it works on day one, and building it yourself would be paying to recreate a product that already exists. The same goes for a menu bot (press 1 for timings, 2 for location) with no AI in it.&lt;/p&gt;

&lt;p&gt;Renting also wins if nobody in the business will own a piece of software. An owned bot needs someone, in-house or hired, to keep the WhatsApp app credentials valid, update the documents it answers from, and fix it when Meta changes something.&lt;/p&gt;

&lt;h2&gt;
  
  
  When owning it is the right answer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;It must answer from your documents.&lt;/strong&gt; Policies, product manuals, fee structures, a price list that changes monthly. This is retrieval-augmented generation (RAG): the bot looks up the relevant passages and answers only from them.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;It must refuse rather than guess.&lt;/strong&gt; When nothing relevant is found above a similarity threshold, a well-built bot says it does not know and hands over to a person. A generic AI agent tuned for engagement will often improvise.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;It must touch your systems.&lt;/strong&gt; Look up an order in your own database, book a slot in your own calendar, write a lead into your own CRM.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;You want the data.&lt;/strong&gt; The conversations and the customer records live in a database you own, not in a vendor's dashboard you export from.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What an owned WhatsApp chatbot actually is
&lt;/h2&gt;

&lt;p&gt;Less than people expect. Meta's Cloud API sends every incoming message to a webhook you host. Your code verifies it, retrieves the relevant passages, asks a model for an answer grounded in them, and posts the reply back to the Graph API. A minimal handler:&lt;/p&gt;

&lt;p&gt;webhook.ts&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;// 1. Meta verifies the webhook once with a GET request.&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hub.verify_token&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VERIFY_TOKEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ok&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hub.challenge&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forbidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Every customer message arrives as a POST.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entry&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;changes&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;value&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;passages&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;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&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;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// vector search&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="nx"&gt;passages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;answerFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;passages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&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;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// grounded reply&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;I don't have that in our documents. A person will reply shortly.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;GRAPH_URL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&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;PHONE_NUMBER_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;/messages&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&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;WA_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&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="na"&gt;messaging_product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;whatsapp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="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;text&lt;/span&gt;&lt;span class="dl"&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;answer&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 reply goes out inside the 24-hour window the customer opened, so it is a service message. The real work sits in &lt;code&gt;retrieve&lt;/code&gt; and &lt;code&gt;answerFrom&lt;/code&gt;: splitting documents sensibly, choosing the threshold, and checking that answers cite what they used. That is also where most cheap chatbots cut corners.&lt;/p&gt;

&lt;p&gt;Two things this does not remove. Outbound messages outside that window still need templates approved by Meta, owned bot or not. And your number still needs a verified Meta Business account, which takes paperwork rather than code.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick way to decide
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; Write down the ten questions customers ask most. If a menu answers them, rent a platform or use the free app's quick replies.&lt;/li&gt;
&lt;li&gt; If the answers live in documents that change, or need a lookup in your systems, you need RAG and probably an owned bot.&lt;/li&gt;
&lt;li&gt; Multiply the platform's plan plus its AI add-on by 36 and set that next to a one-time build plus its running cost.&lt;/li&gt;
&lt;li&gt; Decide who maintains it. If the answer is nobody, rent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the answer points to owning it, the &lt;a href="https://yaseenkhatib.streamerosai.com/solutions/ai-chatbot/" rel="noopener noreferrer"&gt;AI chatbot page&lt;/a&gt; has my fixed prices and a live assistant on this site built the same way, which you can try before deciding anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  ~/keep-reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/mastering-claude-architecture-context-windows-output-limits/" rel="noopener noreferrer"&gt;9 min readBudgeting the Claude Context Window Before It Truncates YouThe Claude context window is one token budget shared by prompt, tools, thinking, and reply. I count tokens and guard it in code so a reply never truncates.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/ai-native-portfolio-landing-lead-roles/" rel="noopener noreferrer"&gt;6 min readThe AI-Native Portfolio: Landing Lead RolesA portfolio that lists 'used ChatGPT' reads junior. One that demonstrates systems architecture — RAG, agents, guardrails, latency budgets — reads like the lead they're trying to hire. Here's how to build the second one.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/chatgpt-changed-full-stack-engineering/" rel="noopener noreferrer"&gt;6 min readThe Day ChatGPT Changed Full-Stack EngineeringLate 2022 dropped the cost of writing software overnight. Why the bottleneck moved from syntax to judgment — and why MERN engineers were positioned to win.&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;YK&lt;/p&gt;

&lt;p&gt;Yaseen Khatib · Full-Stack AI Engineer&lt;/p&gt;

&lt;p&gt;Ships autonomous AI products solo — five in the last twelve months. &lt;a href="https://yaseenkhatib.streamerosai.com/about/" rel="noopener noreferrer"&gt;More about Yaseen →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Want this built for your business?
&lt;/h2&gt;

&lt;p&gt;I build what this post describes for clients — RAG chatbots on your own documents, AI added to systems you already run, custom software — at fixed prices you see before we start. Hiring instead? I'm open to senior and lead roles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/solutions/business/" rel="noopener noreferrer"&gt;See services and prices →&lt;/a&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/hire/" rel="noopener noreferrer"&gt;I'm hiring&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://yaseenkhatib.streamerosai.com/blog/whatsapp-chatbot-saas-vs-owning-it/" rel="noopener noreferrer"&gt;yaseenkhatib.streamerosai.com/blog/whatsapp-chatbot-saas-vs-owning-it/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>whatsapp</category>
    </item>
    <item>
      <title>Real-Time Telemetry: Why Polling Lies, and WebSockets Don't</title>
      <dc:creator>Yaseen Khatib</dc:creator>
      <pubDate>Wed, 23 Sep 2026 09:27:45 +0000</pubDate>
      <link>https://dev.to/yaseenyk04/real-time-telemetry-why-polling-lies-and-websockets-dont-10d2</link>
      <guid>https://dev.to/yaseenyk04/real-time-telemetry-why-polling-lies-and-websockets-dont-10d2</guid>
      <description>&lt;p&gt;[ EXECUTIVE TEARDOWN // TL;DR ]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Polling can't be both fresh and cheap; a push model has the server speak only when state actually changes.&lt;/li&gt;
&lt;li&gt;  Broadcast events over a persistent socket and map them onto React state with a thin subscription.&lt;/li&gt;
&lt;li&gt;  Coalesce high-frequency sources on the server to a renderable frame rate, and isolate live state so only affected components repaint.&lt;/li&gt;
&lt;li&gt;  Single-source-of-truth sync — every client identical in the same instant — is what eliminates a whole class of stale-data errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After shipping enough "live" dashboards — including the real-time layer of the CMZ portal — one truth stuck: a polling dashboard is lying most of the time. Between ticks, operators act on stale numbers; shrink the window and you DoS your own APIs with empty work. For telemetry — CPU, chat velocity, scene state — stop interrogating the server and let it speak. WebSockets push changes the instant they occur, giving every client the same value in the same frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why polling fails at scale
&lt;/h2&gt;

&lt;p&gt;Polling forces a bad trade: either ship latency or ship waste. At 5s, your UI is up to 5s wrong and your fleet fires a wall of redundant reads. At 500ms, you approach freshness while torching CPU, caches, and rate limits with "nothing changed" responses. There isn't a magic interval; the question itself is expensive. Flip the model so the server only emits when state actually mutates.&lt;/p&gt;

&lt;h2&gt;
  
  
  The push architecture
&lt;/h2&gt;

&lt;p&gt;The backend owns an event stream and broadcasts to subscribers over a persistent socket. No request fan-out, no timers — a value flips, the server emits, every dashboard updates in the same frame. In the pattern I call Trinity Architecture, the socket handler lives in the Reactive State / Orchestration layer, the Presentation layer just renders from state and dispatches intents, and a thin Data / Serialization Adapter shapes wire events into the in-memory model. No layer talks past its neighbor, which keeps both payloads and coupling under control.&lt;/p&gt;

&lt;p&gt;telemetry.ts&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;// server: broadcast on change, not on request&lt;/span&gt;
&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tick&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;m&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;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;telemetry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// client: subscribe once, render on push&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;socket&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;telemetry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMetrics&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&amp;gt;, &lt;/span&gt;&lt;span class="se"&gt;[])&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Throttle the wire, not the truth
&lt;/h3&gt;

&lt;p&gt;A telemetry source can fire hundreds of events per second. Humans can't read that, and React can't reconcile that without jank. Shape the firehose at the edge — sample or debounce to a renderable cadence before broadcast — so the feed is truthful but frame-friendly. On &lt;a href="https://yaseenkhatib.streamerosai.com/#projects" rel="noopener noreferrer"&gt;streamerOS&lt;/a&gt; we had to respect 60fps while chat and system metrics spiked; coalescing server-side kept backpressure off the socket and the main thread, without lying about the latest value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consuming streams in React without thrashing
&lt;/h2&gt;

&lt;p&gt;The foot-gun is wiring "telemetry" to setState and letting it reflow the whole tree each packet. Keep hot values in a small, focused store and subscribe only the components that care. I use a Trinity split: Presentation renders from selectors; the orchestrator (Zustand or an event bus) owns the source of truth and optimistic merges; the adapter translates over-the-wire shapes into minimal, denormalized state. A CPU tick should repaint one gauge, not your navbar, tables, and modals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this kills manual errors
&lt;/h2&gt;

&lt;p&gt;When every client observes the same state at the same moment, an entire class of "acted on stale data" mistakes disappears. No operator is reconciling conflicting numbers; no action races a 3s-old snapshot. The win isn't speed for its own sake — it's a single, timely source of truth that every operator shares, so decisions stop diverging.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Polling asks "has it changed?" a thousand times a minute. Push answers once, the moment it does. At scale, that's the line between "live" and "laggy."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the live-data backbone of &lt;a href="https://yaseenkhatib.streamerosai.com/#projects" rel="noopener noreferrer"&gt;streamerOS&lt;/a&gt; and the real-time sync layer in the &lt;a href="https://yaseenkhatib.streamerosai.com/#projects" rel="noopener noreferrer"&gt;CMZ portal&lt;/a&gt; — the same playbook I reach for when correctness, smoothness, and cost need to coexist. Deciding when a system should push instead of being asked is the kind of judgment I bring to a team.&lt;/p&gt;

&lt;h2&gt;
  
  
  ~/keep-reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/websocket-telemetry-at-scale/" rel="noopener noreferrer"&gt;8 min readWebSocket Telemetry at Scale: When One Process Isn't EnoughA single WebSocket server is a weekend project; streaming telemetry to thousands across instances broke for me on streamerOS — Redis pub/sub, rooms, coalescing.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/websocket-reconnect-state-machine-react-node/" rel="noopener noreferrer"&gt;8 min readA WebSocket reconnect state machine for React and NodeReconnection logic written as ad-hoc flags always breaks. An explicit state machine with backoff, jitter and heartbeats survives flaky networks and server restarts.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/halving-websocket-payloads-messagepack-node-react/" rel="noopener noreferrer"&gt;8 min readHalving WebSocket payloads with MessagePack in Node and ReactWhere JSON over WebSockets actually costs you, what MessagePack does and does not fix, and how to batch high-frequency events so the wire is not the bottleneck.&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;YK&lt;/p&gt;

&lt;p&gt;Yaseen Khatib · MERN + AI Architect&lt;/p&gt;

&lt;p&gt;Ships autonomous AI products solo — five in the last twelve months. &lt;a href="https://yaseenkhatib.streamerosai.com/about/" rel="noopener noreferrer"&gt;More about Yaseen →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Need an engineer who can build this?
&lt;/h2&gt;

&lt;p&gt;I'm Yaseen Khatib — a Senior Full-Stack AI Engineer (MERN + TypeScript) who ships production AI systems solo. Open to senior and lead roles, remote or on-site.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/#contact" rel="noopener noreferrer"&gt;Get in touch →&lt;/a&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/products/" rel="noopener noreferrer"&gt;See what I've shipped&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://yaseenkhatib.streamerosai.com/blog/real-time-telemetry-websockets-react/" rel="noopener noreferrer"&gt;yaseenkhatib.streamerosai.com/blog/real-time-telemetry-websockets-react/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>websockets</category>
      <category>realtime</category>
    </item>
    <item>
      <title>Zero-Account Auth: Device-Bound Identity + Capability Tokens</title>
      <dc:creator>Yaseen Khatib</dc:creator>
      <pubDate>Wed, 23 Sep 2026 01:56:03 +0000</pubDate>
      <link>https://dev.to/yaseenyk04/zero-account-auth-device-bound-identity-capability-tokens-3ghe</link>
      <guid>https://dev.to/yaseenyk04/zero-account-auth-device-bound-identity-capability-tokens-3ghe</guid>
      <description>&lt;h2&gt;
  
  
  Why “zero-account” and what problem does it solve?
&lt;/h2&gt;

&lt;p&gt;In a zero-account model, the device is the principal. It generates a key pair and the public key, or its thumbprint, is the identity you use everywhere. Authorization comes from signed capability tokens that spell out what the device is allowed to do.&lt;/p&gt;

&lt;p&gt;You get rid of passwords and most PII. You also remove the central auth server from the hot path. The result still has strong, auditable security properties.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you authenticate devices without an auth server?
&lt;/h2&gt;

&lt;p&gt;Use proof-of-possession. Each device creates a non-extractable key pair locally. Every request carries two artifacts: a DPoP proof (a signed JWT with the HTTP method, target URL, and a fresh jti) and a capability token tied to that key. Resource servers verify the DPoP and check that the capability’s confirmation claim matches the device key thumbprint. No central login step is required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core pieces
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Device key pair (identity): P-256 or Ed25519, with the private key held in a platform keystore or as a non-extractable WebCrypto key.&lt;/li&gt;
&lt;li&gt;DPoP proof per request: a signed JWT carrying htm/htu/iat/jti to stop replay and bind requests to the device key.&lt;/li&gt;
&lt;li&gt;Capability token: a signed, scoped token (for example, PASETO v4.public) that encodes allowed actions and lifetime.&lt;/li&gt;
&lt;li&gt;Stateless verification: services keep the signer’s public key and a small jti replay cache instead of a user database.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How do capability tokens replace user accounts?
&lt;/h2&gt;

&lt;p&gt;Put permissions, resource scopes, rate or usage limits, and expiration into a signed token. Bind it to the device key with a confirmation (cnf) claim. Services only need the issuer’s public key to verify and authorize a request. No per-user record, session, or cookie jar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Token shape (recommended)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Format: PASETO v4.public (Ed25519) or Macaroons for attenuation/delegation chains.&lt;/li&gt;
&lt;li&gt;Claims:

&lt;ul&gt;
&lt;li&gt;sub: device identifier (e.g., JWK thumbprint)&lt;/li&gt;
&lt;li&gt;cnf: { jkt: base64url(thumbprint) } to bind token to device key&lt;/li&gt;
&lt;li&gt;cap: list of capabilities [{ act, res, limit?, nbf?, exp? }]&lt;/li&gt;
&lt;li&gt;iat/exp/jti: temporal and anti-replay metadata&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example capability list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;act=read, res=/notes/*&lt;/li&gt;
&lt;li&gt;act=write, res=/notes/{noteId}&lt;/li&gt;
&lt;li&gt;limit=100 writes/24h (enforced server-side by metering on jti or sub)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bootstrapping without accounts
&lt;/h2&gt;

&lt;p&gt;You still need an initial trust handshake, but not a login.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local-first: app generates a key pair on first run and displays a pairing code. A one-shot provisioning endpoint mints the first capability (for example, from a purchase receipt, license, or admin console) scoped to the device’s jkt.&lt;/li&gt;
&lt;li&gt;Recurring: the device refreshes capabilities using a narrow “refresh” capability, still bound to the device. No username or password, no session cookies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Client: device key generation and DPoP
&lt;/h2&gt;

&lt;p&gt;Generate a non-extractable key and make DPoP-bound API calls. In a browser, use WebCrypto. In native apps, use Keychain/Keystore.&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;// browser-client.ts&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;exportJWK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;calculateJwkThumbprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SignJWT&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;jose&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 1) Generate a non-extractable ES256 keypair and persist via IndexedDB (omitted for brevity)&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;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;privateKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateKey&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;ECDSA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;namedCurve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P-256&lt;/span&gt;&lt;span class="dl"&gt;'&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="c1"&gt;// non-extractable private key&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sign&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;verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pubJwk&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;exportJWK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicKey&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;CryptoKey&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;jkt&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;calculateJwkThumbprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pubJwk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 2) DPoP signer&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;dpopProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&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;url&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;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SignJWT&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;htm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;htu&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="na"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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;setProtectedHeader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;typ&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dpop+jwt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;alg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ES256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;jwk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pubJwk&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setIssuedAt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;privateKey&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;CryptoKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 3) Call API with capability token bound to this key&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;callApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;capToken&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;method&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;url&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;body&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;any&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;proof&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;dpopProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&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="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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;capToken&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;DPoP&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;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;body&lt;/span&gt;&lt;span class="p"&gt;)&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="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;`HTTP &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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The privateKey is non-extractable, so page malware can’t lift it.&lt;/li&gt;
&lt;li&gt;DPoP carries htm/htu/jti/iat; a replay to a different URL or method fails.&lt;/li&gt;
&lt;li&gt;capToken is issued out of band (provisioning or refresh) and must include cnf.jkt = jkt.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Server: verifying DPoP and PASETO capabilities
&lt;/h2&gt;

&lt;p&gt;The resource server runs two separate checks: DPoP validity with replay protection, then capability verification and scope enforcement.&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;// server-auth.ts&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;decodeProtectedHeader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;importJWK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jwtVerify&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;calculateJwkThumbprint&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;jose&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;V4&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;paseto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&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;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Redis&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;ioredis&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&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="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&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="c1"&gt;// PASETO issuer keys (provisioned at deploy time)&lt;/span&gt;
&lt;span class="c1"&gt;// In production, load from HSM or env and rotate.&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;secretKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;V4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&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;redis&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;Redis&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;REDIS_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Authorization middleware&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&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;req&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;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dpop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DPoP&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;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&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;dpop&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;auth&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="s1"&gt;Bearer &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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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;capToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;auth&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 1) Verify DPoP proof&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hdr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decodeProtectedHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dpop&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;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;typ&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dpop+jwt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jwk&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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;dpopKey&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;importJWK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jwk&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alg&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&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;payload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;jwtVerify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dpop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dpopKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;typ&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dpop+jwt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;clockTolerance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;5s&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;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;htm&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&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;toUpperCase&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="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;htu&lt;/span&gt; &lt;span class="k"&gt;as&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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;expectedUrl&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&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="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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalUrl&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;expectedUrl&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="nx"&gt;url&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Anti-replay: store jti with short TTL&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jti&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jti&lt;/span&gt; &lt;span class="k"&gt;as&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;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;jti&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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;replay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setnx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`dpop:jti:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jti&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;1&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;replay&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;replay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`dpop:jti:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jti&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="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// seconds window&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jkt&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;calculateJwkThumbprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jwk&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2) Verify capability token (PASETO v4.public)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;V4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;capToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// cnf binding check&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;cap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cnf&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cnf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jkt&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;jkt&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;key mismatch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Temporal checks (paseto verifies exp/nbf if present, still validate policy)&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;cap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exp&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Scope enforcement&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="nf"&gt;allows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forbidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Optional: per-capability metering (rate/limit)&lt;/span&gt;
    &lt;span class="c1"&gt;// await enforceLimits(cap.sub, cap.jti, cap.cap)&lt;/span&gt;

    &lt;span class="c1"&gt;// Attach identity to request&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&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;cap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jkt&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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;function&lt;/span&gt; &lt;span class="nf"&gt;allows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;method&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;path&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="c1"&gt;// Minimal example: method+path prefix match&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caps&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;act&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;act&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&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="nf"&gt;pathMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&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;path&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;function&lt;/span&gt; &lt;span class="nf"&gt;pathMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="c1"&gt;// naive: '/notes/*' =&amp;gt; prefix match&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;pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/*&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="nx"&gt;path&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="nx"&gt;pattern&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="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/notes/*&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&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;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt; &lt;span class="p"&gt;});&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="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The DPoP header carries the public JWK; the server verifies it and computes the thumbprint.&lt;/li&gt;
&lt;li&gt;Capability verification is stateless, only the publicKey is required.&lt;/li&gt;
&lt;li&gt;cnf.jkt binds the token to the device, so a stolen token is useless without the private key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Issuing capability tokens (no auth server required)
&lt;/h2&gt;

&lt;p&gt;Any internal service holding the signing key can mint capabilities. Do it during provisioning, purchase validation, or admin workflows.&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;// issuer.ts&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;V4&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;paseto&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;randomUUID&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;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;issueCapability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deviceJkt&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;caps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;ttlSec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&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="mi"&gt;1000&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&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="s2"&gt;`device:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;deviceJkt&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="na"&gt;cnf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;jkt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;deviceJkt&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;iat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ttlSec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;iss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cap-issuer:v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;aud&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;V4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Example cap set: read-only notes&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;issueCapability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kQ2…thumbprint…&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;act&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/notes/*&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="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How do you revoke a leaked capability token without a DB?
&lt;/h2&gt;

&lt;p&gt;Favor lifecycle controls over stateful revocation. Issue short-lived tokens, bind them to device keys with cnf.jkt plus DPoP, rotate signing keys on a schedule, and keep a small jti denylist only for emergencies. For long-lived tokens, include a refresh capability you can cut off by rotating a signer or revoking a cohort.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practical revocation strategies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Short TTLs everywhere (5–15 minutes typical; seconds for high-value actions).&lt;/li&gt;
&lt;li&gt;Rotate issuer keys regularly; invalidate cohorts by key ID (kid) rollover.&lt;/li&gt;
&lt;li&gt;Maintain a Redis-backed jti denylist only for incident response; TTL entries to auto-expire.&lt;/li&gt;
&lt;li&gt;Attenuate via nested capabilities or macaroons for delegations; revoke by refusing to mint downstream.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Macaroons vs PASETO
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;PASETO v4.public&lt;/th&gt;
&lt;th&gt;Macaroons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simplicity&lt;/td&gt;
&lt;td&gt;Single signature, flat claims&lt;/td&gt;
&lt;td&gt;Attenuation via chained caveats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delegation&lt;/td&gt;
&lt;td&gt;Explicit via minting new token&lt;/td&gt;
&lt;td&gt;Natural via third-party caveats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification&lt;/td&gt;
&lt;td&gt;Public key verify&lt;/td&gt;
&lt;td&gt;Shared root key + caveat verify&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fit here&lt;/td&gt;
&lt;td&gt;Great for stateless caps&lt;/td&gt;
&lt;td&gt;Great for delegation-heavy flows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most zero-account APIs, PASETO v4.public with cnf binding and DPoP keeps things simple and robust. Use macaroons when you need on-chain attenuation and delegation proofs across services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threat model and mitigations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Token theft: mitigated by cnf.jkt plus DPoP; proofs are replay-protected by a jti cache and htm/htu binding.&lt;/li&gt;
&lt;li&gt;Device compromise: out of scope; reduce blast radius with least-privilege capabilities and short TTLs.&lt;/li&gt;
&lt;li&gt;Phishing: no passwords or sessions, which sharply reduces impact.&lt;/li&gt;
&lt;li&gt;MITM: require HTTPS and HSTS; DPoP helps spot target URL tampering.&lt;/li&gt;
&lt;li&gt;Key exfiltration (browser): make the private key non-extractable; prefer platform keystores in native apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Operational checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Rotate issuer keys and distribute their public half to all services.&lt;/li&gt;
&lt;li&gt;Enforce DPoP on every protected endpoint and run a small jti replay cache.&lt;/li&gt;
&lt;li&gt;Keep capability TTLs short; implement a refresh flow with a narrow-scoped refresh capability.&lt;/li&gt;
&lt;li&gt;Log cap.jti, sub, and authorization decisions for audits.&lt;/li&gt;
&lt;li&gt;Use deterministic resource matching and HTTP method checks in capability evaluation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What about multi-device and recovery?
&lt;/h2&gt;

&lt;p&gt;Use delegation. A primary device issues a short-lived delegation capability to the new device’s jkt via QR or pairing code. The server then grants that device first-party capabilities. For recovery, rely on out-of-band ownership proofs—such as a purchase receipt or an emailed link that mints a new device capability—instead of a persistent account.&lt;/p&gt;

&lt;h2&gt;
  
  
  MERN integration notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;React: hydrate app state from capabilities, no cookies. Use SWR/React Query and attach DPoP to each request.&lt;/li&gt;
&lt;li&gt;Node/Express: middleware as shown; add route-level capability specs so they can be checked statically.&lt;/li&gt;
&lt;li&gt;MongoDB: index audit logs by cap.jti and sub for incident triage. Avoid PII storage.&lt;/li&gt;
&lt;li&gt;Next.js/SSR: don’t embed private keys; sign on the client or via server actions tied to device-bound refresh flows.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Zero-account means the device carries its identity and its rights. Pair capability tokens with DPoP, keep TTLs short, and bind everything to the device key. You get stateless, least-privilege authorization with less operational load than a traditional auth server, and a clear path to delegation and rotation as you grow.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>security</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Serialization Adapters: How I Cut Payloads by 94%</title>
      <dc:creator>Yaseen Khatib</dc:creator>
      <pubDate>Tue, 22 Sep 2026 09:27:24 +0000</pubDate>
      <link>https://dev.to/yaseenyk04/serialization-adapters-how-i-cut-payloads-by-94-3o1j</link>
      <guid>https://dev.to/yaseenyk04/serialization-adapters-how-i-cut-payloads-by-94-3o1j</guid>
      <description>&lt;p&gt;[ EXECUTIVE TEARDOWN // TL;DR ]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Separate the render model (UI) from the transport model (DB) with an adapter that owns the only conversion.&lt;/li&gt;
&lt;li&gt;  Anything derivable — dimensions, selection, handle geometry — is recomputed on load, never stored.&lt;/li&gt;
&lt;li&gt;  The 94% reduction is the sum of removing framework metadata and rounding coordinates, not a codec trick.&lt;/li&gt;
&lt;li&gt;  Most payload-size problems are boundary problems: you're shipping the cache because no line was drawn.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rich UI objects are great for rendering and lousy as records. In IntegrateX — a node-graph automation tool — the node you drag around the canvas is packed with measured geometry, hover/selection flags, handle maps, and framework internals. Persist that verbatim and every save ships kilobytes of cached view state, sync gets chatty, and real-time edits run into backpressure. I put a Serialization Adapter at the boundary, kept only the domain fields, and dropped the UI scaffolding. Net: 94% smaller payloads and predictable sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two models, one boundary
&lt;/h2&gt;

&lt;p&gt;The naive approach fuses two things that must stay separate: the &lt;strong&gt;render model&lt;/strong&gt; (what the UI needs to draw) and the &lt;strong&gt;transport model&lt;/strong&gt; (what the wire and database require). The adapter translates both ways, and nothing crosses without it. This is the Data / Serialization Adapter in the pattern I call &lt;em&gt;Trinity Architecture&lt;/em&gt;: Presentation renders and dispatches only, a Reactive State / Orchestration layer owns the runtime truth and optimistic updates, and the Adapter shapes lean records for the wire. No layer talks past its neighbor — the UI never formats DB schemas, and the adapter never pokes UI state directly.&lt;/p&gt;

&lt;p&gt;adapter.ts&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;// render model → lean transport record (and back)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FlowNode&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;NodeRecord&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&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="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// the only domain data that matters&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;const&lt;/span&gt; &lt;span class="nx"&gt;fromRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NodeRecord&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;FlowNode&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;hydrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;// recompute view state, don't store it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Derived, never stored
&lt;/h2&gt;

&lt;p&gt;The lever behind the compression is simple: anything that can be &lt;em&gt;derived&lt;/em&gt; must not be &lt;em&gt;stored&lt;/em&gt;. Dimensions come from layout. Selection is ephemeral. Handle geometry is a function of node type. Persisting them is persisting a cache. In IntegrateX, the orchestrator (Zustand in the middle of my Trinity split) recomputes those bits on load and on change, so the transport stays lean while the UI stays reactive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 94%, decomposed
&lt;/h2&gt;

&lt;p&gt;This wasn't a clever codec; it was subtraction. Drop measured geometry, per-node flags, duplicated handles, and framework metadata. Round coordinates to integers. A node that used to serialize to roughly a kilobyte collapses to a few dozen bytes. Multiply by a hundred-node graph, and you reclaim the budget for save, load, and real-time sync — fewer bytes on the wire, fewer renders, less state-synchronization lag.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Most payload-size problems aren't compression problems — they're boundary problems. You're shipping the cache because nobody enforced the line between the view and the record.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For the state-management side of the same project — optimistic updates, sync ordering, and throttled broadcasts — see &lt;a href="https://yaseenkhatib.streamerosai.com/blog/94-percent-payload-reduction-react-flow/" rel="noopener noreferrer"&gt;Compressing the Wire&lt;/a&gt;; the system is &lt;a href="https://yaseenkhatib.streamerosai.com/#projects" rel="noopener noreferrer"&gt;IntegrateX&lt;/a&gt;. I reach for this boundary discipline by default now — the engineer who draws the line between view and record early is the one whose sync layer still behaves at a hundred nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  ~/keep-reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/94-percent-payload-reduction-react-flow/" rel="noopener noreferrer"&gt;8 min readCompressing the Wire: A 94% Payload Reduction in React FlowNode-graph editors serialize enormous JSON. A custom Serialization Adapter pattern that separates the React Flow render model from the transport record cut IntegrateX payloads by 94%.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/payload-compression-serialization-patterns/" rel="noopener noreferrer"&gt;6 min readCutting a Payload 94% With Custom Serialization PatternsI cut a React Flow agent-graph payload 94% without losing a node — not with gzip, but by shaping a custom serialization format around the data: schema, not prose.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/optimizing-mongodb-aggregation/" rel="noopener noreferrer"&gt;7 min readMongoDB Aggregation Pipelines: Stage Order Is the WinProfiling a clinical API taught me MongoDB aggregation pipeline optimization lives or dies on stage order: $match first on indexes, $lookup join performance, then explain.&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;YK&lt;/p&gt;

&lt;p&gt;Yaseen Khatib · MERN + AI Architect&lt;/p&gt;

&lt;p&gt;Ships autonomous AI products solo — five in the last twelve months. &lt;a href="https://yaseenkhatib.streamerosai.com/about/" rel="noopener noreferrer"&gt;More about Yaseen →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Need an engineer who can build this?
&lt;/h2&gt;

&lt;p&gt;I'm Yaseen Khatib — a Senior Full-Stack AI Engineer (MERN + TypeScript) who ships production AI systems solo. Open to senior and lead roles, remote or on-site.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/#contact" rel="noopener noreferrer"&gt;Get in touch →&lt;/a&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/products/" rel="noopener noreferrer"&gt;See what I've shipped&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://yaseenkhatib.streamerosai.com/blog/custom-serialization-adapters/" rel="noopener noreferrer"&gt;yaseenkhatib.streamerosai.com/blog/custom-serialization-adapters/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>serialization</category>
      <category>adapter</category>
    </item>
    <item>
      <title>The 10x Reality: How AI Replaced the Traditional MERN Squad</title>
      <dc:creator>Yaseen Khatib</dc:creator>
      <pubDate>Tue, 22 Sep 2026 09:27:22 +0000</pubDate>
      <link>https://dev.to/yaseenyk04/the-10x-reality-how-ai-replaced-the-traditional-mern-squad-424n</link>
      <guid>https://dev.to/yaseenyk04/the-10x-reality-how-ai-replaced-the-traditional-mern-squad-424n</guid>
      <description>&lt;p&gt;[ EXECUTIVE TEARDOWN // TL;DR ]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  AI has moved the developer's role from writing syntax to directing architectural flow.&lt;/li&gt;
&lt;li&gt;  A single architect with Claude can design, build, and ship a full Database → Backend → Frontend pipeline in the time a 10-person team once needed.&lt;/li&gt;
&lt;li&gt;  The only remaining bottleneck is deep comprehension of the system's data flow.&lt;/li&gt;
&lt;li&gt;  Velocity now scales with architectural clarity, not headcount.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most of my career, throughput was something you bought with headcount. More endpoints, add backend engineers; more screens, add frontend engineers. That model just snapped. The limiter was never hands on keyboards — it was the latency between clear architectural intent and working code spread across three layers. AI has crushed that translation latency to near zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottleneck was never typing
&lt;/h2&gt;

&lt;p&gt;Walk the timeline of a standard MERN feature. A lead sketches the data model. Backend builds the Mongoose schema, controller, validation, routes. Another engineer wires the service layer. Frontend ships the form, the fetch hook, the optimistic update, the error states. Someone else writes tests. Strip the meetings and merge conflicts and you see the uncomfortable truth: roughly 80% of that work was &lt;strong&gt;mechanical translation&lt;/strong&gt;, not decisions. We already knew what to build; we were slowly re-encoding a known architecture into code.&lt;/p&gt;

&lt;p&gt;That 80% is what AI now absorbs. Not the thinking — the scaffolding, wiring, and the thousand tiny consistency choices that quietly turn a week into a sprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  From writing code to directing flow
&lt;/h2&gt;

&lt;p&gt;The discipline that replaces manual coding is &lt;strong&gt;AI Orchestration&lt;/strong&gt;: declare a single contract — the canonical data shape and its boundaries — and direct an agent to materialize it through every layer. Stop writing the database record, the endpoint, and the React hook as three separate artifacts. Declare the contract; generate the layers to satisfy it. I keep this inside the pattern I call Trinity Architecture: Presentation renders and dispatches only; Reactive State / Orchestration holds the runtime truth and coordinates events; the Data / Serialization Adapter shapes rich in-memory state into lean, durable payloads. No layer talks past its neighbor.&lt;/p&gt;

&lt;p&gt;orchestrate.ts&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;// the architect declares intent; the orchestrator fills the layers&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;system&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;orchestrate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;caseRecordSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// single source of truth (zod)&lt;/span&gt;
  &lt;span class="na"&gt;layers&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;db&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;api&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;ui&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;guardrails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;strict&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;review&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;human&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;This is not autocomplete. Autocomplete predicts the next token in a file you're already writing. Orchestration operates a layer up: the contract is the spec and the codebase is the build target. Your leverage stops being WPM and starts being how precisely you specify.&lt;/p&gt;

&lt;h3&gt;
  
  
  The orchestration loop
&lt;/h3&gt;

&lt;p&gt;Every productive AI build runs the same loop: declare the contract, generate a layer, verify it against the contract, integrate, repeat. The engineer's attention sits entirely on the seams — where one layer hands data to the next. Get the seams right and the interiors are commodity. Miss a seam and you get schema drift, broken optimistic updates, cache churn, and render thrash no amount of generated code can hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The only bottleneck left: data-flow comprehension
&lt;/h2&gt;

&lt;p&gt;There is exactly one thing AI cannot do for you, and it has always mattered most: decide how data should move through your system. The shape of the records, the boundaries between services, the contracts that cross them, the indexes that make the queries cheap — these are architectural decisions, and they remain stubbornly, permanently human. Whoever holds the clearest mental model of the data's journey from Database → Backend → Frontend is now the entire critical path. On IntegrateX — a React Flow workflow canvas with real-time node execution and Zustand state — the hard problems were all flow and serialization. A Serialization Adapter stripped canvas-only UI metadata before persistence and cut payload sizes 94%, which in turn fixed state-sync lag during bursts. The adapter never touched UI state; it only spoke through the orchestrator. That boundary discipline is what kept real-time stable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The era of bloated engineering teams is over. An AI-specialized architect can execute the output of a 10-person squad because the work that remains is comprehension, and comprehension does not parallelize without heavy coordination cost.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What the 10x architect actually does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Owns the data contract&lt;/strong&gt; — one authoritative schema that every layer is generated to satisfy, eliminating drift between DB, API, and UI, and enforcing my Trinity split so the UI never formats DB shapes and the adapter never reaches into state.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Designs the boundaries&lt;/strong&gt; — service and module cuts, where validation lives, which seams are stable versus allowed to change, and how events move across them.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Reviews AI output adversarially&lt;/strong&gt; — not for style, but for the failure modes models won't surface: auth and tenancy, idempotency, pagination limits, streaming and backpressure, edge-case correctness and silent data loss.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Treats prompts as architecture specs&lt;/strong&gt; — versioned, precise, and scoped; an ambiguous prompt is just an ambiguous requirement with faster failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The economic reality
&lt;/h2&gt;

&lt;p&gt;A ten-person squad does not deliver ten units of work. It delivers maybe five after the tax: standups, handoffs, context rebuilds, merge conflicts, and the mismatch between the schema one person imagined and the one another implemented. A single architect directing AI pays none of that tax. There are no handoffs because there is one context. There is no schema drift because there is one contract. The output isn't incrementally faster — it's structurally different.&lt;/p&gt;

&lt;p&gt;This is why the staffing question for greenfield product work is changing from &lt;em&gt;how many engineers&lt;/em&gt; to &lt;em&gt;how clear is the architecture&lt;/em&gt;. Velocity now scales with architectural clarity, not headcount — and that is far cheaper to buy.&lt;/p&gt;

&lt;p&gt;If you want to see what this looks like shipped, the &lt;a href="https://yaseenkhatib.streamerosai.com/#projects" rel="noopener noreferrer"&gt;project breakdowns&lt;/a&gt; walk through production systems — agentic RAG pipelines, real-time admin portals, a real-time telemetry cockpit — each built and taken to production this way.&lt;/p&gt;

&lt;h2&gt;
  
  
  ~/keep-reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/chatgpt-changed-full-stack-engineering/" rel="noopener noreferrer"&gt;6 min readThe Day ChatGPT Changed Full-Stack EngineeringLate 2022 dropped the cost of writing software overnight. Why the bottleneck moved from syntax to judgment — and why MERN engineers were positioned to win.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/shipped-5-products-solo-12-months/" rel="noopener noreferrer"&gt;6 min readI Shipped 5 Products in 12 Months — Solo, Unfunded, and Faster Than Most Teams Ship OneFive production systems — a realtime telemetry cockpit, a workflow engine, a local-first AI finance agent, and two autonomous pipelines — designed, built, and shipped by one engineer. Here's the operating model.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/vision-over-syntax-architecture-first/" rel="noopener noreferrer"&gt;6 min readVision Over Syntax: I Design the Entire Product in My Head Before the First CommitSyntax is solved; vision is not. How architecture-first thinking — data contracts, trust boundaries, and deliberate refusals — is the real engineering skill in the AI era, shown through shipped products.&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;YK&lt;/p&gt;

&lt;p&gt;Yaseen Khatib · MERN + AI Architect&lt;/p&gt;

&lt;p&gt;Ships autonomous AI products solo — five in the last twelve months. &lt;a href="https://yaseenkhatib.streamerosai.com/about/" rel="noopener noreferrer"&gt;More about Yaseen →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Need an engineer who can build this?
&lt;/h2&gt;

&lt;p&gt;I'm Yaseen Khatib — a Senior Full-Stack AI Engineer (MERN + TypeScript) who ships production AI systems solo. Open to senior and lead roles, remote or on-site.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/#contact" rel="noopener noreferrer"&gt;Get in touch →&lt;/a&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/products/" rel="noopener noreferrer"&gt;See what I've shipped&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://yaseenkhatib.streamerosai.com/blog/the-10x-reality-ai-replaced-the-mern-squad/" rel="noopener noreferrer"&gt;yaseenkhatib.streamerosai.com/blog/the-10x-reality-ai-replaced-the-mern-squad/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>mern</category>
    </item>
    <item>
      <title>The Day ChatGPT Changed Full-Stack Engineering</title>
      <dc:creator>Yaseen Khatib</dc:creator>
      <pubDate>Mon, 21 Sep 2026 10:09:39 +0000</pubDate>
      <link>https://dev.to/yaseenyk04/the-day-chatgpt-changed-full-stack-engineering-4mg2</link>
      <guid>https://dev.to/yaseenyk04/the-day-chatgpt-changed-full-stack-engineering-4mg2</guid>
      <description>&lt;p&gt;[ EXECUTIVE TEARDOWN // TL;DR ]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Late 2022 made writing code cheap for everyone at once, invalidating every estimate of how long features take.&lt;/li&gt;
&lt;li&gt;  The bottleneck moved from typing to judgment — the model amplifies architectural clarity and confusion alike.&lt;/li&gt;
&lt;li&gt;  MERN's typed, structured surface is exactly what models navigate well, positioning full-stack engineers to win.&lt;/li&gt;
&lt;li&gt;  Treat AI as a multiplier on sound architecture, not a substitute for understanding the system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Late 2022 flipped the table: code went from scarce to cheap almost overnight. Not gradual, not gated by early adopters — everyone got the same accelerant at once. A general-purpose model could read intent and emit working code, turning the slowest part of the job — translating a design into syntax — into a conversation. Timelines based on keystrokes and boilerplate instantly stopped mapping to reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Stack Overflow to a collaborator
&lt;/h2&gt;

&lt;p&gt;The old loop: hit a snag, trawl threads, stitch together an answer, adapt, repeat. The new loop: describe the target — a React prop contract, a clean RTK Query slice, a Mongoose schema with indexes — and get a runnable draft that already speaks the local idioms. The grind that used to tax most days — glue code, adapter shims, the same five edge cases — mostly evaporated. What was left was the actual work: shaping behavior, owning the seams, and deciding where state lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottleneck moved to judgment
&lt;/h2&gt;

&lt;p&gt;When typing is cheap, correctness of intent is the constraint. The people who got faster weren't “better prompters”; they carried a crisp architecture and steered the model to it. The pattern I call Trinity Architecture did the heavy lifting for me: a strict split between Presentation (React components that only render and dispatch), Reactive State / Orchestration (Zustand, RTK Query, or an event bus as the runtime source of truth with optimistic updates), and a Data / Serialization Adapter (the bridge that converts rich in-memory state to lean wire payloads). Keep those boundaries and the model locks into the grain; blur them and it produces fast spaghetti — state-synchronization lag, render thrash, and vague code that compiles but won't survive real-time pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it meant for MERN engineers
&lt;/h2&gt;

&lt;p&gt;Full‑stack folks were sitting on the right surface area. MERN with TypeScript — contracts from Mongo through Express to React — is exactly the structured domain models navigate well. On IntegrateX, a React Flow workflow canvas with real-time node execution on a Zustand store, the model could draft nodes, hooks, and endpoints quickly. But the leap came from honoring the Trinity split and inserting a Serialization Adapter at the data boundary: it stripped non-essential React Flow UI metadata before persistence and cut payloads 94%. The model wrote the first pass; judgment decided what was wire-worthy, what stayed in orchestrated client state, and how to keep optimistic updates from outrunning the backend.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The day code became cheap to write was the day architecture became the job. Everything else is an implementation detail moving at 10x speed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The next arc — RAG, orchestration, type‑safe outputs — sits downstream of that shift. It's why the &lt;a href="https://yaseenkhatib.streamerosai.com/#projects" rel="noopener noreferrer"&gt;work&lt;/a&gt; on IntegrateX, streamerOS, Path Saathi, and SANKALP reads like systems design under new physics: same constraints, but now the time goes to boundaries, contracts, and flow control instead of scaffolding.&lt;/p&gt;

&lt;h2&gt;
  
  
  ~/keep-reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/the-10x-reality-ai-replaced-the-mern-squad/" rel="noopener noreferrer"&gt;8 min readThe 10x Reality: How AI Replaced the Traditional MERN SquadAI shifted the engineer's job from writing syntax to directing architectural flow. Why one architect with Claude now outships a traditional 10-person MERN squad.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/mastering-claude-architecture-context-windows-output-limits/" rel="noopener noreferrer"&gt;9 min readBudgeting the Claude Context Window Before It Truncates YouThe Claude context window is one token budget shared by prompt, tools, thinking, and reply. I count tokens and guard it in code so a reply never truncates.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/ai-native-portfolio-landing-lead-roles/" rel="noopener noreferrer"&gt;6 min readThe AI-Native Portfolio: Landing Lead Roles by Shipping the SystemA portfolio that lists 'used ChatGPT' reads junior. One that demonstrates systems architecture — RAG, agents, guardrails, latency budgets — reads like the lead they're trying to hire. Here's how to build the second one.&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;YK&lt;/p&gt;

&lt;p&gt;Yaseen Khatib · MERN + AI Architect&lt;/p&gt;

&lt;p&gt;Ships autonomous AI products solo — five in the last twelve months. &lt;a href="https://yaseenkhatib.streamerosai.com/about/" rel="noopener noreferrer"&gt;More about Yaseen →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Need an engineer who can build this?
&lt;/h2&gt;

&lt;p&gt;I'm Yaseen Khatib — a Senior Full-Stack AI Engineer (MERN + TypeScript) who ships production AI systems solo. Open to senior and lead roles, remote or on-site.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/#contact" rel="noopener noreferrer"&gt;Get in touch →&lt;/a&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/products/" rel="noopener noreferrer"&gt;See what I've shipped&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://yaseenkhatib.streamerosai.com/blog/chatgpt-changed-full-stack-engineering/" rel="noopener noreferrer"&gt;yaseenkhatib.streamerosai.com/blog/chatgpt-changed-full-stack-engineering/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Shipped 5 Products in 12 Months — Solo, Unfunded, and Faster Than Most Teams Ship One</title>
      <dc:creator>Yaseen Khatib</dc:creator>
      <pubDate>Mon, 21 Sep 2026 10:09:38 +0000</pubDate>
      <link>https://dev.to/yaseenyk04/i-shipped-5-products-in-12-months-solo-unfunded-and-faster-than-most-teams-ship-one-9a8</link>
      <guid>https://dev.to/yaseenyk04/i-shipped-5-products-in-12-months-solo-unfunded-and-faster-than-most-teams-ship-one-9a8</guid>
      <description>&lt;p&gt;[ EXECUTIVE TEARDOWN // TL;DR ]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Five production products shipped in twelve months by one engineer: streamerOS, IntegrateX, Sable, a zero-cost content engine, and a LinkedIn automation pipeline.&lt;/li&gt;
&lt;li&gt;  Solo means every architecture, trust-boundary, and economics decision was made by one head — AI supplied throughput, not vision.&lt;/li&gt;
&lt;li&gt;  Each product was built to remove a recurring job from my life; that automation mindset transfers directly to company work.&lt;/li&gt;
&lt;li&gt;  One engineer who has shipped alone has verifiably done the work of PM, backend, frontend, DevOps, and QA.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the last twelve months I designed, built, and shipped five production products — a realtime telemetry cockpit, a node-based workflow engine, a local-first AI finance agent, an autonomous content pipeline, and a LinkedIn outreach machine. No team. No funding. No standups. Every architecture decision, every line of infrastructure, every product call came from one place: my own head.&lt;/p&gt;

&lt;p&gt;This post is not a humble-brag. It is a thesis about how software gets built now — and what a company actually buys when it hires an engineer who has done this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The portfolio, in one table
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;streamerOS&lt;/strong&gt; — a realtime telemetry cockpit for streamers: live system telemetry, multi-platform chat velocity, and automated OBS scene control, rendering at 60fps on a fraction of Electron's memory.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;IntegrateX&lt;/strong&gt; — a React Flow workflow-automation canvas whose custom serialization adapter cut graph payloads by 94%, making save/load feel instant.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Sable&lt;/strong&gt; — a local-first AI financial agent. All data lives on-device in SQLite; the model proposes, a human confirms, and nothing financial ever leaves the phone.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The Zero-Cost AI Blog Writer&lt;/strong&gt; — a pipeline that drafts, commits, and deploys technical articles on a cron schedule for exactly $0 of infrastructure.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The LinkedIn Pipeline&lt;/strong&gt; — an autonomous system that turns my shipped work into scheduled LinkedIn content and grew my professional network while I slept.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What does "solo" mean when AI writes the code?
&lt;/h2&gt;

&lt;p&gt;Solo does not mean I typed every character by hand like it's 2015. It means every decision was mine: the data model, the trust boundaries, the failure modes, the economics. I use Claude as a workforce and MCP as the wiring between it and my systems — but AI is leverage, not vision. A model can generate a component; it cannot decide that a finance app should never let the model touch the money, or that a streaming cockpit should hold its own state in process because streamers run OBS, a game, and an encoder on the same machine. Those calls are the product. The rest is throughput.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The scarce skill in 2026 is not writing code. It is holding an entire product in your head — data flow, trust boundaries, unit economics — clearly enough that the code becomes an output, not a struggle.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The compounding trick: products that work for you
&lt;/h2&gt;

&lt;p&gt;The pattern across all five products is that each one removed a job from my life. The blog writer removed content production. The LinkedIn pipeline removed outreach. Sable removed money anxiety. streamerOS removed the four-apps-and-a-prayer streaming setup. I did not build demos — I built &lt;strong&gt;employees&lt;/strong&gt;, and they have been on shift ever since. That is the mindset I bring to a company: look at the recurring work, ask which of it deserves a human, and ship a system for the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this proves to an employer
&lt;/h2&gt;

&lt;p&gt;A candidate who has shipped five products alone has, by construction, done the job of a product manager, a backend engineer, a frontend engineer, a DevOps engineer, and a QA function — and paid the price for every mistake personally. There is no diffusion of responsibility in a team of one. When I say I understand systems end-to-end, the evidence is deployed, running, and documented across this site — every product has a full architecture breakdown in the &lt;a href="https://yaseenkhatib.streamerosai.com/products/" rel="noopener noreferrer"&gt;products section&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The next nine posts in this series each take one of these systems — or one of the operating principles behind them — and show exactly how it was built, why it was built that way, and what the same thinking would do inside your company.&lt;/p&gt;

&lt;h2&gt;
  
  
  ~/keep-reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/vision-over-syntax-architecture-first/" rel="noopener noreferrer"&gt;6 min readVision Over Syntax: I Design the Entire Product in My Head Before the First CommitSyntax is solved; vision is not. How architecture-first thinking — data contracts, trust boundaries, and deliberate refusals — is the real engineering skill in the AI era, shown through shipped products.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/linkedin-pipeline-job-search-runs-itself/" rel="noopener noreferrer"&gt;6 min readMy Networking Runs Itself: The LinkedIn Pipeline That Grew My Reach While I SleptHow I engineered consistency: an autonomous GitHub Actions pipeline that turns real work into scheduled LinkedIn posts, tracks state in Git, costs $0 to run — and what the same pattern automates inside a company.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://yaseenkhatib.streamerosai.com/blog/zero-dollar-content-engine/" rel="noopener noreferrer"&gt;6 min readThe $0 Content Engine: My Blog Writes, Commits, and Deploys Itself Twice a WeekA cron-scheduled pipeline drafts articles with Gemini, writes MDX, commits to Git, and redeploys the static site — no CMS, no server, no database, no bill. The architecture of refusing infrastructure.&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;YK&lt;/p&gt;

&lt;p&gt;Yaseen Khatib · MERN + AI Architect&lt;/p&gt;

&lt;p&gt;Ships autonomous AI products solo — five in the last twelve months. &lt;a href="https://yaseenkhatib.streamerosai.com/about/" rel="noopener noreferrer"&gt;More about Yaseen →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Need an engineer who can build this?
&lt;/h2&gt;

&lt;p&gt;I'm Yaseen Khatib — a Senior Full-Stack AI Engineer (MERN + TypeScript) who ships production AI systems solo. Open to senior and lead roles, remote or on-site.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/#contact" rel="noopener noreferrer"&gt;Get in touch →&lt;/a&gt;&lt;a href="https://yaseenkhatib.streamerosai.com/products/" rel="noopener noreferrer"&gt;See what I've shipped&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://yaseenkhatib.streamerosai.com/blog/shipped-5-products-solo-12-months/" rel="noopener noreferrer"&gt;yaseenkhatib.streamerosai.com/blog/shipped-5-products-solo-12-months/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>solo</category>
      <category>shipped</category>
    </item>
    <item>
      <title>Stop mixing render and domain state in React dashboards</title>
      <dc:creator>Yaseen Khatib</dc:creator>
      <pubDate>Mon, 21 Sep 2026 01:49:26 +0000</pubDate>
      <link>https://dev.to/yaseenyk04/stop-mixing-render-and-domain-state-in-react-dashboards-2404</link>
      <guid>https://dev.to/yaseenyk04/stop-mixing-render-and-domain-state-in-react-dashboards-2404</guid>
      <description>&lt;h2&gt;
  
  
  What’s the difference between render state and domain state?
&lt;/h2&gt;

&lt;p&gt;Render state drives pixels: selection, panel open/close, column order, scroll offsets, hover, active tab, in-flight drag positions. Domain state is the business data from the server, normalized and cached, with mutations and invalidation. When these are mixed, you get re-render storms, bloated payloads, and painful debugging.&lt;/p&gt;

&lt;p&gt;I keep them apart. Domain state lives in a server-state cache (TanStack Query v5.47 or RTK Query 2.2.4). Render state lives in a tiny client store (Zustand 4.5.x) or local component state. Components read from both through selectors. Nothing else.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do you separate render state from domain state in React?
&lt;/h2&gt;

&lt;p&gt;Keep domain data in a server cache (TanStack Query/RTK Query) keyed by query params; keep UI state (sort, filters, selection, layout) in a co-located client store (Zustand). Components subscribe to minimal slices with shallow equality. Domain and UI layers talk only via dispatched events and props, not direct imports across layers.&lt;/p&gt;

&lt;p&gt;Here’s a concrete cut on React 18.3, TypeScript 5.5.&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;// ui/store.ts&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;create&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;zustand&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;subscribeWithSelector&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;zustand/middleware&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Sort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;column&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;dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;asc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desc&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="kc"&gt;null&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TableUIState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Sort&lt;/span&gt;
  &lt;span class="nx"&gt;filters&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="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Set&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;// transient, not persisted&lt;/span&gt;
  &lt;span class="nx"&gt;columnOrder&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;columnWidths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;scrollTop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;expandedRowIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Set&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="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DashboardUIState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TableUIState&lt;/span&gt;
  &lt;span class="nx"&gt;modals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;inspectorOpen&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="c1"&gt;// actions&lt;/span&gt;
  &lt;span class="nl"&gt;setSort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Sort&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="nx"&gt;toggleSelect&lt;/span&gt;&lt;span class="p"&gt;:&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;
  &lt;span class="nx"&gt;setColumnWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;col&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;px&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;
  &lt;span class="nx"&gt;setScrollTop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&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="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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useUI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DashboardUIState&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;subscribeWithSelector&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;get&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="na"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;sort&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="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
      &lt;span class="na"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;columnOrder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="na"&gt;columnWidths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
      &lt;span class="na"&gt;scrollTop&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="na"&gt;expandedRowIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;modals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;inspectorOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;setSort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&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="na"&gt;table&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sort&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="na"&gt;toggleSelect&lt;/span&gt;&lt;span class="p"&gt;:&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;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&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;next&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;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&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="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&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="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;table&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;setColumnWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&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="na"&gt;table&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;columnWidths&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columnWidths&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;px&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="na"&gt;setScrollTop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&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="na"&gt;table&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;scrollTop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;y&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 typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// data/orders.ts (TanStack Query v5)&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;useQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;QueryClient&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="k"&gt;export&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="na"&gt;defaultOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;staleTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gcTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;refetchOnWindowFocus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;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;status&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;total&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;createdAt&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;type&lt;/span&gt; &lt;span class="nx"&gt;OrdersResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="nl"&gt;total&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrdersResponse&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;byId&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;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="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;o&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;byId&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;o&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="nx"&gt;o&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;ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&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;o&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;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;byId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&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;useOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;status&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;q&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;page&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;pageSize&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&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="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;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="nx"&gt;key&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="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;qs&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;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/orders?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;qs&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="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;`HTTP &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="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;OrdersResponse&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;keepPreviousData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/OrdersTable.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;useOrders&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;../data/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useUI&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;../ui/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useVirtualizer&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-virtual&lt;/span&gt;&lt;span class="dl"&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;OrdersTable&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;sort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUI&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&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;selection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUI&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selection&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;setScrollTop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUI&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setScrollTop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isFetching&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useOrders&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&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;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ids&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;id&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byId&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parentRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rowVirtualizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useVirtualizer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;getScrollElement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;parentRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;estimateSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;overscan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;parentRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onScroll&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&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="nf"&gt;setScrollTop&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="nx"&gt;target&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;scrollTop&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;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rowVirtualizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTotalSize&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;100%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;relative&lt;/span&gt;&lt;span class="dl"&gt;'&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;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rowVirtualizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVirtualItems&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;vi&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;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;vi&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;absolute&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;top&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="na"&gt;left&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="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;100%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`translateY(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px)`&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;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Row&lt;/span&gt; &lt;span class="na"&gt;row&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&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="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&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;isFetching&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"dim"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Loading…&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&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="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&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;Important details that keep this fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The UI store holds only render concerns. No domain objects.&lt;/li&gt;
&lt;li&gt;The server cache owns data fetching, normalization, and memoized selection.&lt;/li&gt;
&lt;li&gt;Components subscribe via small selectors (e.g., s.table.sort), not the whole store.&lt;/li&gt;
&lt;li&gt;Virtualization avoids mounting 50k DOM nodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the three-layer split I call Trinity Architecture: presentation components render from state and dispatch events; a reactive client state holds UI and orchestrates; and a data/serialization adapter bridges persistence. Layers never talk past neighbors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common symptom when you don’t split
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;setState in render or mixing stores often triggers: “Warning: Cannot update a component (X) while rendering a different component (Y).” Lift transient UI updates into the render store and update in events/effects.&lt;/li&gt;
&lt;li&gt;Changing a large Redux slice that holds both UI and data causes 100+ dependent components to rerender. Selecting fine-grained slices in a small UI store reduces this.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How do you persist large dashboard state without bloating payloads?
&lt;/h2&gt;

&lt;p&gt;Persist only stable layout and configuration, never raw data or transient UI noise. Introduce a small serialization adapter that strips ephemeral fields and converts non-serializable structures (Sets) to arrays. Store this layout JSON separately from data queries, versioned and small.&lt;/p&gt;

&lt;p&gt;Example adapter for saving a dashboard layout to the server. This cut has kept payloads under 3–8 KB for complex boards; typical “save everything” payloads on real dashboards end up 200–800 KB due to selections and widths.&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;// ui/serialize.ts&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;useUI&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;./store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PersistedLayoutV1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;column&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;dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;asc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desc&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="kc"&gt;null&lt;/span&gt;
    &lt;span class="na"&gt;filters&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="na"&gt;columnOrder&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="na"&gt;columnWidths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;// intentionally no selection, no scrollTop&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nl"&gt;modals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;inspectorOpen&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="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;buildPersistedLayout&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="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;useUI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getState&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;PersistedLayoutV1&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;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;sort&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;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;filters&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;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;columnOrder&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;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columnOrder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;columnWidths&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;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columnWidths&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;modals&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;modals&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;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;saveLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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;layout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildPersistedLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;useUI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/layout`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&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;layout&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hydrateLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PersistedLayoutV1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// cautiously merge only known keys to avoid clobbering live transient state&lt;/span&gt;
  &lt;span class="nx"&gt;useUI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&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="na"&gt;table&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;columnOrder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columnOrder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;columnWidths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columnWidths&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;modals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This “serialization adapter” is purposefully boring. It’s a whitelist. It strips non-essential UI metadata, which also future-proofs the layout format. On one production workflow tool (IntegrateX), a similar adapter cut saved graph payloads by about 94% by excluding React Flow render geometry.&lt;/p&gt;

&lt;p&gt;If you must persist heavy mappings such as selection of thousands of rows, cap it. For instance, keep at most 500 IDs, and coerce it to an array of stable strings. Anything more should be re-derived on load from a saved filter.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do you avoid re-render storms with large datasets?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Normalize server data and expose cheap selectors. - Keep UI state small and use fine-grained selectors with shallow compare. - Virtualize long lists and avoid inline object/array props. - Batch mutations and use startTransition when flipping large UI flags.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concrete knobs that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TanStack Query select normalization keeps referential stability. Derived “row” objects change only when their id actually changes.&lt;/li&gt;
&lt;li&gt;Zustand selectors: use &lt;code&gt;(s) =&amp;gt; s.table.sort&lt;/code&gt; not &lt;code&gt;(s) =&amp;gt; s.table&lt;/code&gt;. If you must select multiple keys, memoize or use &lt;code&gt;shallow&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;shallow&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;zustand/shallow&lt;/span&gt;&lt;span class="dl"&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;sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;columnOrder&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUI&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columnOrder&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;shallow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Avoid creating new objects in JSX props on hot paths. Move them out or use &lt;code&gt;useMemo&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&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="nf"&gt;buildColumns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;columnOrder&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;columnOrder&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Table&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;For bulk UI updates that trigger lots of child work, wrap in a transition.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startTransition&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useTransition&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;onToggleAll&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="nf"&gt;startTransition&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;useUI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toggleAllVisible&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Typical figures to keep in mind (illustrative only): 50–100k rows of JSON over a 4G link is 8–20 MB and costs 1.5–4 s network + 100–250 ms parse/GC on an M1. Don’t fetch that. Paginate server-side and virtualize client-side.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What’s the SSR/Next.js 14 story without hydration bugs?
&lt;/h2&gt;

&lt;p&gt;Prefetch domain data on the server and dehydrate it; create UI state only on the client after mount. Don’t embed UI state in the HTML. If you must seed it, keep it minimal and idempotent, and merge during a client effect, not during initial render.&lt;/p&gt;

&lt;p&gt;Sketch for Next.js 14.2 with TanStack Query dehydrate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/orders/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dehydrate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Hydrate&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;queryClient&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;@/data/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prefetchQuery&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&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="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;dehydrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryClient&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;Hydrate&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&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;OrdersPageClient&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;Hydrate&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/orders/OrdersPageClient.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hydrateLayout&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;@/ui/serialize&lt;/span&gt;&lt;span class="dl"&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;OrdersPageClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// fetch persisted layout and hydrate into UI store after mount&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/me/layout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hydrateLayout&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OrdersTable&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;Two rules that prevent hydration mismatches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never render from a UI store value that differs between server and client during the initial paint.&lt;/li&gt;
&lt;li&gt;If a UI default must depend on &lt;code&gt;window&lt;/code&gt; or layout, compute it in &lt;code&gt;useEffect&lt;/code&gt; and update the store there.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Real-time and AI panels: keep tokens out of domain state
&lt;/h2&gt;

&lt;p&gt;For streaming telemetry or AI summaries, treat partial tokens/lines as render state. Domain state should track the final artifact and its metadata.&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;// ai/summarize.ts - SSE streaming client&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;streamSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&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;onToken&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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;es&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;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/tasks/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/summary/stream`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&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;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="nf"&gt;onToken&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;MessageEvent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nx"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/SummaryPanel.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;useUI&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;@/ui/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SummaryPanel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;taskId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;taskId&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="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;setText&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="c1"&gt;// render state only&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;streamSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setText&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="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&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;taskId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"summary"&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;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Persist only the final text (domain) as part of task metadata through a normal mutation. Do not shove partial token streams into your global stores.&lt;/p&gt;




&lt;h2&gt;
  
  
  When not to do this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Small CRUD with &amp;lt;10 screens and &amp;lt;2k rows/page: local component state + a single query cache is fine. Splitting stores adds cognitive overhead and boilerplate.&lt;/li&gt;
&lt;li&gt;Teams without discipline on selectors and memoization can end up with two anti-patterns instead of one. Keep the rules enforced in code review.&lt;/li&gt;
&lt;li&gt;Heavy cross-slice coupling: if a mutation in domain must atomically update multiple UI concerns, put a thin orchestrator function next to the UI store; don’t call the data cache directly from view code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Failure modes to watch
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Over-persisting UI: saving selection sets, hover rows, scroll positions balloons payloads and creates janky restores.&lt;/li&gt;
&lt;li&gt;Non-serializable UI state in devtools: Sets/Maps are fine in-memory but convert at persistence boundaries.&lt;/li&gt;
&lt;li&gt;Thundering updates: writing to the UI store from tight mousemove handlers without &lt;code&gt;requestAnimationFrame&lt;/code&gt; batching will stutter.&lt;/li&gt;
&lt;li&gt;WebSocket or RTU messages that include render-only fields will hurt mobile users; keep wire payloads to business data.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Tooling and versions that work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;React 18.3, Next.js 14.2&lt;/li&gt;
&lt;li&gt;Zustand 4.5.x with &lt;code&gt;subscribeWithSelector&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;TanStack Query 5.47 (or RTK Query 2.2.4 if you’re already on Redux Toolkit 2.2.x)&lt;/li&gt;
&lt;li&gt;@tanstack/react-virtual 3.x or AG Grid 31.x for big tables&lt;/li&gt;
&lt;li&gt;TypeScript 5.5, Node 20.16 LTS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A quick install baseline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add zustand @tanstack/react-query @tanstack/react-virtual
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  A minimal checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Domain state: queries/mutations, normalized, cached, invalidated by keys.&lt;/li&gt;
&lt;li&gt;Render state: sort, filters, column order, selection, scroll, modals.&lt;/li&gt;
&lt;li&gt;Serialization adapter: whitelist-only persisted layout, versioned.&lt;/li&gt;
&lt;li&gt;Components: subscribe to minimal slices; virtualize long lists; memoize heavy props.&lt;/li&gt;
&lt;li&gt;SSR: dehydrate domain data; hydrate UI after mount.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One blunt question to keep taped to the monitor: will this value cross a process boundary (persist, sync, or query)? If yes, it belongs to domain state; if not, keep it in render state. Keeping that line bright is most of the battle.&lt;/p&gt;

</description>
      <category>react</category>
      <category>frontend</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
