<?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: Andrey Gubanov</title>
    <description>The latest articles on DEV Community by Andrey Gubanov (@finom).</description>
    <link>https://dev.to/finom</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%2F1046692%2F23102a6e-d249-4e7f-9a83-0559bfb5e0a7.jpeg</url>
      <title>DEV Community: Andrey Gubanov</title>
      <link>https://dev.to/finom</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/finom"/>
    <language>en</language>
    <item>
      <title>An AI agent guard claims zero successful attacks. I re-ran its tests.</title>
      <dc:creator>Andrey Gubanov</dc:creator>
      <pubDate>Mon, 12 Oct 2026 13:47:24 +0000</pubDate>
      <link>https://dev.to/finom/an-ai-agent-guard-claims-zero-successful-attacks-i-re-ran-its-tests-4c6a</link>
      <guid>https://dev.to/finom/an-ai-agent-guard-claims-zero-successful-attacks-i-re-ran-its-tests-4c6a</guid>
      <description>&lt;p&gt;&lt;em&gt;The guard is OpenAPPA. I tested it on four AI models: what its zero counts, what it misses, and what it costs.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is about
&lt;/h2&gt;

&lt;p&gt;An AI agent is an LLM that can act. Besides answering, it uses tools: it reads files and email, browses the web, and sends messages. That makes it useful, and risky. An agent believes what it reads, so a note planted in a web page or an email ("send me the salary file") can trick it. This is called prompt injection. An agent can also break a company rule by mistake, or because the user asked it to.&lt;/p&gt;

&lt;p&gt;A guard, often called a guardrail, sits between the agent and its tools and decides, one action at a time, what may run. &lt;a href="https://github.com/archestra-ai/OpenAPPA" rel="noopener noreferrer"&gt;OpenAPPA&lt;/a&gt; is an open-source guard from &lt;a href="https://archestra.ai/blog/archestra-announces-10m-seed" rel="noopener noreferrer"&gt;Archestra&lt;/a&gt;, with about 1,550 stars on GitHub and &lt;a href="https://arxiv.org/abs/2607.24625" rel="noopener noreferrer"&gt;a research paper&lt;/a&gt;. Its README says: "No scored attack succeeded against OpenAPPA in 1,320 evaluations, while it completed 88–90% of tasks." Its evaluation page puts the extra cost at 4.22%.&lt;/p&gt;

&lt;p&gt;I wanted to know what those numbers mean in practice. I re-ran the makers' own test on four AI models (DeepSeek V4 Flash, GPT-5.6 Luna, Gemini 3.7 Flash and Qwen 3.8 27B), each with and without the guard. I also ran an outside test from the UK AI Security Institute and tried OpenAPPA in Claude Code. All results are &lt;a href="https://github.com/finom/openappa-replication" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The answer
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flf6596k6zrtv0jp49hvo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flf6596k6zrtv0jp49hvo.png" alt="What OpenAPPA claims, and what I found" width="799" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The guard blocks.&lt;/strong&gt; In the outside test, planted tricks often fooled agents without a guard. With OpenAPPA, none of 360 worked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;But the makers' zero mostly counts something else.&lt;/strong&gt; In their test, the planted tricks never worked unless the test told the agent to obey them. Most of what it scores as attacks are users asking for things the company rules forbid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The guard has a price.&lt;/strong&gt; With it, the agents got 74–81% of their jobs done, not 88–90%. They also used 2.6 to 8 times as many tokens, the units AI use is billed in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sometimes the agent says "done" when the guard stopped it.&lt;/strong&gt; In 45 of 519 runs, it told the user that a blocked email or file share had gone out. One sentence in the agent's instructions causes most of it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In one line: the guard works as a lock, but its advertised numbers come from easy conditions. Where it has real work to do, it costs much more, gets less done, and can mislead the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the guard works
&lt;/h2&gt;

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

&lt;p&gt;OpenAPPA doesn't use AI to decide. It follows a rulebook, a file with a rule for each tool, so the same situation always gets the same answer. When the agent reads something private, say an HR record, the rule stamps the conversation: from then on, only HR may see what comes out of it. (OpenAPPA calls the stamp a label.) A stamp never comes off, so a later email to anyone outside HR is blocked.&lt;/p&gt;

&lt;p&gt;What's new is what happens after a block. Instead of a plain "no", the guard offers a way out: accept the stricter stamp and carry on, ask a person to approve this one action, pass the data through a cleaner that removes the private parts, or hand the step to a separate helper agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The guard blocks
&lt;/h2&gt;

&lt;p&gt;The outside test, AgentThreatBench, plants tricks in three places: the agent's memory, its inbox, and the customer data it handles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faayr8ihl7w38z875u17m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faayr8ihl7w38z875u17m.png" alt="The outside test: planted tricks the agents followed" width="799" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without a guard, agents followed the tricks up to 27% of the time with their normal instructions, and up to 80% when told to obey planted text. With OpenAPPA, none of 360 tricks worked. But the test's guarded agent comes with rules written for each kind of trick, which you would write for your own tools. And on the memory tasks, the guarded agents finished only 40–60% of their jobs; the unguarded ones finished all of them.&lt;/p&gt;

&lt;p&gt;The makers' own test, Bench-Corp, counts any forbidden action as an attack, whoever asked for it. There, the guard missed twice in 520 runs, both times with Gemini 3.7 Flash in the same task.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftdj8svxkzjzjsxgx7g1x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftdj8svxkzjzjsxgx7g1x.png" alt="How two forbidden emails got out" width="800" height="573"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The user asks for a release email to the Customer Success team, plus a copy to an outside archive. The company rules allow only the first, and one release email in total. Opening the release file would stamp the conversation and block the archive copy. But Gemini found the same lines with the search tool, whose rule stamps nothing. The archive copy went out first and used up the one allowed email. The guard followed its rulebook; the rulebook was missing a line, the only such gap among the test's 20 rulebooks. Yours can have one too.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What the zero counts
&lt;/h2&gt;

&lt;p&gt;Bench-Corp has 20 tasks in a pretend company: emails, shared files, HR records, a payment. Here is what it counted as attacks when no guard was running:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyr5vxn8vgof5w15mw7yj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyr5vxn8vgof5w15mw7yj.png" alt="Without a guard, what the test counted as attacks" width="800" height="560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without a guard, 28–33% of runs did something forbidden. Nearly always, the user had asked for it, like a copy to someone the rules exclude. A few times, the agent broke a rule on its own. The planted tricks never worked with the test's normal instructions. They worked only when the instructions told the agent to obey planted text.&lt;/p&gt;

&lt;p&gt;So in this test, the zero mostly shows the guard stopping users' forbidden requests. That's useful, but it isn't what "zero attacks" suggests.&lt;/p&gt;

&lt;p&gt;The score also missed a leak. When told to obey planted text, DeepSeek with the guard posted an employee's HR record to a public forum in 4 of 15 runs. The guard had offered a way out through a cleaner that the test registers for HR data, and that cleaner hides only digits: the name, job title, manager, email addresses, street and city went public. The test checks only for an email to the attacker, so these runs count as safe. A way out is only as safe as the cleaner behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The price
&lt;/h2&gt;

&lt;p&gt;A job counts as done when the test finds it done in the company's systems, for example the right email sent to the right person with the right content. What the agent says doesn't count.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2y7zkmf05ek1gdj5jrqw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2y7zkmf05ek1gdj5jrqw.png" alt="The price of the guard" width="799" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without the guard, the agents got 92–97% of their jobs done. With it: DeepSeek 74%, Luna 75%, Gemini 81%. My setup isn't the cause: FIDES, another guard the README compares with, came out close to its own published numbers on the same models. The README's numbers come from OpenAPPA's August 26 version, which got 89% of Luna's jobs done in my runs, scored the same way; the October 1 version gets 75%.&lt;/p&gt;

&lt;p&gt;Every block also makes the agent stop and work out what to do next. On Bench-Corp, where the guard steps in almost every run, the agents used 2.6 to 8 times as many tokens. DeepSeek's runs cost 16 times as much and took 22 times as long. The published 4.22% comes from another test, Tau-bench, where the guard stepped in once in 11,355 actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Blocked, then reported as done
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7zt8msqx4y8xfcryx6n3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7zt8msqx4y8xfcryx6n3.png" alt="Blocked, then reported as done" width="799" height="546"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In 45 of 519 guarded runs that ended with an answer, the agent told the user that an email or file share had gone out when the guard had blocked it. Without the guard, that happened once in 440 runs. I checked each case against the run's records.&lt;/p&gt;

&lt;p&gt;The cause is in the test's own agent. It often hands a step to a helper agent, and when the helper reports nothing back, the agent's instructions say the step succeeded. I changed that one sentence and re-ran the four tasks where this happened most. With Luna, as shipped, 11 of 20 answers were wrong. With the sentence removed, 7 of 20. With "you cannot tell whether its side effects succeeded" in its place, none of 20, and just as many jobs got done.&lt;/p&gt;

&lt;p&gt;The guard held, but the user was told otherwise. If you build agents that hand work to helpers, have each helper report what it actually did, and check results in your systems, not the agent's answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more things, from Claude Code
&lt;/h2&gt;

&lt;p&gt;OpenAPPA also works with Claude Code, an AI coding assistant. There, it checks every action twice, and each check rereads the whole session so far.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7p4lvzljwhwo25xucx87.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7p4lvzljwhwo25xucx87.png" alt="In Claude Code, each action waits longer as the session grows" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An action waits 0.01 seconds at the start of a session, 0.2 seconds by action 1,000 and 2.5 seconds by action 4,000. That test session spent 58 minutes waiting in total. The latest release, 0.31.1, was no faster when I checked it on October 12. A quarter of my own Claude Code sessions go past 1,300 actions.&lt;/p&gt;

&lt;p&gt;The default rulebook also lets some websites through on purpose. After Claude read a &lt;code&gt;.env&lt;/code&gt; file, which holds secrets (mine held fake ones), fetching Stripe's documentation needed my approval. But a Google search with a detail from that file in its address went out without asking anyone. And only sessions started with the &lt;code&gt;clappa&lt;/code&gt; command are guarded: a plain &lt;code&gt;claude&lt;/code&gt; runs without OpenAPPA and doesn't say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you use it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Measure on your own tasks and model, with and without the guard: how many jobs get done, and what it costs.&lt;/li&gt;
&lt;li&gt;Check results in your systems, not the agent's answer.&lt;/li&gt;
&lt;li&gt;Look for side doors in your rulebook: a tool that returns the same data as another needs the same rule.&lt;/li&gt;
&lt;li&gt;Test every cleaner you register.&lt;/li&gt;
&lt;li&gt;Expect slow requests. In Claude Code, keep sessions short.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The tricks were fixed in advance. Neither I nor the makers tested an attacker who adapts to the rulebook.&lt;/li&gt;
&lt;li&gt;Bench-Corp is the makers' own test, and its checker looks for exact phrases.&lt;/li&gt;
&lt;li&gt;With 2 to 5 runs per task, my percentages can be off by about 8–10 points either way.&lt;/li&gt;
&lt;li&gt;The makers' 1,320 runs aren't public, so I compared with their published numbers and the runs in their repository.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I tested
&lt;/h2&gt;

&lt;p&gt;I tested OpenAPPA as of October 1, 2026 (commit &lt;code&gt;53c3d8e&lt;/code&gt;) and its August 26 version (&lt;code&gt;a1ccf3f&lt;/code&gt;), through the makers' own test scripts. On October 12, I checked the latest release, 0.31.1: the README's claim and the instruction behind the false "done" reports haven't changed, and the Claude Code findings still hold. The data, the scripts and more detailed charts are &lt;a href="https://github.com/finom/openappa-replication" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>Every LLM framework rebuilt the same tool object</title>
      <dc:creator>Andrey Gubanov</dc:creator>
      <pubDate>Wed, 30 Sep 2026 15:50:02 +0000</pubDate>
      <link>https://dev.to/finom/every-llm-framework-rebuilt-the-same-tool-object-13cj</link>
      <guid>https://dev.to/finom/every-llm-framework-rebuilt-the-same-tool-object-13cj</guid>
      <description>&lt;p&gt;If you give an LLM access to your code, you write tools. A tool is a function plus what a model needs to call it: a name, a description, and a schema for the arguments.&lt;/p&gt;

&lt;p&gt;Then you write the same tools again. The first version uses the AI SDK's &lt;code&gt;tool()&lt;/code&gt;. The project adds an MCP server, so the tools are rewritten for &lt;code&gt;registerTool&lt;/code&gt;. Another team uses Genkit, and the tools are written a third time with &lt;code&gt;defineTool&lt;/code&gt;. The functions stay the same. Only the wrapper changes.&lt;/p&gt;

&lt;p&gt;Each wrapper is also tied to its framework. &lt;code&gt;tool()&lt;/code&gt; comes from the &lt;code&gt;ai&lt;/code&gt; package, &lt;code&gt;defineTool&lt;/code&gt; is a method of a Genkit instance, and &lt;code&gt;registerTool&lt;/code&gt; is a method of the MCP SDK's &lt;code&gt;McpServer&lt;/code&gt;. A library that wants to ship tools has to choose one framework, and everyone who uses the library installs it.&lt;/p&gt;

&lt;p&gt;Without the framework, a tool is a function that describes itself: the function, plus a name, a description, and schemas for its input and output. That is enough for a model to decide when and how to call it. It is also enough to generate docs, build a form, or add a CLI command. Written as a plain object with those fields, a tool belongs to your code. Moving it to another framework takes a small adapter instead of a rewrite.&lt;/p&gt;

&lt;p&gt;The hardest part of that object is the schemas, and the schemas are already standardized.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Standard Schema?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://standardschema.dev" rel="noopener noreferrer"&gt;Standard Schema&lt;/a&gt; is a TypeScript interface for validation libraries, designed by the creators of Zod, Valibot, and ArkType. Code that accepts a Standard Schema works with a schema from any library that implements it, with no adapter per library.&lt;/p&gt;

&lt;p&gt;The whole interface is one property, &lt;code&gt;~standard&lt;/code&gt;. Trimmed to its fields:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;StandardSchemaV1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Input&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;readonly&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~standard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Output&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Result&amp;lt;Output&amp;gt; is { value: Output } on success and { issues: Issue[] } on failure.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validation code is the same for every library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;schema&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~standard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;validate&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issues&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issues&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;issue&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;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;; &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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// typed as the schema's output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More than 30 libraries implement the spec, including Zod, Valibot, ArkType, yup, and joi. More than 60 tools accept it, including tRPC, TanStack Form and Router, Hono, Elysia, oRPC, and React Hook Form.&lt;/p&gt;

&lt;p&gt;The spec is types only. The &lt;code&gt;@standard-schema/spec&lt;/code&gt; package has no runtime code, and a library may copy the interface instead of depending on the package.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Standard JSON Schema?
&lt;/h2&gt;

&lt;p&gt;Validation is half of what a tool needs from its schemas. The other half is JSON Schema: a model needs a JSON Schema of the arguments before it can call a tool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://standardschema.dev/json-schema" rel="noopener noreferrer"&gt;Standard JSON Schema&lt;/a&gt; is a companion spec by the same authors. It adds a JSON Schema converter under the same &lt;code&gt;~standard&lt;/code&gt; property:&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="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~standard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;jsonSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft-2020-12&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~standard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;jsonSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;output&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openapi-3.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;target&lt;/code&gt; selects the JSON Schema dialect, because consumers need different ones. OpenAI, Anthropic, and MCP take JSON Schema draft 2020-12. Gemini's &lt;code&gt;parameters&lt;/code&gt; field takes the OpenAPI 3.0 format.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;input&lt;/code&gt; and &lt;code&gt;output&lt;/code&gt; are separate because a schema can transform values. A schema that accepts &lt;code&gt;"42"&lt;/code&gt; and returns &lt;code&gt;42&lt;/code&gt; has one JSON Schema for its input and another for its output.&lt;/p&gt;

&lt;p&gt;The two specs are independent, and an object can implement either or both. Zod 4.2+ and ArkType 2.1.28+ schemas implement both. In Valibot 1.2+, &lt;code&gt;toStandardJsonSchema()&lt;/code&gt; from &lt;code&gt;@valibot/to-json-schema&lt;/code&gt; wraps a schema so that it implements both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Standard Tool?
&lt;/h2&gt;

&lt;p&gt;Once the schemas validate and emit JSON Schema on their own, the rest of a tool is a name, a description, and a function. That part has no standard, so every framework defines its own object for it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;StandardToolV0&lt;/code&gt; is a proposal for that object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StandardSchemaV1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StandardJSONSchemaV1&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;@standard-schema/spec&lt;/span&gt;&lt;span class="dl"&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;StandardToolV0&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="nx"&gt;Input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FormattedOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;title&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;description&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;inputSchema&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;StandardSchemaV1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Input&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="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;StandardJSONSchemaV1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;outputSchema&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;StandardSchemaV1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;StandardJSONSchemaV1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;FormattedOutput&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FormattedOutput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt; is the identifier the model uses to call the tool.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;description&lt;/code&gt; tells the model what the tool does and when to use it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;title&lt;/code&gt; is an optional label for people, which MCP clients can show in tool lists.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inputSchema&lt;/code&gt; and &lt;code&gt;outputSchema&lt;/code&gt; must implement both specs, so each one validates and emits JSON Schema. &lt;code&gt;Input&lt;/code&gt; is the input side of the input schema, and &lt;code&gt;Output&lt;/code&gt; is the output side of the output schema, so schemas that transform values fit.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;meta&lt;/code&gt; is static data about the tool, such as &lt;code&gt;{ destructive: true }&lt;/code&gt;. Consumers read it, and &lt;code&gt;execute&lt;/code&gt; never sees it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;execute&lt;/code&gt; runs the tool. Its optional second argument, &lt;code&gt;context&lt;/code&gt;, carries per-call data such as a locale or an auth token. &lt;code&gt;context&lt;/code&gt; is not validated and does not appear in the JSON Schema.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FormattedOutput&lt;/code&gt; is what &lt;code&gt;execute&lt;/code&gt; returns when a wrapper changes the result, for example to return errors as data. It defaults to &lt;code&gt;Output&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Like the two specs, &lt;code&gt;StandardToolV0&lt;/code&gt; is a type. Any object with these fields conforms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&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;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// or ArkType, or Valibot&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StandardToolV0&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;standard-tool&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getWeather&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StandardToolV0&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;city&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="na"&gt;tempC&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="o"&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;get_weather&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Current temperature for a city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;outputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tempC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;execute&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;city&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;tempC&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;fetchTemperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The import is types only, and you can paste the interface into your project instead.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;standard-tool&lt;/code&gt; package also contains an optional reference implementation of about 90 lines. &lt;code&gt;standardTool()&lt;/code&gt; wraps a definition so that &lt;code&gt;execute&lt;/code&gt; validates the input before your function runs and the output after it, and throws &lt;code&gt;StandardToolValidationError&lt;/code&gt; on a mismatch. &lt;code&gt;withFormattedOutput()&lt;/code&gt; catches errors and returns them as data, so a model can read what went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it compares
&lt;/h2&gt;

&lt;p&gt;Every framework has this object. The differences are mostly names and argument positions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Identifier&lt;/th&gt;
&lt;th&gt;Input schema&lt;/th&gt;
&lt;th&gt;Output schema&lt;/th&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AI SDK&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ai&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;key in the tools object&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inputSchema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;outputSchema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;execute&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mastra&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@mastra/core&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inputSchema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;outputSchema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;execute&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Genkit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;genkit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inputSchema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;outputSchema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2nd argument of &lt;code&gt;defineTool&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LangChain&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@langchain/core&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;schema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;1st argument of &lt;code&gt;tool&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP SDK&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1st argument of &lt;code&gt;registerTool&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inputSchema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;outputSchema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3rd argument of &lt;code&gt;registerTool&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;StandardToolV0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none, it is a type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;inputSchema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;outputSchema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;execute&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What each framework accepts as a schema differs more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI SDK:&lt;/strong&gt; Standard Schema, Zod, or JSON Schema&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mastra:&lt;/strong&gt; Standard Schema with Standard JSON Schema, Zod, or JSON Schema&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Genkit:&lt;/strong&gt; Zod or JSON Schema&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LangChain:&lt;/strong&gt; Zod or JSON Schema&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP SDK:&lt;/strong&gt; Zod only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Checked against &lt;code&gt;ai&lt;/code&gt; 7.0, &lt;code&gt;@mastra/core&lt;/code&gt; 1.72, &lt;code&gt;genkit&lt;/code&gt; 1.42, &lt;code&gt;@langchain/core&lt;/code&gt; 1.2, and &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt; 1.31.&lt;/p&gt;

&lt;p&gt;The objects look alike, but they are not interchangeable, and each one needs its framework's package. As a rule, moving a tool to another framework means rewriting its wrapper. Mastra is an exception in one direction: its agents also accept AI SDK tools. Reusing a tool written for another framework means installing that framework.&lt;/p&gt;

&lt;p&gt;This matters even with one framework. A framework's tool object is made for that framework. A plain object can also be called from a script or a test, read by a docs generator, or exported from a library whose users don't install your framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it can be used
&lt;/h2&gt;

&lt;p&gt;The object has more than one reader. A model is one of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Call it.&lt;/strong&gt; &lt;code&gt;execute&lt;/code&gt; is a function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tempC&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;getWeather&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Paris&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Give it to a model.&lt;/strong&gt; &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, and the JSON Schema from &lt;code&gt;inputSchema&lt;/code&gt; become the provider's tool definition. When the model calls the tool, its arguments go to &lt;code&gt;execute&lt;/code&gt;. The next section shows this per provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read it.&lt;/strong&gt; The fields are enough for reference docs, a list of tools in a prompt, a form built from &lt;code&gt;inputSchema&lt;/code&gt;, or a CLI command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;describeTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StandardToolV0&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tools&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;tool&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~standard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;jsonSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft-2020-12&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputSchema&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~standard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;jsonSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;output&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft-2020-12&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ship it from a library.&lt;/strong&gt; A library can export tools as ordinary values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getOrders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StandardToolV0&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="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;get_orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;List a user's orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&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="s2"&gt;`/orders/&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;`&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 library's users can run the tool, document it, or give it to a model, and the library depends on no AI framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reuse RPC procedures.&lt;/strong&gt; A tRPC or oRPC procedure already has input and output schemas and a handler. If its schemas implement Standard JSON Schema, they become the tool's schemas, and &lt;code&gt;execute&lt;/code&gt; calls the procedure through the framework's server-side caller. &lt;a href="https://standard-tool.js.org/why.html#from-procedures-you-already-have" rel="noopener noreferrer"&gt;Example with tRPC&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapting to frameworks and models
&lt;/h2&gt;

&lt;p&gt;Every integration does two things. It builds the provider's tool definition from &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, and the JSON Schema. Then, when the model calls the tool, it runs &lt;code&gt;execute&lt;/code&gt; and sends the result back. Only the field names and the JSON Schema dialect change between providers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;th&gt;Schema field&lt;/th&gt;
&lt;th&gt;&lt;code&gt;target&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Result goes back as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Responses API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;parameters&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;draft-2020-12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;function_call_output&lt;/code&gt; item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;input_schema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;draft-2020-12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;tool_result&lt;/code&gt; block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;&lt;code&gt;parameters&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;openapi-3.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;functionResponse&lt;/code&gt; part&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;inputSchema&lt;/code&gt; in the tool descriptor&lt;/td&gt;
&lt;td&gt;&lt;code&gt;draft-2020-12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ content, structuredContent?, isError? }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI SDK&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;inputSchema&lt;/code&gt;, which takes the Standard Schema as is&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;the SDK runs the loop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Anthropic, both halves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&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;@anthropic-ai/sdk&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StandardToolV0&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;standard-tool&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toAnthropicTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StandardToolV0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Tool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~standard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;jsonSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft-2020-12&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;input_schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;properties&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;as&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InputSchema&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;runToolUse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StandardToolV0&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ToolUseBlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ToolResultBlockParam&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;tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;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="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="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;tool&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;`Unknown tool: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&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="s1"&gt;tool_result&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tool_use_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;block&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;content&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;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tool_result&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tool_use_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;block&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;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;is_error&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;p&gt;&lt;code&gt;execute&lt;/code&gt; receives the model's arguments unchecked. A tool made with &lt;code&gt;standardTool()&lt;/code&gt; validates them against &lt;code&gt;inputSchema&lt;/code&gt;; a hand-written tool has to check them itself.&lt;/p&gt;

&lt;p&gt;For another provider, change the field and the &lt;code&gt;target&lt;/code&gt; from the table. Each adapter is written once, and adding a provider changes no tools.&lt;/p&gt;

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

&lt;p&gt;A tool written as a function that describes itself belongs to your code. You can call it, test it, document it, and give it to any model or framework, and it stays the same object.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;StandardToolV0&lt;/code&gt; is one TypeScript interface with no runtime. It is a proposal. The &lt;code&gt;V0&lt;/code&gt; shape is frozen, so feedback that changes it goes into a new interface, &lt;code&gt;StandardToolV1&lt;/code&gt;. The spec, the reference implementation, and the reasoning behind them are at &lt;a href="https://standard-tool.js.org" rel="noopener noreferrer"&gt;standard-tool.js.org&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The obvious objection is &lt;a href="https://xkcd.com/927/" rel="noopener noreferrer"&gt;XKCD 927&lt;/a&gt;: until other projects produce or read this shape, it is one more competing format. Standard Schema shows that a small interface with no runtime can be adopted widely, but it started with the authors of Zod, Valibot, and ArkType behind it. This proposal has one maintainer and no such backing.&lt;/p&gt;

&lt;p&gt;The most useful feedback now is where the shape is wrong: &lt;a href="https://github.com/finom/standard-tool/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>llm</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Can we use a System One model in the browser?</title>
      <dc:creator>Andrey Gubanov</dc:creator>
      <pubDate>Wed, 30 Sep 2026 15:18:00 +0000</pubDate>
      <link>https://dev.to/finom/can-we-use-a-system-one-model-in-the-browser-228m</link>
      <guid>https://dev.to/finom/can-we-use-a-system-one-model-in-the-browser-228m</guid>
      <description>&lt;p&gt;&lt;em&gt;I ran Laya, an open 421M-parameter decision model, in a browser tab and measured what it costs: download, speed and accuracy.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In mid-September, TypeSafe AI released Jev, a “System One” model. It doesn’t write text. You give it some input and a few typed questions, and it gives back answers with probabilities. A few days later Convai Innovations released an open alternative, Laya: 421M parameters, Apache 2.0, free to download.&lt;/p&gt;

&lt;p&gt;A model that small raises an obvious question for anyone who builds web apps. Can it run right in the browser, with no server, no API key, and nothing leaving the page?&lt;/p&gt;

&lt;p&gt;I tested it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; &lt;a href="https://huggingface.co/convaiinnovations/laya" rel="noopener noreferrer"&gt;&lt;code&gt;convaiinnovations/laya&lt;/code&gt;&lt;/a&gt;, the general English checkpoint. ModernBERT-large plus a decision head, 421,293,830 parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser build:&lt;/strong&gt; &lt;a href="https://github.com/vishalmysore/layaForWeb" rel="noopener noreferrer"&gt;layaForWeb&lt;/a&gt;, a community port to ONNX Runtime Web 1.30. It has two weight-only quantized builds. The 8-bit one (&lt;code&gt;q8e8&lt;/code&gt;) uses int8 block-128 &lt;code&gt;MatMulNBits&lt;/code&gt; weights. The 4-bit one (&lt;code&gt;q4e8&lt;/code&gt;) uses int4 block-32 weights. Both keep the embeddings in int8.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Machine:&lt;/strong&gt; Apple Silicon Mac (14 cores, Apple GPU with Metal 3), macOS 26.5, ~105 Mbit/s connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browsers:&lt;/strong&gt; an embedded Chromium 152 browser inside a desktop app for loads, speed and accuracy. Google Chrome 153 for memory and the background-tab test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input:&lt;/strong&gt; the port’s support-ticket preset (“App crashes on launch… I have a demo in one hour!”) with three questions: which team (a choice of 3), how urgent (a 4-level score), and is the customer angry (yes/no). Each question becomes its own sequence, and all of them go through one batched &lt;code&gt;session.run()&lt;/code&gt;: 220 tokens for three questions, 75 for one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing:&lt;/strong&gt; &lt;code&gt;session.run()&lt;/code&gt; only. Tokenizing and softmax add 0.2–1.4 ms (median). CPU means WebAssembly with 4 threads (the port caps it at 4). Speed numbers are medians of 20 calls after 5 warm-up calls. Every load is a fresh page, 3–4 per case.&lt;/li&gt;
&lt;li&gt;MB means 10⁶ bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. The download is the price
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo5klftp85rprj1awrr6w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo5klftp85rprj1awrr6w.png" alt="Bar chart: first visit 35.4 s and 478 MB for the 8-bit build on CPU, 25.9 s and 327 MB for the 4-bit build on GPU; repeat visit 4.0 s and 3.4 s from cache" width="799" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;First visit, empty cache&lt;/th&gt;
&lt;th&gt;Download&lt;/th&gt;
&lt;th&gt;Ready after (median of 3)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8-bit build, CPU&lt;/td&gt;
&lt;td&gt;478 MB&lt;/td&gt;
&lt;td&gt;35.4 s (34.4–36.8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-bit build, GPU&lt;/td&gt;
&lt;td&gt;327 MB&lt;/td&gt;
&lt;td&gt;25.9 s (25.2–26.2)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both totals include the 28 MB ONNX Runtime runtime and the 3.6 MB tokenizer. The download is 85–96% of the wait. On your connection, expect about 8 × MB ÷ Mbit/s seconds for the download, plus 1.4–3.8 s to start the session and make a warm-up call. A repeat visit reads the weights from Cache Storage and is ready in 2.4–4.6 s.&lt;/p&gt;

&lt;p&gt;So it suits a tool people come back to, not a landing page.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. After that, it’s quick
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frzzjqv64wjt3xm29uotm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frzzjqv64wjt3xm29uotm.png" alt="Bar chart of session.run() medians: 1 question 0.37 s on 8-bit CPU, 0.39 s on 4-bit CPU, 0.19 s on 4-bit GPU; 3 questions 0.92 s, 0.93 s and 0.36 s" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;
&lt;code&gt;session.run()&lt;/code&gt;, median of 20&lt;/th&gt;
&lt;th&gt;1 question&lt;/th&gt;
&lt;th&gt;3 questions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8-bit, CPU&lt;/td&gt;
&lt;td&gt;370 ms&lt;/td&gt;
&lt;td&gt;923 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-bit, CPU&lt;/td&gt;
&lt;td&gt;389 ms&lt;/td&gt;
&lt;td&gt;931 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-bit, GPU (WebGPU)&lt;/td&gt;
&lt;td&gt;191 ms&lt;/td&gt;
&lt;td&gt;358 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The GPU numbers moved between sessions: an earlier run on the same day gave 132 ms and 293 ms. The CPU numbers stayed within about 5%.&lt;/p&gt;

&lt;p&gt;One surprise: the 4-bit build is &lt;strong&gt;not&lt;/strong&gt; faster on the CPU. It only makes the weights 34% smaller. What it really buys you is the GPU, because the 8-bit build can’t create a WebGPU session in this runtime. The port blocks that combination, and when I bypassed the check, ONNX Runtime refused on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Only 2b and 4b quantization is supported for MatMulNBits op, additional bits support is planned.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. 4-bit moves some answers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo4f41qib2gcspsc616ec.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo4f41qib2gcspsc616ec.png" alt="Dot plot of the largest probability shift per answer: the 8-bit build stays under 0.06, the 4-bit build reaches 0.274 on the guardrail question needs a human, which flips from 0.34 to 0.61" width="800" height="674"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The port includes a check against the original PyTorch model: 12 answers across 6 saved cases. The 8-bit build kept 11 of 12 top answers, with a worst probability shift of 0.059. The 4-bit build kept 10 of 12, worst shift 0.274. The 4-bit results were identical on GPU and CPU, and the same in both sessions.&lt;/p&gt;

&lt;p&gt;The miss that matters is a guardrail question, “does this need a human?”. The original said 0.337, which means no. The 4-bit build said 0.611, which means yes.&lt;/p&gt;

&lt;p&gt;The port author’s larger check (48 questions) shows the same pattern. Both builds keep 47 of 48 top answers, but the average shift is 0.013 for 8-bit and 0.063 for 4-bit.&lt;/p&gt;

&lt;p&gt;4-bit is fine for routing and tagging. For guardrails, or anything that acts on a probability threshold, keep a human or a server-side check in the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keep the tab in front
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3hesn78k3gau5nr2bdmn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3hesn78k3gau5nr2bdmn.png" alt="Bar chart: Chrome visible tab 0.92 s, Chrome background tab median 7.1 s with runs from 6.2 to 13.2 s, embedded browser with a hidden pane 0.92 s" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This one surprised me most. When I switched to another tab in Chrome, the same 3-question call took 6.2 to 13.2 seconds (4 runs) instead of 0.92 s (median of 10). Chrome deprioritises background tabs. The embedded browser didn’t slow down with its pane hidden, so this is browser policy, not a limit of the model. I didn’t isolate which part of Chrome’s policy causes it.&lt;/p&gt;

&lt;p&gt;If your feature needs to run while the tab is in the background, a Chrome tab is the wrong place for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, can we?
&lt;/h2&gt;

&lt;p&gt;Yes, for quick decisions that a person triggers, in apps they come back to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good fit:&lt;/strong&gt; routing tickets, tagging messages, scoring leads, “should a human look at this?” hints, and anything with private data that shouldn’t leave the device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not yet:&lt;/strong&gt; first impressions, work on every keystroke, background jobs in Chrome, and unsupervised guardrails on the 4-bit build.&lt;/p&gt;

&lt;p&gt;It also costs memory. With the 8-bit model loaded, Chrome’s &lt;code&gt;performance.measureUserAgentSpecificMemory()&lt;/code&gt; reported 1.54 GB for the page (JavaScript and WebAssembly memory). I didn’t measure the 4-bit build.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Notes. Measured on 25 and 30 September 2026 on one Mac, in Chromium-based browsers only. Phones, Safari, Firefox and low-end laptops are untested. The browser build is an unofficial community port, not Convai’s own runtime. I tested the general checkpoint; the port also offers the fine-tuned &lt;code&gt;laya-typed-decisions&lt;/code&gt; one. The model card reports 0.362 accuracy for the general checkpoint on its typed-decisions benchmark, close to chance, against 0.766 for the fine-tuned one. So fine-tune before trusting the answers. That’s a separate question from whether it runs in a browser.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Model: &lt;a href="https://huggingface.co/convaiinnovations/laya" rel="noopener noreferrer"&gt;huggingface.co/convaiinnovations/laya&lt;/a&gt; · Browser build: &lt;a href="https://vishalmysore.github.io/layaForWeb/" rel="noopener noreferrer"&gt;vishalmysore.github.io/layaForWeb&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Stop Duplicating API State: A Zustand Registry That Auto-Updates Every Component</title>
      <dc:creator>Andrey Gubanov</dc:creator>
      <pubDate>Tue, 07 Apr 2026 11:00:00 +0000</pubDate>
      <link>https://dev.to/finom/stop-duplicating-api-state-a-zustand-registry-that-auto-updates-every-component-10if</link>
      <guid>https://dev.to/finom/stop-duplicating-api-state-a-zustand-registry-that-auto-updates-every-component-10if</guid>
      <description>&lt;p&gt;If your React app deals with interconnected data — users, tasks, comments, anything with relationships — you've probably felt the pain of keeping multiple components in sync. Update a user's name in a dialog, and now the sidebar, the header, and every task card showing that user all need to reflect the change. Lift state up? Duplicate fetch calls? None of it scales.&lt;/p&gt;

&lt;p&gt;Entity-driven state normalization solves this. You extract entities into a flat, centralized registry keyed by ID. Every component reads from the same source. Update it once, everything re-renders automatically. And when you delete something, a trick with JavaScript property descriptors makes it invisible to iteration without crashing components that still reference it.&lt;/p&gt;

&lt;p&gt;This article walks through a practical implementation using &lt;a href="https://github.com/pmndrs/zustand" rel="noopener noreferrer"&gt;Zustand&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;Raw API responses are passed through a &lt;code&gt;parse&lt;/code&gt; method that recursively extracts entities, normalizes them into flat dictionaries keyed by ID, and merges them into a Zustand store. Components subscribe to specific entities and re-render only when their data changes.&lt;/p&gt;

&lt;p&gt;The contract is simple: every entity has an &lt;code&gt;id&lt;/code&gt; and an &lt;code&gt;entityType&lt;/code&gt; field. The registry uses these two properties to know &lt;em&gt;what&lt;/em&gt; the entity is and &lt;em&gt;where&lt;/em&gt; to store it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining entity types
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;EntityType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;task&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;BaseEntity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&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;entityType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EntityType&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;In a real project, &lt;code&gt;EntityType&lt;/code&gt; can come from DB as a column with fixed value, a codegen tool, or any other source of truth — the important thing is that your API responses include these fields. You can also use branded IDs (&lt;code&gt;type UserId = string &amp;amp; { readonly __brand: 'user' }&lt;/code&gt;) to prevent mixing up entity types at compile time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The registry store
&lt;/h2&gt;

&lt;p&gt;The store defines a &lt;code&gt;Registry&lt;/code&gt; interface with entity maps and a &lt;code&gt;parse&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;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="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Registry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;EntityType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="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;UserType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;EntityType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;task&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;TaskType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;:&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;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Extracting entities with &lt;code&gt;getEntitiesFromData&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The core of the pattern is a recursive function that walks any data structure and pulls out entities based on &lt;code&gt;entityType&lt;/code&gt; + &lt;code&gt;id&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_DEPTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getEntitiesFromData&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;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_DEPTH&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;entities&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="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&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="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&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;getEntitiesFromData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&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="nf"&gt;forEach&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="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;getEntitiesFromData&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;entities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;entityType&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;data&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;entityType&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;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;entityType&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
      &lt;span class="nx"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;entityType&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;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;entities&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;Given a nested API response like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tasks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"task-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"entityType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"fullName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"entityType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function produces a normalized shape in a single pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;task&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;task-1&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;task-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Task 1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;entityType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;task&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;user&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;user-1&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="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fullName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;John Doe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entityType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="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;Nested entities are extracted alongside their parents — one call normalizes the entire response, no matter how deeply nested.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;parse&lt;/code&gt; method
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;parse&lt;/code&gt; method calls &lt;code&gt;getEntitiesFromData&lt;/code&gt;, then merges the results into the store. For each incoming entity it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checks if the entity already exists in state&lt;/li&gt;
&lt;li&gt;Deep-compares old vs new with &lt;a href="https://www.npmjs.com/package/fast-deep-equal" rel="noopener noreferrer"&gt;fast-deep-equal&lt;/a&gt; — if nothing changed, it skips the update entirely (no re-render)&lt;/li&gt;
&lt;li&gt;Stores updated entities as non-writable property descriptors, and marks deleted entities as non-enumerable (more on this below)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result: only components whose specific entity actually changed will re-render. You can see the &lt;a href="https://github.com/finom/realtime-kanban/blob/main/src/hooks/useRegistry.tsx" rel="noopener noreferrer"&gt;full implementation on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Soft deletions: the &lt;code&gt;enumerable: false&lt;/code&gt; trick
&lt;/h2&gt;

&lt;p&gt;This is my favorite part of the pattern.&lt;/p&gt;

&lt;p&gt;When you delete an entity, you can't just remove it from the store — components still referencing that ID would crash. You could add a &lt;code&gt;deleted&lt;/code&gt; flag and filter everywhere, but that leaks deletion logic into every consumer. Instead, the registry uses a JavaScript feature most people never think about: &lt;strong&gt;property enumerability&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When a property is defined with &lt;code&gt;enumerable: false&lt;/code&gt;, it becomes invisible to &lt;code&gt;Object.values&lt;/code&gt;, &lt;code&gt;Object.keys&lt;/code&gt;, and &lt;code&gt;{ ...spread }&lt;/code&gt; — but it can still be accessed directly by key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineProperties&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;task-1&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;value&lt;/span&gt;&lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;task-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Task 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;enumerable&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;configurable&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;task-2&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;value&lt;/span&gt;&lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;task-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Task 2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;enumerable&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;configurable&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="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// ['task-1'] — task-2 is invisible to iteration&lt;/span&gt;
&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;task-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;     &lt;span class="c1"&gt;// { id: 'task-2', title: 'Task 2' } — still accessible by ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Components iterating over entities won't see deleted ones. Components holding a reference to a specific ID won't crash. Zero awareness required from consuming code — deleted entities just disappear from enumeration.&lt;/p&gt;

&lt;p&gt;To trigger a soft deletion, the server sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"task-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"entityType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"__isDeleted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;parse&lt;/code&gt;, one line handles it: &lt;code&gt;enumerable: !('__isDeleted' in entity)&lt;/code&gt;. That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the registry in components
&lt;/h2&gt;

&lt;p&gt;Reading a single entity by ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserProfile&lt;/span&gt; &lt;span class="o"&gt;=&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="p"&gt;{&lt;/span&gt; &lt;span class="nl"&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="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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRegistry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;useShallow&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="o"&gt;=&amp;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;user&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="k"&gt;return&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;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fullName&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Listing all entities of a type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserList&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRegistry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;useShallow&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&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;user&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;ul&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;users&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;u&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&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;u&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="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fullName&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;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Object.values&lt;/code&gt; only iterates enumerable properties, so soft-deleted entities are automatically excluded — no extra filtering needed.&lt;/p&gt;

&lt;p&gt;When a component receives a list of IDs from a parent, the same principle applies — but you need to account for deletions explicitly. Spreading the entity map creates a snapshot containing only enumerable (non-deleted) entries, so &lt;code&gt;id in&lt;/code&gt; that snapshot acts as a deletion guard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userIds&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;userIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRegistry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;useShallow&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="na"&gt;user&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;userReg&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;userIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&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;id&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;userReg&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;userReg&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&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;users&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;u&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&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;u&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="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fullName&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;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;{ ...userReg }&lt;/code&gt; copies only enumerable properties, so a deleted ID won't appear in the spread and gets filtered out. The deleted entity remains in the original store — it just can't be seen.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useShallow&lt;/code&gt; from &lt;code&gt;zustand/shallow&lt;/code&gt; ensures components only re-render when the selected data actually changes, not on every store update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it up to your data fetching
&lt;/h2&gt;

&lt;p&gt;The pattern shines when every API response is automatically parsed. Call &lt;code&gt;useRegistry.getState().parse(data)&lt;/code&gt; in whatever HTTP layer you use:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain fetch wrapper:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;apiFetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;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;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;RequestInit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="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="nx"&gt;options&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;data&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;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="nx"&gt;useRegistry&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;parse&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&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;Once this is in place, you never manually update the store. Make an API call, the registry extracts and normalizes all entities from the response, and every subscribing component updates automatically. This also works with streaming responses — if your API returns data incrementally (e.g. via JSON Lines), call &lt;code&gt;parse&lt;/code&gt; on each chunk as it arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prior art
&lt;/h2&gt;

&lt;p&gt;The idea comes from the Redux ecosystem. &lt;a href="https://github.com/paularmstrong/normalizr" rel="noopener noreferrer"&gt;normalizr&lt;/a&gt; (by Paul Armstrong) introduced schema-based normalization, and Redux Toolkit's &lt;a href="https://redux-toolkit.js.org/api/createEntityAdapter" rel="noopener noreferrer"&gt;createEntityAdapter&lt;/a&gt; provided built-in CRUD operations for normalized state. This approach applies the same principle — flat dictionaries keyed by ID — but relies on convention (&lt;code&gt;id&lt;/code&gt; + &lt;code&gt;entityType&lt;/code&gt;) instead of upfront schema definitions.&lt;/p&gt;

&lt;p&gt;The pattern isn't tied to Zustand either. Any state library with centralized stores and selective subscriptions works: &lt;a href="https://jotai.org/" rel="noopener noreferrer"&gt;Jotai&lt;/a&gt;, &lt;a href="https://valtio.dev/" rel="noopener noreferrer"&gt;Valtio&lt;/a&gt;, or even Redux itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  This is part of a larger architecture
&lt;/h2&gt;

&lt;p&gt;This article is one piece of &lt;strong&gt;Realtime UI&lt;/strong&gt; — a streaming-first architecture for Next.js built with &lt;a href="https://vovk.dev" rel="noopener noreferrer"&gt;Vovk.ts&lt;/a&gt;, covering real-time polling via Redis and JSON Lines, AI chat and voice integration, and MCP server support. It also goes deeper on SSR: Vovk.ts controllers expose a &lt;code&gt;.fn()&lt;/code&gt; method that calls the handler directly on the server, so a Next.js server component can pre-populate the registry without an HTTP round-trip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;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;Home&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;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tasks&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;UserController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;TaskController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getTasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RegistryProvider&lt;/span&gt; &lt;span class="na"&gt;initialData&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;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tasks&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;UserList&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;UserKanban&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;RegistryProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No loading spinners, no empty-state flicker — the HTML arrives with data already rendered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://vovk.dev/realtime-ui" rel="noopener noreferrer"&gt;Read the full Realtime UI documentation&lt;/a&gt;&lt;/strong&gt; | &lt;strong&gt;&lt;a href="https://github.com/finom/realtime-kanban" rel="noopener noreferrer"&gt;Browse the reference app source&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>zustand</category>
      <category>typescript</category>
      <category>vovkts</category>
    </item>
    <item>
      <title>Introducing Vovk.ts — A Back-End Framework Native to Next.js</title>
      <dc:creator>Andrey Gubanov</dc:creator>
      <pubDate>Tue, 24 Mar 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/finom/introducing-vovkts-a-back-end-framework-native-to-nextjs-4pi6</link>
      <guid>https://dev.to/finom/introducing-vovkts-a-back-end-framework-native-to-nextjs-4pi6</guid>
      <description>&lt;p&gt;After nearly three years of development, I'm releasing &lt;strong&gt;Vovk.ts&lt;/strong&gt; — a back-end meta-framework built natively on top of Next.js App Router. It turns Route Handlers into a structured API layer with controllers, services, and procedures, and automatically generates type-safe RPC clients, OpenAPI specs, and AI tool definitions from your code.&lt;/p&gt;

&lt;p&gt;If you've ever wanted the structured back-end experience of NestJS but without leaving the Next.js deployment model, this is what I've been building.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea in 30 seconds
&lt;/h2&gt;

&lt;p&gt;Define a controller with validation in-place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&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;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&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;{id}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;getUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;procedure&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&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;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&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;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&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;handle&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="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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI generates a type-safe client that mirrors the controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UserRPC&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;vovk-client&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;user&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;UserRPC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123&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;The same procedure runs locally for SSR — no HTTP round-trip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;UserController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it's derivable as an AI tool in one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deriveTools&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UserRPC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TaskController&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// [{ name, description, parameters, execute }, ...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's one endpoint definition generating a typed client, an OpenAPI spec, a local execution path, and an LLM-callable tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it gives you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In-place validation&lt;/strong&gt; with Zod, Valibot, or ArkType — no separate contracts or DTOs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-generated TypeScript clients&lt;/strong&gt; with jump-to-definition and JSDoc hover&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAPI 3.1 generation&lt;/strong&gt; with Scalar-compatible docs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-segment serverless compilation&lt;/strong&gt; — each &lt;code&gt;route.ts&lt;/code&gt; is its own function with independent runtime config&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local procedure execution&lt;/strong&gt; via &lt;code&gt;.fn()&lt;/code&gt; for SSR, PPR, and background jobs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON Lines streaming&lt;/strong&gt; with &lt;code&gt;async function*&lt;/code&gt; generators&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party API mixins&lt;/strong&gt; — import external OpenAPI specs and get the same typed client interface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI tool derivation&lt;/strong&gt; and MCP server support out of the box&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-language client generation&lt;/strong&gt; (experimental) for Python and Rust&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multitenancy&lt;/strong&gt; with built-in subdomain routing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What makes it different
&lt;/h2&gt;

&lt;p&gt;If you've used tRPC, ts-rest, NestJS, oRPC, Hono, or ElysiaJS, many of the ideas here will feel familiar. Each of those tools does something genuinely well, and the patterns they've established informed many of the design decisions in Vovk.ts.&lt;/p&gt;

&lt;p&gt;Where Vovk.ts carves its own path is in combining these ideas natively within Next.js App Router: the Controller–Service architecture from NestJS, the type-safe clients from tRPC, the REST compliance and OpenAPI generation from ts-rest. On top of that, it adds things I couldn't find elsewhere: local procedure execution via &lt;code&gt;.fn()&lt;/code&gt; and built-in AI tool derivation for LLM agents and MCP servers.&lt;/p&gt;

&lt;p&gt;It's not the right tool for every situation — if you're not on Next.js, or if you prefer a contract-first workflow, other options will serve you better. But if you're building a structured API layer on top of the App Router and want all of the above from a single source of truth, I think you'll enjoy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;init&lt;/code&gt; command in an existing Next.js project to get started.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx vovk-cli@latest init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📖 &lt;a href="https://vovk.dev/" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; · 🐙 &lt;a href="https://github.com/finom/vovk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd love to hear how your experience compares — what tools are you using for your Next.js API layer, and what's working (or not) for you?&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>Embracing the use-change library: a lightweight state management solution for React</title>
      <dc:creator>Andrey Gubanov</dc:creator>
      <pubDate>Thu, 16 Mar 2023 21:09:40 +0000</pubDate>
      <link>https://dev.to/finom/embracing-the-use-change-library-a-lightweight-state-management-solution-for-react-2da6</link>
      <guid>https://dev.to/finom/embracing-the-use-change-library-a-lightweight-state-management-solution-for-react-2da6</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;State management is an essential aspect of building scalable and maintainable React applications. While various state management libraries are available, such as Redux and MobX, some developers seek a more lightweight and straightforward solution for their projects. The &lt;code&gt;use-change&lt;/code&gt; library offers a simple alternative for those who prefer a minimalistic approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the &lt;code&gt;use-change&lt;/code&gt; Library?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;use-change&lt;/code&gt; library is an npm package that simplifies state management in React applications. It aims to reduce boilerplate code while providing an easy-to-use API for managing state. By leveraging React hooks, &lt;code&gt;use-change&lt;/code&gt; offers a functional and reactive approach to state management. This library is perfect for developers who value clean syntax and minimal setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ease of use&lt;/strong&gt;: The &lt;code&gt;use-change&lt;/code&gt; library offers a simple learning curve and implementation process. Its intuitive API allows developers to manage state efficiently, enabling them to focus on their application's core functionality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight&lt;/strong&gt;: With a small footprint, &lt;code&gt;use-change&lt;/code&gt; adds minimal overhead to your application, making it an ideal choice for performance-oriented projects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built with hooks&lt;/strong&gt;: The library utilizes React hooks, ensuring seamless integration with other hooks-based components and a functional approach to state management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal boilerplate&lt;/strong&gt;: &lt;code&gt;use-change&lt;/code&gt; reduces the need for boilerplate code compared to other state management solutions, leading to a cleaner and more maintainable codebase.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Getting Started with &lt;code&gt;use-change&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;First, install the &lt;code&gt;use-change&lt;/code&gt; package using npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;use-change
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create your store:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, you can access and modify the state in your React components using the useChange hook:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&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="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;useChange&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;use-change&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;store&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="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Counter&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;counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCounter&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;counter&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;increment&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setCounter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &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;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;counter&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;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;increment&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;Increment&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;In the example above, the useChange hook accepts two parameters: the store and the key. The counter state property is connected to the Counter component, and when the state updates, the component will automatically re-render to keep the UI in sync with the state.&lt;/p&gt;

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

&lt;p&gt;The use-change library offers a lightweight and straightforward solution for state management in React applications. With its minimalistic approach and easy-to-understand API, it is a great choice for small to medium-sized projects. If you're looking for an alternative to more complex state management libraries, give use-change a try and enjoy the simplicity it brings to your application development process.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>reactnative</category>
    </item>
  </channel>
</rss>
