<?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: Sanjay Singh</title>
    <description>The latest articles on DEV Community by Sanjay Singh (@sanjay_singh_1).</description>
    <link>https://dev.to/sanjay_singh_1</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%2F3961409%2F9efd98f2-cabb-46b0-817f-1f8cefd2c79b.jpg</url>
      <title>DEV Community: Sanjay Singh</title>
      <link>https://dev.to/sanjay_singh_1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sanjay_singh_1"/>
    <language>en</language>
    <item>
      <title>Cross-Posting for Developers: How to Reach More Readers Without Losing Your SEO</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Wed, 09 Sep 2026 04:04:55 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/cross-posting-for-developers-how-to-reach-more-readers-without-losing-your-seo-34hk</link>
      <guid>https://dev.to/sanjay_singh_1/cross-posting-for-developers-how-to-reach-more-readers-without-losing-your-seo-34hk</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Cross-posting expands your reach across developer networks like Dev.to, Hashnode, Medium, WordPress, Bluesky, and Mastodon. But managing manual re-formatting and platform quirks is exhausting, and missing canonical tags can destroy your search rankings. Learn how canonical URLs, automated formatting (including Mermaid diagrams), Git backups, and modern AEO/GEO optimization let you syndicate everywhere while keeping complete ownership of your work.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  You wrote a good technical post. Now what?
&lt;/h3&gt;

&lt;p&gt;Writing in-depth technical articles is demanding. You spent hours debugging an edge case, verifying benchmarks, constructing architecture diagrams, and refining code snippets until the explanation clicked.&lt;/p&gt;

&lt;p&gt;Publishing that post is where the next challenge begins.&lt;/p&gt;

&lt;p&gt;If you publish exclusively to your own blog, you run into the cold-start problem: without an established audience or high domain authority, very few developers will ever discover your post. If you publish solely inside a walled garden or a single developer network, you become dependent on that platform’s algorithm, formatting constraints, or reading paywalls.&lt;/p&gt;

&lt;p&gt;The logical move is &lt;strong&gt;cross-posting&lt;/strong&gt; (syndication): putting your writing in front of existing communities on Dev.to, Hashnode, Medium, and WordPress, while sharing dispatches across developer-friendly social networks like Bluesky and Mastodon.&lt;/p&gt;

&lt;p&gt;The concept is straightforward. The execution, however, is notoriously tedious. Once the article is finished, you are suddenly stuck converting Markdown formats, fixing broken code blocks, uploading images across separate dashboards, remembering each platform’s canonical URL settings, and tracking fragmented views across multiple sites.&lt;/p&gt;

&lt;p&gt;That is the unglamorous side of content distribution nobody talks about: the friction between &lt;em&gt;“I finished the article”&lt;/em&gt; and &lt;em&gt;“the article is live everywhere.”&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The part that’s easy to get wrong: Canonical URLs
&lt;/h3&gt;

&lt;p&gt;The biggest risk in cross-posting technical content is search engine cannibalization.&lt;/p&gt;

&lt;p&gt;When search engines like Google crawl identical or substantially similar articles published across multiple domains, they must decide which version to rank. Without explicit guidance, a high-authority domain like Medium or Dev.to will almost always outrank your primary post—robbing your home profile of traffic, backlinks, and domain authority.&lt;/p&gt;

&lt;p&gt;This is where the &lt;strong&gt;canonical URL&lt;/strong&gt; (&lt;code&gt;rel="canonical"&lt;/code&gt;) comes in.&lt;/p&gt;

&lt;p&gt;A canonical URL acts as an authoritative signal telling search engines: &lt;em&gt;“Even though this article appears here, credit the original URL as the definitive source of truth.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Suppose your primary article lives on your preferred canonical source—whether that is your ZyVOP author hub or your own self-hosted domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://zyvop.com/@alex/how-to-build-a-resilient-cli
# (or your custom domain: https://alexdev.io/blog/how-to-build-a-resilient-cli)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When syndicated across other networks, those copies should explicitly identify your primary URL as the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dev.to      → canonical: https://zyvop.com/@alex/how-to-build-a-resilient-cli
Hashnode    → canonical: https://zyvop.com/@alex/how-to-build-a-resilient-cli
Medium      → canonical: https://zyvop.com/@alex/how-to-build-a-resilient-cli
WordPress   → attribution link &amp;amp; canonical header
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNqNkm9rE0EQh7_KOC-Kwl6K4KuggfxpjNBgaLSit76Yu5v0luzthN251Gub7y6XKAgR68tZnmdnfsM8YikV4xA3Xu7LmqLC9Y0NAADj3OJXaSOsomsodrCWNpb8toiXo5ffutuPK1i0BUiEFcckgTzMpCEXXln8bsOvTyDLRk8W112oXEnKsGm9h50ktfgEk9zijPcDlaPzH8Y0t7igVAep-MyZSthzVFCBxaflNVzAri28S3VvznKLS65c25x5qxMF905rKKjcehe2vXOVW_wisVpFTulvsWqKDOrUs4HUNsc9XcBvfZ5bnPiW07Y7z_dv9X0_LSWVSsIfjSeQDcBiZP-upCDBleQtwiAbwfhETJ8lZs8SVyeCVKMrWnUS4BLOWDTYcGzIVTh8RK256Q-p4g21XtGcXm4pOio8p57ZSNA5Nc53OMSMdjvPWeqScmNg0idfUrk-1nMJasDimu-E4fMHiwZupBAVAwv2e1ZXkoFxdOQNJAopSxzdBs2xydo99LO8frP7gYeDweJuKl4iDvHFfe2U8fATbtzzNg%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNqNkm9rE0EQh7_KOC-Kwl6K4KuggfxpjNBgaLSit76Yu5v0luzthN251Gub7y6XKAgR68tZnmdnfsM8YikV4xA3Xu7LmqLC9Y0NAADj3OJXaSOsomsodrCWNpb8toiXo5ffutuPK1i0BUiEFcckgTzMpCEXXln8bsOvTyDLRk8W112oXEnKsGm9h50ktfgEk9zijPcDlaPzH8Y0t7igVAep-MyZSthzVFCBxaflNVzAri28S3VvznKLS65c25x5qxMF905rKKjcehe2vXOVW_wisVpFTulvsWqKDOrUs4HUNsc9XcBvfZ5bnPiW07Y7z_dv9X0_LSWVSsIfjSeQDcBiZP-upCDBleQtwiAbwfhETJ8lZs8SVyeCVKMrWnUS4BLOWDTYcGzIVTh8RK256Q-p4g21XtGcXm4pOio8p57ZSNA5Nc53OMSMdjvPWeqScmNg0idfUrk-1nMJasDimu-E4fMHiwZupBAVAwv2e1ZXkoFxdOQNJAopSxzdBs2xydo99LO8frP7gYeDweJuKl4iDvHFfe2U8fATbtzzNg%3Ftype%3Dpng" alt="Mermaid Diagram" width="623" height="578"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While the concept is simple, the real problem is that &lt;strong&gt;every platform implements canonicalization differently&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dev.to&lt;/strong&gt; supports a &lt;code&gt;canonical_url&lt;/code&gt; property in its Markdown frontmatter and REST API.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hashnode&lt;/strong&gt; handles original article URLs through its GraphQL schema (&lt;code&gt;originalArticleURL&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Medium&lt;/strong&gt; provides a dedicated import API workflow with a &lt;code&gt;canonicalUrl&lt;/code&gt; field, which behaves completely differently from pasting Markdown directly into its web editor.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WordPress&lt;/strong&gt; REST API does not have a native canonical meta field out of the box without third-party plugins (such as Yoast or Rank Math), requiring structured editorial attribution links.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remembering the quirks, API schemas, and formatting rules of each platform for every single article you write is why manual cross-posting quickly becomes unsustainable.&lt;/p&gt;




&lt;h3&gt;
  
  
  Own the source, syndicate the reach
&lt;/h3&gt;

&lt;p&gt;Cross-posting does not mean surrendering ownership of your content. The core principle of a sustainable publishing strategy is simple: &lt;strong&gt;own the primary source of truth, and treat third-party networks purely as distribution channels.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Notice the distinction between the two groups in the diagram above:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Long-form publication platforms&lt;/strong&gt; (Dev.to, Hashnode, Medium, WordPress) host a syndicated copy of the article itself. Their job is to bring organic community readership and pass SEO credit back to your primary home via canonical tags.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Social and micro-blogging platforms&lt;/strong&gt; (Bluesky, Mastodon) are broadcast channels. Their job is to share a concise summary, key takeaways, and a trackable link directing readers back to your primary article.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You should never have to choose between building your own developer presence and publishing where developers already spend their time.&lt;/p&gt;




&lt;h3&gt;
  
  
  The hidden headaches of manual syndication
&lt;/h3&gt;

&lt;p&gt;Publishing one article to one extra platform once a month is manageable. Doing it every week across six destinations is painful.&lt;/p&gt;

&lt;p&gt;Beyond canonical links, there are technical friction points that break down when moving between platforms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Code formatting and syntax highlighting:&lt;/strong&gt; Some editors strip language tags, fail to parse multi-line comments, or wrap code poorly on mobile viewports.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mermaid diagrams and LaTeX math:&lt;/strong&gt; If your article includes architectural flowcharts (&lt;code&gt;mermaid&lt;/code&gt;) or mathematical formulas (&lt;code&gt;LaTeX&lt;/code&gt;), pasting them into platforms like Medium or WordPress breaks them entirely because those platforms don't render them natively.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Asset management:&lt;/strong&gt; Images hosted on ephemeral CDNs or temporary local paths can break when exported across platforms.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Metadata and SEO tags:&lt;/strong&gt; Manually writing custom meta descriptions, OpenGraph tags, and social cards for each platform consumes time that should be spent writing.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these tasks make the writing better. They are mechanical plumbing problems.&lt;/p&gt;




&lt;h3&gt;
  
  
  Where automation makes sense: How ZyVOP solves this
&lt;/h3&gt;

&lt;p&gt;Cross-posting is an ideal candidate for automation because the creative work is already done. You researched the topic, wrote the code, drafted the post, and edited it. At that stage, everything remaining is purely programmatic: formatting content, making API calls, attaching canonical URLs, and handling platform differences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That is the exact workflow ZyVOP was built to handle.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ZyVOP operates as a &lt;strong&gt;Developer Publishing Hub&lt;/strong&gt;: you write your article once, and syndicate it simultaneously across supported developer networks and social channels in a single step.&lt;/p&gt;

&lt;p&gt;Instead of managing separate routines for &lt;strong&gt;Dev.to, Hashnode, Medium, WordPress, Bluesky, and Mastodon&lt;/strong&gt;, ZyVOP’s syndication engine orchestrates the platform-specific heavy lifting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automated canonical tagging:&lt;/strong&gt; Every syndicated copy on Dev.to, Hashnode, and Medium automatically points back to your primary article URL as the canonical source.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Support for custom personal domains:&lt;/strong&gt; If you already run your own independent blog (&lt;code&gt;yourdomain.com&lt;/code&gt;), you can specify your custom URL in ZyVOP's SEO settings. ZyVOP will syndicate the article across all external platforms while ensuring 100% of the canonical authority points back to your personal domain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Diagram and code transformation:&lt;/strong&gt; For destinations like Medium and WordPress that lack native Mermaid support, ZyVOP converts your Mermaid diagram blocks into rendered SVG embeds under the hood, ensuring your system designs never break.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UTM campaign tagging:&lt;/strong&gt; Every outbound link and backlink is automatically tagged with clean UTM parameters, so you can track where your incoming traffic originates.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Your writing should be portable too (Automatic GitHub Sync)
&lt;/h3&gt;

&lt;p&gt;Distribution is only half the battle. The other half is ensuring your writing isn't trapped inside a proprietary database.&lt;/p&gt;

&lt;p&gt;Developers spend years accumulating technical tutorials, architectural post-mortems, and debugging notes. If a platform shuts down, changes its monetization model, or introduces aggressive paywalls, recovering your content can be a nightmare.&lt;/p&gt;

&lt;p&gt;ZyVOP eliminates vendor lock-in with &lt;strong&gt;Automatic GitHub Markdown Sync&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every time you publish or update an article on ZyVOP, the system automatically commits a clean, portable &lt;code&gt;.md&lt;/code&gt; file with standard YAML frontmatter directly to your connected &lt;strong&gt;personal GitHub repository&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cross-Posting&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Developers:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;How&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Reach&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;More&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Readers&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Without&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Losing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Your&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;SEO"&lt;/span&gt;
&lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-09-08T03:15:00.066Z"&lt;/span&gt;
&lt;span class="na"&gt;canonical_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://zyvop.com/@sanjay687/cross-posting-for-developers-how-to-reach-more-readers-without-losing-your-seo-csyc6"&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cross-posting"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seo"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;developer-blogging"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-distribution"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="s"&gt;Your clean Markdown content lives here alongside your code...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For developers, this workflow feels natural:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Your articles live in Git alongside your software projects.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You maintain full commit history, diffs, and offline access.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can migrate your entire library anywhere at any time with zero friction.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Beyond Google: Preparing for AI Answer Engines (AEO &amp;amp; GEO)
&lt;/h3&gt;

&lt;p&gt;Search is evolving rapidly. Developers don't just rely on traditional Google keyword search anymore—they discover technical solutions through AI answer engines like Perplexity, ChatGPT Search, Claude, and Gemini, as well as community discussions on Discord, Reddit, and Bluesky.&lt;/p&gt;

&lt;p&gt;This requires optimizing for both &lt;strong&gt;SEO (Search Engine Optimization)&lt;/strong&gt; and &lt;strong&gt;AEO / GEO (Answer &amp;amp; Generative Engine Optimization)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Structured Technical Schema:&lt;/strong&gt; ZyVOP injects structured JSON-LD (&lt;code&gt;Article&lt;/code&gt;, &lt;code&gt;TechArticle&lt;/code&gt;, and &lt;code&gt;SpeakableSpecification&lt;/code&gt;) so crawlers and LLM indexers clearly understand the author, programming languages, key concepts, and licensing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Citation-Ready Summaries:&lt;/strong&gt; Built-in AI copilot tooling helps you generate concise TL;DR blocks and key takeaway bullet points that AI agents can cite directly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Machine-Readable Endpoints:&lt;/strong&gt; Content is served with clean semantic markup and dedicated metadata endpoints (&lt;code&gt;/api/geo/posts/...&lt;/code&gt;), making your technical explanations easy for AI search systems to parse and attribute back to your source.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Having a stable, high-performance primary URL means AI search engines have an authoritative source to quote and link back to.&lt;/p&gt;




&lt;h3&gt;
  
  
  Unified telemetry: Tracking impact without twenty tabs
&lt;/h3&gt;

&lt;p&gt;When an article is distributed across multiple platforms, checking its performance usually requires opening half a dozen browser tabs and piecing disparate numbers together.&lt;/p&gt;

&lt;p&gt;ZyVOP consolidates this with &lt;strong&gt;unified cross-platform analytics&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Aggregated views across ZyVOP, Dev.to, and Hashnode in a single dashboard.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;30-second engaged dwell time&lt;/strong&gt;, tracking whether readers actually read your code or bounced immediately.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A real-time &lt;strong&gt;Syndication Multiplier&lt;/strong&gt; showing the exact reach boost gained from cross-posting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Referral source classification breaking down visits from search, aggregators, social networks, and direct links.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Terminal-first publishing: Write in your editor, publish via CLI
&lt;/h3&gt;

&lt;p&gt;You shouldn't be forced into a web browser if you prefer writing in VS Code, Obsidian, or Neovim.&lt;/p&gt;

&lt;p&gt;With ZyVOP’s official NPX CLI, you can publish and trigger multi-platform syndication directly from your terminal or a GitHub Actions CI/CD pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Publish markdown directly from your terminal&lt;/span&gt;
npx zyvop publish ./articles/my-new-post.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write your markdown locally, test your code, and syndicate everywhere in one command.&lt;/p&gt;




&lt;h3&gt;
  
  
  The takeaway
&lt;/h3&gt;

&lt;p&gt;Cross-posting is one of the highest-leverage habits a technical writer can build. Developers already read across Dev.to, Hashnode, Medium, and social channels—limiting your articles to a single platform limits your opportunities to be discovered.&lt;/p&gt;

&lt;p&gt;The mistake is treating every platform as a separate manual chore.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Own the source:&lt;/strong&gt; Establish your primary canonical home (whether on ZyVOP with zero reader paywalls, or on your own personal domain).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Protect your SEO:&lt;/strong&gt; Ensure every cross-posted destination references your canonical URL to avoid search engine cannibalization.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep an independent archive:&lt;/strong&gt; Automatically sync clean Markdown copies to your personal GitHub repository.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automate the repetitive plumbing:&lt;/strong&gt; Let software handle format transformations, diagram embeds, social teasers, and multi-platform analytics.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You already did the hard work by writing the code and crafting the explanation. The publishing workflow shouldn't make you do it all over again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write once, publish everywhere, own your source.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://zyvop.com/write" rel="noopener noreferrer"&gt;&lt;strong&gt;Start writing on ZyVOP&lt;/strong&gt;&lt;/a&gt; — 100% free, zero reader paywalls, with automated multi-platform syndication and Git backup.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/cross-posting-for-developers-how-to-reach-more-readers-without-losing-your-seo-csyc6?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication-footer" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>crossposting</category>
      <category>seo</category>
      <category>developerblogging</category>
      <category>contentdistribution</category>
    </item>
    <item>
      <title>Postgres vs MySQL vs MongoDB: The 2026 Decision Guide</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Wed, 09 Sep 2026 03:58:45 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/postgres-vs-mysql-vs-mongodb-the-2026-decision-guide-1p7h</link>
      <guid>https://dev.to/sanjay_singh_1/postgres-vs-mysql-vs-mongodb-the-2026-decision-guide-1p7h</guid>
      <description>&lt;p&gt;PostgreSQL just posted its biggest DB-Engines score gain on record. MySQL still outranks it on raw popularity. MongoDB makes you read a license clause before you build a business on top of it. None of that tells you which one to actually use.&lt;/p&gt;

&lt;p&gt;This guide skips the vendor slides. It's built on where these three databases actually stand in 2026: current versions, real benchmark numbers, real production deployments, and the tradeoffs engineers hit once traffic shows up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&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;PostgreSQL&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;th&gt;MongoDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Relational, extensible&lt;/td&gt;
&lt;td&gt;Relational&lt;/td&gt;
&lt;td&gt;Document (NoSQL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current line&lt;/td&gt;
&lt;td&gt;18.6 (major 18 shipped Sept 2025; 19 in beta)&lt;/td&gt;
&lt;td&gt;9.7 LTS and 8.4 LTS; 8.0 hit end of life in April 2026&lt;/td&gt;
&lt;td&gt;8.0 major, supported through Oct 2029; 8.3 is the latest rapid release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;PostgreSQL License — permissive, BSD-style&lt;/td&gt;
&lt;td&gt;GPLv2 Community, commercial Enterprise from Oracle&lt;/td&gt;
&lt;td&gt;SSPL — source-available, not OSI-approved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transactions&lt;/td&gt;
&lt;td&gt;Full ACID since day one&lt;/td&gt;
&lt;td&gt;Full ACID via InnoDB&lt;/td&gt;
&lt;td&gt;Multi-document ACID since 4.0 (2018), with limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Horizontal scaling&lt;/td&gt;
&lt;td&gt;Needs an extension (Citus) or app-level sharding&lt;/td&gt;
&lt;td&gt;Needs a layer like Vitess&lt;/td&gt;
&lt;td&gt;Built into the core product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Known for&lt;/td&gt;
&lt;td&gt;JSONB, extensions, SQL depth&lt;/td&gt;
&lt;td&gt;Simplicity, huge hosting ecosystem&lt;/td&gt;
&lt;td&gt;Flexible schema, native sharding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs at&lt;/td&gt;
&lt;td&gt;Instagram, Uber (partially), Reddit, Apple, Spotify, Coinbase&lt;/td&gt;
&lt;td&gt;Meta, YouTube (via Vitess), Booking.com, most of WordPress&lt;/td&gt;
&lt;td&gt;Wells Fargo card platforms, CSX rail operations, Electronic Arts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you only read one line: Postgres is the safe default for almost anything you'll build in 2026. MySQL earns its keep when you're already deep in its ecosystem. MongoDB wins when your data genuinely doesn't want to be rows and columns.&lt;/p&gt;

&lt;p&gt;Here's the reasoning behind that table.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each one actually is
&lt;/h2&gt;

&lt;h3&gt;
  
  
  PostgreSQL
&lt;/h3&gt;

&lt;p&gt;Postgres is a relational database that got extensible instead of staying simple. It ships with a real type system (arrays, ranges, native JSON), full ACID transactions, and a plugin architecture that lets you bolt on capabilities most teams would otherwise buy as separate products.&lt;/p&gt;

&lt;p&gt;That's not a small deal. &lt;a href="https://en.wikipedia.org/wiki/PostGIS" rel="noopener noreferrer"&gt;PostGIS&lt;/a&gt; has handled geospatial queries in production since 2001. &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt; turned it into a viable vector database for AI apps. TimescaleDB turned it into a time-series engine. You add an extension, not a new system to operate.&lt;/p&gt;

&lt;h3&gt;
  
  
  MySQL
&lt;/h3&gt;

&lt;p&gt;MySQL is the database that shipped with every LAMP-stack tutorial you've ever read. It's simpler than Postgres by design, uses a pluggable storage engine architecture (InnoDB by default since 5.5), and optimizes hard for the common case: read-heavy web and content workloads at scale.&lt;/p&gt;

&lt;p&gt;It's owned by Oracle, which unsettles some teams and doesn't bother others. WordPress alone runs on more than 40% of all websites on the internet, and closer to 60% of sites that run any CMS at all, and it keeps MySQL relevant regardless of anyone's opinion on Oracle.&lt;/p&gt;

&lt;h3&gt;
  
  
  MongoDB
&lt;/h3&gt;

&lt;p&gt;MongoDB stores documents (BSON, a binary form of JSON) instead of rows. There's no fixed schema to negotiate before you ship, no joins to write, and horizontal scaling (sharding) is a first-class feature instead of a bolt-on.&lt;/p&gt;

&lt;p&gt;The tradeoff is consistency guarantees that took years to catch up to relational databases, and a license (more on that below) that matters if you're building a hosted product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each one stands right now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL 18&lt;/strong&gt; shipped in September 2025 with a new async I/O subsystem that &lt;a href="https://www.postgresql.org/about/news/postgresql-18-released-3142/" rel="noopener noreferrer"&gt;demonstrated up to 3x faster storage reads&lt;/a&gt; in some workloads, plus virtual generated columns, a &lt;code&gt;uuidv7()&lt;/code&gt; function, and OAuth 2.0 support for SSO. The current patch is 18.6, and PostgreSQL 19 is already in beta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL&lt;/strong&gt; renumbered its release train this year to match the calendar. MySQL 26.7 Innovation is the bleeding edge, while &lt;a href="https://blogs.oracle.com/mysql/mysql-9-7-0-lts-is-now-available-expanded-community-capabilities-and-dynamic-data-masking-for-enterprise" rel="noopener noreferrer"&gt;MySQL 9.7&lt;/a&gt; became the newest LTS line in April 2026, adding a Hypergraph optimizer, dynamic data masking, and in-database JavaScript. MySQL 8.4 LTS is still fully supported. MySQL 8.0 reached end of life in April 2026, so if you're still on it, that's your actual priority this quarter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MongoDB 8.0&lt;/strong&gt; is the current major release, &lt;a href="https://www.mongodb.com/docs/manual/release-notes/8.0" rel="noopener noreferrer"&gt;supported through October 2029&lt;/a&gt;. MongoDB's own benchmarks claim up to 36% better read throughput and 56% faster bulk writes over 7.0, with the company citing 32% overall gains specifically for typical mixed read/write web application workloads. The &lt;a href="https://www.mongodb.com/products/updates/mongodb-8-3/" rel="noopener noreferrer"&gt;8.3 rapid release&lt;/a&gt; from May 2026 is the current downloadable tip, adding native type coercion in queries and stronger security defaults.&lt;/p&gt;

&lt;h2&gt;
  
  
  How they actually store and query the same data
&lt;/h2&gt;

&lt;p&gt;Same use case across all three: an order with a flexible list of line items. Here's what it looks like in each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt;, structured columns plus a JSONB field for the flexible part:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;line_items&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_line_items&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Every pending order with an electronics item&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line_items&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;line_items&lt;/span&gt; &lt;span class="o"&gt;@&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'[{"category": "electronics"}]'&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;MySQL&lt;/strong&gt;, the same shape with a native &lt;code&gt;JSON&lt;/code&gt; column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;DATETIME&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;InnoDB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;line_items&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;DATETIME&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;InnoDB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Every pending order with an electronics item&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line_items&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;JSON_CONTAINS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{"category": "electronics"}'&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;MongoDB&lt;/strong&gt;, where there's no schema to declare at all:&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="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;65f1a2b3c4d5e6f7a8b9c0d1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lineItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SKU-1029&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;electronics&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;49.99&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lineItems.category&lt;/span&gt;&lt;span class="dl"&gt;"&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="c1"&gt;// Every pending order with an electronics item&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lineItems.category&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;electronics&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;Notice how close the Postgres and MySQL versions are to the MongoDB one. This is the thing that's actually changed since 2015: JSONB and native JSON columns closed most of the "but I need flexible documents" gap that used to send people straight to MongoDB.&lt;/p&gt;

&lt;p&gt;One honest flag on that MySQL query above: &lt;code&gt;JSON_CONTAINS&lt;/code&gt; with no supporting index is exactly what most tutorials show you, and it's the slow way to run this in production. Keep reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  I actually ran this, so you don't have to trust a slide deck
&lt;/h2&gt;

&lt;p&gt;Most comparison posts quote someone else's benchmark. Instead of doing that here, I generated 200,000 identical order records (same JSON shape, same random category distribution, same seed) and loaded the exact same dataset into a real PostgreSQL 16 instance and a real MySQL 8.0.46 instance, then timed the exact containment queries shown above.&lt;/p&gt;

&lt;p&gt;Small disclosure up front: this ran on a single-vCPU, 4GB sandbox VM, not production hardware. Don't treat the millisecond figures as absolute. Treat the &lt;em&gt;relative&lt;/em&gt; story as the finding, because that part holds regardless of the box it runs on.&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;Query&lt;/th&gt;
&lt;th&gt;Result count&lt;/th&gt;
&lt;th&gt;Median latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;WHERE line_items @&amp;gt; '[{"category":...}]', no index&lt;/td&gt;
&lt;td&gt;27,318 / 200,000&lt;/td&gt;
&lt;td&gt;83.6ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;Same query, with a GIN index on line_items&lt;/td&gt;
&lt;td&gt;27,318 / 200,000&lt;/td&gt;
&lt;td&gt;36.3ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;WHERE JSON_CONTAINS(line_items, ...), no index (the query most tutorials show)&lt;/td&gt;
&lt;td&gt;27,318 / 200,000&lt;/td&gt;
&lt;td&gt;225.9ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;Same logic, via a STORED generated boolean column plus a normal index&lt;/td&gt;
&lt;td&gt;27,318 / 200,000&lt;/td&gt;
&lt;td&gt;4.7ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both engines agreed on the row count, which cross-checks that the queries are actually correct and comparing like with like.&lt;/p&gt;

&lt;p&gt;Two things stand out. First, unindexed, Postgres outperformed MySQL by roughly 2.7x on this exact containment pattern. Second, and this is the part most comparison posts skip entirely: once you index MySQL properly with a &lt;code&gt;STORED&lt;/code&gt; generated column, it comes out &lt;strong&gt;faster than Postgres's GIN index&lt;/strong&gt; for this specific pattern, not slower.&lt;/p&gt;

&lt;p&gt;The lesson isn't "MySQL is secretly faster." It's that &lt;code&gt;JSON_CONTAINS&lt;/code&gt; without a matching index, which is what nearly every MySQL JSON tutorial shows you, is leaving most of MySQL's actual performance on the table. Postgres's GIN index is closer to a reasonable default for JSONB; MySQL's fast path requires you to know to build it. That's a real, practical difference in how much JSON expertise each database demands from you, and it's more useful than either database's marketing number.&lt;/p&gt;

&lt;p&gt;The Postgres schema, load, and benchmark script are straightforward to reproduce: create the tables shown earlier, load a comparable dataset, run &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; before and after adding the GIN index, and compare.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the broader benchmark literature shows
&lt;/h2&gt;

&lt;p&gt;Beyond this one query pattern, there's an older but far more detailed benchmark worth knowing, with an upfront caveat: it's dated, and it's worth being precise about what it actually measured rather than repeating the rounded-off version other 2026 blog posts pass around.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://info.enterprisedb.com/rs/069-ALB-339/images/PostgreSQL_MongoDB_Benchmark-WhitepaperFinal.pdf" rel="noopener noreferrer"&gt;EnterpriseDB and OnGres ran a three-part benchmark&lt;/a&gt; in 2019 comparing PostgreSQL 11 against MongoDB 4.0. It's the most detailed public benchmark of these two databases that exists, but both have shipped several major versions since, including MongoDB's own multi-document ACID transactions maturing considerably. Treat what follows as historical signal, not a current-generation verdict.&lt;/p&gt;

&lt;p&gt;The three tests told three different stories. A custom airline-booking transaction benchmark had Postgres processing over 20,000 transactions per second at high concurrency while MongoDB topped out around 1,800, a 4-15x gap depending on isolation level. A separate sysbench OLTP test found Postgres 2.7-3.2x faster when the dataset fit in memory, widening to 25-40x once the dataset grew past what fit in RAM. A third test, querying a year of GitHub Archive data stored as JSON in both engines, was the closest race of the three: Postgres won three of four queries by 22-53%, and lost the fourth by 22%.&lt;/p&gt;

&lt;p&gt;The latency numbers are worth citing precisely instead of loosely, because this is exactly where secondary sources blur things: at 256 concurrent threads in the transaction benchmark, MongoDB's 99th-percentile latency hit 377ms against Postgres's 35ms. Postgres's &lt;em&gt;median&lt;/em&gt; latency, separately, stayed under 10ms across every concurrency level tested. Several 2026 recap posts compress that into "MongoDB's p99 exceeds 250ms while Postgres stays under 10ms," which quietly swaps in Postgres's median for its p99. Both individual numbers are real; the comparison isn't apples to apples.&lt;/p&gt;

&lt;p&gt;The pattern that holds up, from that 2019 data and from the benchmark I ran myself above: &lt;strong&gt;indexing strategy and connection handling change the outcome more than the choice of database does. Postgres tends to win joins, complex queries, and high-concurrency transactions by a wide margin. MongoDB's disadvantage shrinks on workloads that fit entirely in memory and don't need cross-document consistency.&lt;/strong&gt; Benchmark your actual query patterns and your actual indexes before trusting anyone's numbers, including the ones in this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling: replicas, sharding, and what's actually built in
&lt;/h2&gt;

&lt;p&gt;PostgreSQL scales vertically first, then through streaming read replicas. Getting real horizontal write scaling means adding &lt;a href="https://www.citusdata.com/" rel="noopener noreferrer"&gt;Citus&lt;/a&gt; or a similar extension, or building sharding logic into your app.&lt;/p&gt;

&lt;p&gt;MySQL scales the same way at the core, but has more mature tooling around it. Read replicas are simple and battle-tested. For write-side horizontal scale, most large deployments reach for &lt;a href="https://vitess.io" rel="noopener noreferrer"&gt;Vitess&lt;/a&gt;, the sharding layer originally built at YouTube and now a CNCF graduated project, which is how YouTube reportedly serves billions of users on top of a MySQL foundation.&lt;/p&gt;

&lt;p&gt;MongoDB is the only one of the three with sharding built into the core product from day one. You define a shard key, and the database handles distributing data across nodes plus routing queries to the right shard. Replica sets give you automatic failover without extra tooling. This is MongoDB's clearest structural advantage, and it's a real one if you actually need to scale writes past a single machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI angle: vector search in 2026
&lt;/h2&gt;

&lt;p&gt;All three databases now do vector search, which matters if you're building RAG pipelines or semantic search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Postgres&lt;/strong&gt; has &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt;, and the newer pgvectorscale extension adds a DiskANN-based index that claims up to 28x lower latency at a fraction of the cost of a dedicated vector database. You get vector search, full-text search, and your relational data in one system with one backup process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL 9.0&lt;/strong&gt; added a native &lt;code&gt;VECTOR&lt;/code&gt; data type for storing and querying embeddings directly in columns, aimed at teams that don't want a separate vector store bolted onto an existing MySQL-based app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MongoDB&lt;/strong&gt; ships Atlas Vector Search as a managed feature, and it's become a real differentiator for AI-native startups. MongoDB's own customer write-ups point to companies like &lt;a href="https://www.mongodb.com/company/blog/innovation/innovating-with-mongodb-customer-successes-april-2026" rel="noopener noreferrer"&gt;DevRev&lt;/a&gt; building agent memory systems on top of it, citing faster iteration versus keeping vectors and metadata in separate systems.&lt;/p&gt;

&lt;p&gt;If you're already on Postgres, pgvector is very likely good enough and saves you an entire extra system. If you're greenfield and fully document-native, Atlas Vector Search removes a decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Licensing: the part nobody reads until it matters
&lt;/h2&gt;

&lt;p&gt;This one's boring until it isn't, and then it costs you weeks of legal review.&lt;/p&gt;

&lt;p&gt;PostgreSQL uses the &lt;strong&gt;PostgreSQL License&lt;/strong&gt;, a permissive BSD-style license with no strings attached. No company controls it. Nobody can change the terms out from under you.&lt;/p&gt;

&lt;p&gt;MySQL Community Edition is &lt;strong&gt;GPLv2&lt;/strong&gt;, and Oracle also sells commercial Enterprise editions with extra features like the dynamic data masking mentioned above. Fine for almost everyone; worth a glance if you're embedding MySQL inside a product you resell.&lt;/p&gt;

&lt;p&gt;MongoDB Community Server has shipped under the &lt;strong&gt;Server Side Public License (SSPL)&lt;/strong&gt; since 2018, replacing the AGPL it used before. The &lt;a href="https://en.wikipedia.org/wiki/Server_Side_Public_License" rel="noopener noreferrer"&gt;Open Source Initiative has never approved SSPL&lt;/a&gt; as an open-source license, because it requires anyone who offers MongoDB "as a service" to a third party to open-source their entire surrounding application stack, not just their changes to MongoDB itself. Most companies never trigger that clause. If you're building a SaaS product that exposes database functionality to customers, &lt;a href="https://www.mongodb.com/legal/licensing/server-side-public-license/faq" rel="noopener noreferrer"&gt;read the actual FAQ&lt;/a&gt; before you architect around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who actually runs what
&lt;/h2&gt;

&lt;p&gt;Real deployments tell you more than feature lists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt;: Instagram runs one of the largest known Postgres deployments in the world. Coinbase uses it for account and transaction data where ACID guarantees aren't optional. Spotify, Reddit, and Apple all run it at scale. Multiple 2026 technology-adoption trackers put production Postgres deployments in the tens of thousands of companies, though their crawl methodologies aren't independently auditable, so treat the scale as directional rather than exact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL&lt;/strong&gt;: Meta has run one of the largest MySQL fleets on earth for years, going as far as co-developing &lt;a href="https://en.wikipedia.org/wiki/WebScaleSQL" rel="noopener noreferrer"&gt;WebScaleSQL&lt;/a&gt; with Google, LinkedIn, and Alibaba to solve shared scaling problems. YouTube's backend runs on MySQL through Vitess. Booking.com and most of the WordPress-powered internet run on it too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MongoDB&lt;/strong&gt;: &lt;a href="https://www.mongodb.com/solutions/customer-case-studies/wells-fargo" rel="noopener noreferrer"&gt;Wells Fargo's own case study&lt;/a&gt; describes building an operational data store on MongoDB for its Cards 2.0 initiative, now handling more than seven million transactions with sub-second response and serving 40% of external vendor traffic. &lt;a href="https://www.mongodb.com/company/blog/innovation/innovating-customer-successes-october-2025" rel="noopener noreferrer"&gt;CSX&lt;/a&gt;, the US railroad, migrated its real-time operations platform onto MongoDB Atlas to keep it running 24/7 through the cutover. Electronic Arts has used it since its FIFA Online 3 days to scale a multiplayer title to millions of concurrent players, per &lt;a href="https://www.mongodb.com/company/blog/tags/case-study" rel="noopener noreferrer"&gt;MongoDB's own case-study archive&lt;/a&gt;, though that specific example is now over a decade old.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Uber story, and why it's more nuanced than the headline
&lt;/h2&gt;

&lt;p&gt;If you've been in backend engineering for more than a year, you've seen the 2016 &lt;a href="https://www.uber.com/us/en/blog/postgres-to-mysql-migration/" rel="noopener noreferrer"&gt;Uber engineering post&lt;/a&gt; titled "Why Uber Engineering Switched from Postgres to MySQL." It's still one of the most-cited database posts ever written, and it's worth understanding accurately instead of just as ammunition.&lt;/p&gt;

&lt;p&gt;Uber's actual complaints were specific: Postgres's process-per-connection model cost more memory than MySQL's thread-per-connection model at their connection counts, secondary indexes pointed to physical row locations that changed on every update (write amplification), and replication was verbose across data centers. They built a MySQL-based sharding layer called Schemaless to solve it.&lt;/p&gt;

&lt;p&gt;That post is from 2016, describing Postgres 9.3-era behavior. PostgreSQL 18's async I/O subsystem and a decade of replication improvements have closed a meaningful part of that gap. Old benchmarks describing old versions don't transfer cleanly to 2026 decisions, in either direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use case matrix: what to actually pick
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fintech, payments, anything with money.&lt;/strong&gt; PostgreSQL. Full ACID transactions with no practical limits on complexity or duration, decades of correctness under real audits, and PostGIS if you need geofencing on top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Early-stage startup, schema still moving fast.&lt;/strong&gt; Either PostgreSQL with JSONB columns for the unstable parts, or MongoDB if your entire data model is naturally document-shaped. Prototype both against your real access patterns before committing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content management, blogs, anything WordPress-adjacent.&lt;/strong&gt; MySQL. The hosting ecosystem, tutorials, and plugin compatibility make it the path of least resistance, and read-heavy CMS traffic is exactly what it's tuned for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product catalogs and content with wildly variable attributes.&lt;/strong&gt; MongoDB. A shoe and a laptop have almost nothing in common as database rows, but they're both natural documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analytics-adjacent application logic.&lt;/strong&gt; PostgreSQL. Window functions, CTEs, and a query planner that handles complexity neither of the other two matches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RAG pipelines and AI agent memory.&lt;/strong&gt; PostgreSQL with pgvector if you already run Postgres. MongoDB Atlas Vector Search if you're document-native from the start and want vectors plus flexible metadata in one query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IoT, event streams, logs with unpredictable shape.&lt;/strong&gt; MongoDB, or Postgres with TimescaleDB if the data is fundamentally time-series with a known schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You genuinely need to shard writes across hundreds of nodes today, not eventually.&lt;/strong&gt; MongoDB's native sharding beats bolting Citus or Vitess onto something else, unless you already have that infrastructure running.&lt;/p&gt;

&lt;h2&gt;
  
  
  A decision flow
&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNpVUl1v2zAM_CucnmUMA_YUDB2apF9AU6zzNmBI80DLjKVVFg2RSesG-e-DnWbwHsUj7ninOxjHNZmZ2UZ-cR6zwo_lUwIAuFw_0At0mf-QUwvOM0tIDSDUqFih0AaK4gLmhyWTgHoaAfC4J8iEETJF1MBJfOjkS5U_XmCqIRHVIJqDU3CcJIhScv3X40l2DkUBv0lG7sXhYdjek1POIITZeQsNsXSoAaMdWTmDhpYKoRxIoNqFqBDSmXExZVyuv7Fok6l8vN9M8Ace4avDZcyEdQ81UQchAcKqLx_vi8FwPcqJonu2sFiVFjiDZ9EhFyHddWfNq6nm9Xqk2Eygd7nlxPP76OZQOk8tgvOYGpIxI8Wksf_ndsw5CCTUXcYYe0gkSjXU7HYtJZXzHTfTO25PaSbUsCfwnMMbJ8V4cuUx14MP3inwdvzPil_PRLdTorv1ilPDy_lmAv5v6WY6Mta0lFsMtZkdjHpqh8rVtMVdVGNPk1-YA1aRZNjZctJrbEPszcwU2HWRCulFqbUwjyE9r9CV4_uak1p4MiU1TPDz7slY-M4VK1u4pbgnDQ4tXOahLiCYZOzJ1thRpAxvwy2fPnev5ni0pmoWHDmbmfnw4oOSOf4FdOwB6Q%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNpVUl1v2zAM_CucnmUMA_YUDB2apF9AU6zzNmBI80DLjKVVFg2RSesG-e-DnWbwHsUj7ninOxjHNZmZ2UZ-cR6zwo_lUwIAuFw_0At0mf-QUwvOM0tIDSDUqFih0AaK4gLmhyWTgHoaAfC4J8iEETJF1MBJfOjkS5U_XmCqIRHVIJqDU3CcJIhScv3X40l2DkUBv0lG7sXhYdjek1POIITZeQsNsXSoAaMdWTmDhpYKoRxIoNqFqBDSmXExZVyuv7Fok6l8vN9M8Ace4avDZcyEdQ81UQchAcKqLx_vi8FwPcqJonu2sFiVFjiDZ9EhFyHddWfNq6nm9Xqk2Eygd7nlxPP76OZQOk8tgvOYGpIxI8Wksf_ndsw5CCTUXcYYe0gkSjXU7HYtJZXzHTfTO25PaSbUsCfwnMMbJ8V4cuUx14MP3inwdvzPil_PRLdTorv1ilPDy_lmAv5v6WY6Mta0lFsMtZkdjHpqh8rVtMVdVGNPk1-YA1aRZNjZctJrbEPszcwU2HWRCulFqbUwjyE9r9CV4_uak1p4MiU1TPDz7slY-M4VK1u4pbgnDQ4tXOahLiCYZOzJ1thRpAxvwy2fPnev5ni0pmoWHDmbmfnw4oOSOf4FdOwB6Q%3Ftype%3Dpng" alt="Mermaid Diagram" width="814" height="1281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is PostgreSQL faster than MongoDB?&lt;/strong&gt; Neither wins outright. Postgres tends to win joins, complex queries, and high-concurrency transactions. MongoDB tends to win simple point lookups and raw insert throughput on unstructured data. Test your actual query patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is MySQL dead in 2026?&lt;/strong&gt; No. &lt;a href="https://www.red-gate.com/our-company/newsroom/press-releases/db-engines-postgresql-leads-h1-2026-database-growth-as-data-platforms-gain-momentum/" rel="noopener noreferrer"&gt;DB-Engines' own H1 2026 report&lt;/a&gt; shows Postgres posted the largest popularity-score gain of any database that half-year, with MongoDB not far behind in third place. But that's a momentum metric. By DB-Engines' absolute score, which weighs years of accumulated job postings, forum mentions, and search volume, MySQL still sits ahead of Postgres in the overall rankings. MySQL just got a new LTS line (9.7) with real enterprise features too. Read: not the default pick for new greenfield projects anymore, but nowhere close to going away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can PostgreSQL replace MongoDB entirely?&lt;/strong&gt; For most workloads, yes. JSONB with GIN indexes covers a large share of what used to require MongoDB, while keeping full SQL and ACID transactions. The exceptions are workloads that need MongoDB's native sharding at massive scale, or teams that are fully committed to a document-first architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should a startup default to PostgreSQL or MongoDB in 2026?&lt;/strong&gt; PostgreSQL, unless your data model is unambiguously document-shaped from day one. It handles more future scenarios (analytics, AI, geospatial, strict consistency) without adding a second database later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is MongoDB safe to use commercially?&lt;/strong&gt; Yes, for running it yourself. The SSPL only creates obligations if you offer MongoDB's functionality as a hosted service to third parties. Read the actual license text if that describes your product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest verdict
&lt;/h2&gt;

&lt;p&gt;Postgres earned its 2025-2026 momentum the hard way: by quietly closing the gaps that used to send people to specialized databases, one extension at a time. It's the right first choice for most new projects in 2026, and the &lt;a href="https://survey.stackoverflow.co/2025/technology" rel="noopener noreferrer"&gt;Stack Overflow 2025 Developer Survey&lt;/a&gt; usage numbers back that up.&lt;/p&gt;

&lt;p&gt;MySQL isn't losing because it got worse. It's losing mindshare because it stayed simple while Postgres got more capable, and simple stops being the deciding factor once teams need more than a fast key-value store with SQL on top.&lt;/p&gt;

&lt;p&gt;MongoDB's document model still solves a real problem: data that doesn't want a fixed shape. Pick it when that's actually true for you, not because a tutorial from 2018 said schemaless was the future.&lt;/p&gt;

&lt;p&gt;Run the workload. Check the license. Read the actual docs instead of the comparison articles, including this one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/postgres-vs-mysql-vs-mongodb-the-2026-decision-guide-78cm3?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication-footer" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>mysql</category>
      <category>mongodb</category>
      <category>databasecomparison</category>
    </item>
    <item>
      <title>The Sandbox Held. The Headline Didn't.</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Mon, 07 Sep 2026 05:32:04 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/the-sandbox-held-the-headline-didnt-5bdl</link>
      <guid>https://dev.to/sanjay_singh_1/the-sandbox-held-the-headline-didnt-5bdl</guid>
      <description>&lt;p&gt;On September 5, 2026, a post titled "Actively exploited sandbox RCE in all Chromium versions" sat near the top of Hacker News. By the time it had been reblogged twice, "sandbox RCE" had quietly become "an attacker can take over your machine by opening a tab," and "all Chromium versions" had become "there is no fixed version yet."&lt;/p&gt;

&lt;p&gt;Neither is true. The gap between the headline and the advisory is a better story than the panic version, mostly because closing it means understanding how Chromium is actually built.&lt;/p&gt;

&lt;p&gt;The bug underneath the headline is real, serious, and was still being weaponized against real targets as this was written. It's called CVE-2026-85046, it lives in V8, and Google shipped the fix two days before this piece went up.&lt;/p&gt;

&lt;p&gt;What it is not, on its own, is a sandbox escape. That distinction is the whole point of this piece: not to downplay a genuinely dangerous vulnerability class, but to explain what Chromium's architecture actually bought its two billion or so users while the bug was live.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CVE-2026-85046 actually is
&lt;/h2&gt;

&lt;p&gt;Google patched the flaw in Chrome 152.0.7977.82/.83 (Windows/macOS) and 152.0.7977.82 (Linux) on September 3, 2026. It's a type-confusion bug in Maglev, V8's mid-tier JIT compiler, rated CVSS 8.8.&lt;/p&gt;

&lt;p&gt;The U.S. Cybersecurity and Infrastructure Security Agency added it to the Known Exploited Vulnerabilities catalog the next day, giving federal civilian agencies until September 18 to remediate under Binding Operational Directive 22-01. That deadline binds federal agencies specifically, not the rest of us, but it's still the industry's highest-confidence public signal that a bug is actually being used against real targets.&lt;/p&gt;

&lt;p&gt;Security researcher Salvatore Gulizia, who goes by "Serotav," reported the bug to Google on August 4, 2026, and was paid a $1,000 bounty. He published a full technical writeup on his own blog on August 28, before Chrome 152 had even shipped, laying out how he'd chained the bug with a separate, already-known sandbox-escape vulnerability to enter Google's v8CTF competition.&lt;/p&gt;

&lt;p&gt;That detail matters more than it looks. It's Gulizia's own account of how he built a full exploit chain, and it undercuts the "sandbox RCE" framing on its own terms: that framing implies the bug itself breaks the sandbox. It doesn't. What it gets you is arbitrary read/write inside the sandboxed renderer process, which is bad, but a different and much more contained thing.&lt;/p&gt;

&lt;p&gt;Google's standard Vulnerability Reward Program pays far more for renderer remote-code-execution reports, which makes the $1,000 bounty worth a second look. The V8 zero-day patched three months earlier, CVE-2026-11645, earned its reporter $55,000 for what was, on paper, a comparable class of bug.&lt;/p&gt;

&lt;p&gt;The gap isn't about severity. Gulizia reported through v8CTF, Google's standing capture-the-flag program, which pays a fixed prize regardless of impact, rather than through the standard VRP pipeline that scales rewards to real-world damage. Same bug class, wildly different payout, simply because of which door it walked in through.&lt;/p&gt;

&lt;p&gt;Because V8 is shared code across the whole Chromium project, the bug was never really Chrome-specific. Microsoft Edge, Brave, Opera, and Vivaldi all ship the same vulnerable V8 code, and each needs its own vendor update on its own schedule. That's the part of "all Chromium versions" that's actually true, and actually underreported: not that Chrome itself remains unpatched, but that patching Chrome does nothing for the other four browsers sitting in the same Applications folder.&lt;/p&gt;

&lt;p&gt;Here's how the clock actually ran:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fma4wef9krcr9q6opk873.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fma4wef9krcr9q6opk873.webp" alt="chart disclosure timeline iynwseza" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gulizia's technical writeup, which laid out the exploitation approach without being a copy-paste weapon, went public on August 28, six days before Chrome 152 shipped. The disclosure-to-patch window was already short; the disclosure-to-public-writeup window was shorter still.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chromium's actual defense-in-depth
&lt;/h2&gt;

&lt;p&gt;Most coverage of "sandbox RCE" skips the part where Chromium isn't one program with one set of privileges. It's a federation of OS processes with deliberately unequal trust, wired together over an internal IPC layer called Mojo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41okhcchgrc7n87ttlbc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41okhcchgrc7n87ttlbc.webp" alt="Diagram sandbox architecture" width="800" height="573"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three things in that diagram do the actual work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Site Isolation&lt;/strong&gt; puts each origin in its own OS-level renderer process, so a bug triggered by &lt;code&gt;evil.example&lt;/code&gt; can't directly read memory belonging to your bank's tab in the next process over. They're not just logically separate; they're physically separate address spaces.&lt;/p&gt;

&lt;p&gt;It wasn't originally conceived as a browser-exploitation defense at all. Google fast-tracked its rollout in 2018 as a direct response to Spectre, the speculative-execution hardware flaw that meant any JavaScript sharing a process with sensitive data could potentially read it through a timing side channel.&lt;/p&gt;

&lt;p&gt;A fix built for a CPU-level information leak ended up doubling as one of the sturdiest walls against process-level exploitation. Defense-in-depth built for one threat often pays off against a completely different one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The renderer OS sandbox&lt;/strong&gt; strips the renderer process of almost all direct syscall access: no arbitrary file opens, no raw sockets, no spawning processes. Everything it needs, like rendering a page or fetching a resource, goes through Mojo IPC to the privileged Browser process, which can apply its own policy checks on every request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The V8 Sandbox&lt;/strong&gt; is the newest and narrowest layer, and it's the one CVE-2026-85046 actually lives inside. It's an in-process mitigation: heap pointers inside V8 are represented in a way that keeps a corrupted pointer from forging an arbitrary address into the rest of the process.&lt;/p&gt;

&lt;p&gt;Samuel Groß, who leads V8 security at Google, has made the underlying argument plainly: V8's worst bugs are mostly subtle compiler logic errors rather than classic memory-safety bugs, which is exactly why ordinary memory-safety fixes don't reach them. He's called the V8 Sandbox "a necessary step towards memory safety" for that reason. It shipped as the default in Chrome 123, in early 2024.&lt;/p&gt;

&lt;p&gt;Put together: reaching the Browser process, and from there the OS, means beating Site Isolation (or already being on the same origin), beating the renderer's OS sandbox, and beating the V8 Sandbox — or finding one bug that skips past all three at once, which is a different and much harder thing than what Gulizia found. CVE-2026-85046 broke the third layer only. That's a real result. It is not sandbox RCE.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the bug lived, and how it worked
&lt;/h2&gt;

&lt;p&gt;To see why a bug like this keeps recurring, it helps to look at where in V8 it actually sits. V8 doesn't run JavaScript one way; it pushes code through increasingly aggressive tiers, promoting "hot" functions to faster, more speculative compilers as it learns more about the types actually flowing through them:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fomh95ykkayl7x0o3b87e.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fomh95ykkayl7x0o3b87e.webp" alt="Diagram v8 mechanic" width="800" height="627"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Maglev, added to V8 in 2023, sits between the baseline Sparkplug JIT and the deeply optimizing TurboFan. One of its optimizations, &lt;code&gt;TryReduceArrayPrototypeSort&lt;/code&gt;, replaces calls to &lt;code&gt;Array.prototype.sort&lt;/code&gt; on small arrays (16 elements or fewer) with a much cheaper inline insertion sort, but only once it has observed, through type feedback, that a call site has been trained on both of V8's "packed" array representations: &lt;code&gt;PACKED_SMI_ELEMENTS&lt;/code&gt; for small integers and &lt;code&gt;PACKED_ELEMENTS&lt;/code&gt; for arbitrary objects.&lt;/p&gt;

&lt;p&gt;Because a JavaScript comparator function can have side effects, the inlined sort can't safely sort in place. It copies the array's contents into a temporary buffer, sorts the copy, and then re-checks the original array's map before writing the sorted values back, since the comparator might have mutated the array in the meantime.&lt;/p&gt;

&lt;p&gt;Gulizia's writeup, linked at the end of this piece, shows exactly what that check verifies: that the array's current map is present somewhere in the set of maps the call site was trained on, not that it's the same map the array had when sorting began.&lt;/p&gt;

&lt;p&gt;That's the whole bug. &lt;code&gt;Array.prototype.fill()&lt;/code&gt;, when it replaces every element of an array, is allowed to migrate the array's map backward, from &lt;code&gt;PACKED_ELEMENTS&lt;/code&gt; to &lt;code&gt;PACKED_SMI_ELEMENTS&lt;/code&gt;, without reallocating the backing store, since both representations are, under the hood, a plain &lt;code&gt;FixedArray&lt;/code&gt; of tagged pointer-sized slots.&lt;/p&gt;

&lt;p&gt;Call &lt;code&gt;fill()&lt;/code&gt; inside the comparator, and the trained-maps check passes even though the array now claims to hold raw integers over memory that still holds real object pointers. From there it's standard V8-exploitation technique: reading the confused slots leaks heap addresses, an "addrof" primitive, and writing through them, using an operation like &lt;code&gt;Array.prototype.unshift()&lt;/code&gt; that shifts elements without triggering V8's garbage-collector write barrier, turns into a fake-object primitive.&lt;/p&gt;

&lt;p&gt;Chain the two together and you have arbitrary read/write across the JavaScript heap: still inside the V8 Sandbox, still inside the renderer's OS sandbox, and still, on its own, nowhere near your filesystem.&lt;/p&gt;

&lt;p&gt;It's worth being precise about what the V8 Sandbox does and doesn't stop here. It doesn't prevent type confusion inside the JS heap; it can't, since that confusion happens entirely within V8's own bookkeeping, before any pointer goes anywhere.&lt;/p&gt;

&lt;p&gt;What it prevents is that confusion turning into a pointer that reaches outside the sandboxed heap region into the rest of the renderer process's memory. Gulizia's addrof and fakeobj primitives work on sandboxed, compressed heap references: powerful within that boundary, useless outside it without a second bug that itself breaks out of the Sandbox. That's the layer CVE-2026-85046 broke, and the layer it didn't.&lt;/p&gt;

&lt;p&gt;The fix Google shipped is narrow and fairly unglamorous: Maglev simply stops inlining &lt;code&gt;Array.prototype.sort&lt;/code&gt; once a call site has been trained on mixed element kinds. No architectural change was needed here. The architecture wasn't what failed; one optimization's bookkeeping was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why V8 keeps producing this exact bug class
&lt;/h2&gt;

&lt;p&gt;CVE-2026-85046 was the third V8 zero-day patched in Chrome in 2026, out of six actively exploited zero-days total:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmmw5fsoy0udnf8l78vkk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmmw5fsoy0udnf8l78vkk.webp" alt="Chart zeroday components" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every one of 2026's six actively-exploited Chrome zero-days carried an identical CVSS score of 8.8. Half of them lived directly in V8; the other half hit adjacent memory subsystems (Skia's 2D rasterizer, the Dawn WebGPU backend) that feed the same rendering pipeline.&lt;/p&gt;

&lt;p&gt;That's not a coincidence, and it isn't new. Google's own security research has found that roughly sixty percent of Chrome exploits observed in the wild between 2021 and 2023 originated in V8.&lt;/p&gt;

&lt;p&gt;The reason is structural, not a staffing problem. V8's most dangerous bugs are rarely classic memory-unsafety, like a use-after-free or an out-of-bounds write; they're subtle logic errors in speculative compiler optimizations, exactly like the map-check gap here. Conventional memory-safety tooling (bounds checking, Rust rewrites, hardware memory tagging) doesn't reach a bug where every individual memory access is, technically, in-bounds.&lt;/p&gt;

&lt;p&gt;The V8 Sandbox exists because Google's own team decided this bug class can't be eliminated at the source, only contained after the fact.&lt;/p&gt;

&lt;p&gt;This particular shape of bug, a fast-path optimization for a built-in Array method gated on element-kind feedback that a callback can invalidate mid-operation, isn't a one-off.&lt;/p&gt;

&lt;p&gt;Array and TypedArray methods that accept a user callback (&lt;code&gt;sort&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;forEach&lt;/code&gt;, &lt;code&gt;splice&lt;/code&gt;, and their relatives) have been a recurring source of V8 type-confusion bugs for years, precisely because each one creates the same structural trap: the engine has to trust that an array's shape hasn't changed between when it checked and when it acts, and a callback is a standing invitation to change it in between.&lt;/p&gt;

&lt;p&gt;Fixing &lt;code&gt;TryReduceArrayPrototypeSort&lt;/code&gt; closes this instance. It doesn't close the pattern, and V8's own security team would likely be the first to admit it.&lt;/p&gt;

&lt;p&gt;The other three 2026 zero-days (a CSS font-handling use-after-free in February, a Skia 2D-rasterizer out-of-bounds write in March, and a Dawn/WebGPU use-after-free in April) sit in the same broad territory: native C++ code processing untrusted, attacker-shaped input as fast as possible. None of the six needed a user to do anything beyond load a page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blind spot: headless Chrome, Electron, and CI images
&lt;/h2&gt;

&lt;p&gt;Most coverage of a Chrome zero-day assumes a human with a mouse. A meaningful share of Chromium's install base doesn't have one. Headless Chrome renders PDFs and screenshots in backend services; Electron embeds a full Chromium in thousands of desktop apps; CI runners spin up Chrome to run end-to-end test suites against pages they don't fully control.&lt;/p&gt;

&lt;p&gt;None of that changes the exploit's mechanics; rendering the malicious page is still what triggers it. But it does change the blast radius, since a compromised renderer in a CI job or a scraping pipeline can sit far closer to real credentials (CI secrets, internal network access) than a compromised renderer in a consumer's browser tab.&lt;/p&gt;

&lt;p&gt;Electron makes this worse in one specific, structural way: unlike Chrome or Edge, an Electron app doesn't auto-update its bundled Chromium independently of the app itself. Its maintainers have to bump the Electron dependency, rebuild, test, and ship a new release before users ever see the fix.&lt;/p&gt;

&lt;p&gt;That adds days to weeks on top of Google's own patch timeline as a matter of routine, and sometimes much longer for apps still pinned to an older Electron major version. A vulnerable V8 build can keep shipping inside a popular desktop app long after Chrome itself is clean.&lt;/p&gt;

&lt;p&gt;It also changes the patch story in a way that's easy to miss. Chrome updates its binaries on disk but doesn't take effect until the process restarts, which matters more for a long-lived headless service or a kiosk build than for a browser someone closes and reopens every day.&lt;/p&gt;

&lt;p&gt;VM templates, Docker base images, and CI runner images are exactly the kind of artifact that quietly reverts a patched Chromium back to a vulnerable one the next time it gets rebuilt from an older layer. If your team ships Electron, runs headless Chrome for PDF generation or link previews, or bakes Chrome into a CI image, updating Chrome is a fleet-management problem, not a one-click menu action.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do about it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Update and restart, not just update.&lt;/strong&gt; Downloading the patch in the background doesn't protect a running process; every Chrome and Chromium-based browser needs a full relaunch to load 152.0.7977.82 or later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check every Chromium-based browser separately.&lt;/strong&gt; Edge, Brave, Opera, and Vivaldi each ship their own build on their own timeline. Updating Chrome does nothing for the others.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audit build images, not just endpoints.&lt;/strong&gt; CI base images, Docker images bundling headless Chrome, and Electron app bundles are common places for a patched version to silently regress on the next rebuild.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat anomalous renderer behavior as a signal.&lt;/strong&gt; A Chrome renderer process spawning a shell, hitting non-Google infrastructure, or restarting unexpectedly is a stronger detection signal than trying to catch the initial JavaScript exploit, which executes entirely in-memory and leaves little to fingerprint.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If you operate Federal Civilian Executive Branch infrastructure&lt;/strong&gt;, the CISA KEV deadline of September 18, 2026 is a binding compliance date, not a suggestion.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Is "the sandbox held" the real headline?
&lt;/h2&gt;

&lt;p&gt;Here's the opinion part, since a headline correction alone doesn't quite earn the label "opinion breakdown." Chromium's process-and-sandbox architecture is, in my view, one of the more genuinely successful pieces of applied security engineering in mainstream consumer software, and that fact is nearly invisible in how zero-days get covered, including, this time, in the framing of the original submission.&lt;/p&gt;

&lt;p&gt;A bug that gets arbitrary read/write on the JavaScript heap and still can't touch your filesystem is exactly what defense-in-depth is supposed to produce. The fact that it takes real effort to explain why that's true, and that the effort mostly doesn't get made, says more about how security coverage works than about the bug itself.&lt;/p&gt;

&lt;p&gt;That doesn't make the instinct to patch immediately wrong. It's the correct instinct, and the bug really is being used against real targets today. But there's a second, quieter problem that the accurate framing surfaces and the inaccurate one doesn't: Gulizia's complete technical writeup, methodology included, went live on a public blog six days before most of Chrome's install base had the fix.&lt;/p&gt;

&lt;p&gt;Google's own restrained-disclosure policy (withholding bug-tracker details until most users update) depends on nobody else publishing the technical path first. A researcher publishing for CTF credit, entirely within the rules of a program Google itself runs, ended up creating exactly the situation that policy exists to prevent.&lt;/p&gt;

&lt;p&gt;That tension, between what's good for an individual researcher's career and what's good for the ecosystem's rollout timing, is the more interesting story underneath this one. "Sandbox RCE in all Chromium versions" doesn't leave room for it.&lt;/p&gt;

&lt;p&gt;None of this is unique to Chrome. Almost every high-profile CVE goes through the same compression on its way to a headline: a precise, hedged advisory gets summarized by a blog, re-summarized by an aggregator, and re-titled by whoever wants the most clicks off the front page. The words that survive that process tend to be the scariest ones, not the most accurate ones.&lt;/p&gt;

&lt;p&gt;"Sandbox RCE in all Chromium versions" isn't really a lie. It's a headline that won a game of telephone. The advisory it started from was more careful than that, and so, it turns out, was the researcher who found the bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://serotav.github.io/Writeups/v8/when-sorting-leads-to-confusion/" rel="noopener noreferrer"&gt;Salvatore Gulizia's technical writeup&lt;/a&gt; — the original source for the Maglev bug mechanics described above&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://chromereleases.googleblog.com/" rel="noopener noreferrer"&gt;Chrome Releases blog, Chrome 152 Stable update&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-85046" rel="noopener noreferrer"&gt;NIST National Vulnerability Database: CVE-2026-85046&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog" rel="noopener noreferrer"&gt;CISA Known Exploited Vulnerabilities Catalog&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/the-sandbox-held-the-headline-didn-t-6qv9x?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication-footer" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>chromium</category>
      <category>v8</category>
      <category>browsersecurity</category>
      <category>cve202685046</category>
    </item>
    <item>
      <title>OpenBot: A Technical Architecture Review of CopilotKit's Governed Agent Runtime</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Sun, 06 Sep 2026 05:54:12 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/openbot-a-technical-architecture-review-of-copilotkits-governed-agent-runtime-3ej6</link>
      <guid>https://dev.to/sanjay_singh_1/openbot-a-technical-architecture-review-of-copilotkits-governed-agent-runtime-3ej6</guid>
      <description>&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;CopilotKit released OpenBot's first alpha on August 17, 2026, then shipped three more tagged versions inside the following five days, ending with v0.0.4 on August 22. The project is MIT licensed and lives at &lt;a href="https://github.com/CopilotKit/openbot" rel="noopener noreferrer"&gt;github.com/CopilotKit/openbot&lt;/a&gt;. The pitch: a Bot works from a browser and a set of files it shares with no other Bot, limited strictly to whatever an administrator has explicitly granted it. A policy check runs against every action before it executes, and the decision, whether allowed or refused, is written to an audit trail afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it sits
&lt;/h2&gt;

&lt;p&gt;OpenBot is not CopilotKit's first release. The company stewards AG-UI, an open protocol for agent-to-application communication that OpenBot's own documentation lists as compatible with LangGraph, Mastra, CrewAI, Pydantic AI, and Google ADK, among other frameworks. CopilotKit raised a $27 million Series A in May 2026, led by Glilot Capital, NFX, and SignalFire, to build out AG-UI and an enterprise layer on top of it, according to &lt;a href="https://techcrunch.com/2026/05/05/copilotkit-raises-27m-to-help-devs-deploy-app-native-ai-agents/" rel="noopener noreferrer"&gt;TechCrunch&lt;/a&gt;. OpenBot is the self-hosted, open half of that layer: governance is its main feature rather than something bolted on afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of a deployment
&lt;/h2&gt;

&lt;p&gt;A deployment is eight moving parts, most of them Docker containers: &lt;code&gt;app&lt;/code&gt;, the React interface; &lt;code&gt;server&lt;/code&gt;, a Hono API that also holds auth, policy, audit, and the coworker roster; &lt;code&gt;agent-computer&lt;/code&gt;, one Chromium instance per Bot with its own workspace. &lt;code&gt;agent-bot&lt;/code&gt; and &lt;code&gt;agent-langgraph&lt;/code&gt; are the two example Bots that ship in the box, and a &lt;code&gt;supervisor&lt;/code&gt; creates and tears them down. PostgreSQL with pgvector holds everything else, and CopilotKit Intelligence, an external service, holds threads and memory.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;app&lt;/td&gt;
&lt;td&gt;3010&lt;/td&gt;
&lt;td&gt;React/Vite interface for channels, chat, and admin pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;server&lt;/td&gt;
&lt;td&gt;3001&lt;/td&gt;
&lt;td&gt;API, auth, policy, audit, plugins, coworkers, channels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent-computer&lt;/td&gt;
&lt;td&gt;4100&lt;/td&gt;
&lt;td&gt;Chromium, workspace, browser profile per Bot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent-bot&lt;/td&gt;
&lt;td&gt;4200&lt;/td&gt;
&lt;td&gt;Proof-of-concept AG-UI Bot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent-langgraph&lt;/td&gt;
&lt;td&gt;4201&lt;/td&gt;
&lt;td&gt;LangGraph AG-UI Bot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;supervisor&lt;/td&gt;
&lt;td&gt;4500/4300&lt;/td&gt;
&lt;td&gt;Creates, stops, resets per-Bot computers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL + pgvector&lt;/td&gt;
&lt;td&gt;5432&lt;/td&gt;
&lt;td&gt;Policy, audit, credentials, grants, channels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CopilotKit Intelligence&lt;/td&gt;
&lt;td&gt;external&lt;/td&gt;
&lt;td&gt;Durable threads and memory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/CopilotKit/openbot/blob/main/docs/architecture.md" rel="noopener noreferrer"&gt;docs/architecture.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gateway is the whole point
&lt;/h2&gt;

&lt;p&gt;A Bot never reaches a browser, a file, or an MCP server directly. Every attempt loops back through the server first, which works through a fixed sequence before anything happens: resolve what the Bot is pointing at, using the snapshot on file, weigh that against the live policy, log the outcome, and only then hand the request to the computer. A second log entry follows if the computer itself fails to carry the action out, and nowhere in that sequence can an action complete before its own record does.&lt;/p&gt;

&lt;p&gt;Each rule is written as a CEL expression that can inspect fields such as &lt;code&gt;tool.name&lt;/code&gt;, &lt;code&gt;bot.id&lt;/code&gt;, &lt;code&gt;page.host&lt;/code&gt;, &lt;code&gt;element.ref&lt;/code&gt;, or &lt;code&gt;mcp.tool&lt;/code&gt;. Denials are always weighed before permissions, and the engine treats its own malfunctions as a reason to say no: nothing gets through under a missing policy, and a deny rule that breaks still counts as a refusal.&lt;/p&gt;

&lt;p&gt;There's a catch worth flagging: the shipped default is &lt;code&gt;deny: []&lt;/code&gt; and &lt;code&gt;allow: ["true"]&lt;/code&gt;, which permits everything until an administrator writes a restrictive rule at &lt;code&gt;/admin/boundaries&lt;/code&gt;. "Fails closed" describes what happens when the policy breaks, not what the box does on day one.&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNplkMFu4kAMQH_F6_PkUGlPHFYCCrRS9wLq7iFwMImTjOqMoxmHLAX-vUrg1D3aenrP8gULLRlnWIkORUPR4G27DwAA83yhBidPMN9k768HyLJfV1MVKEjkCot8Q8YDnQ93fjECsMy3nFRODEaxZoMqagspUJcatQe6nNDny-pE0pMxLFdv0Kn44ny7E89TjUR04PIKq_xv9MZAfekNog4P0WoSrXOqOVhWaNv1xhEKDUY-cHxg68lWkZd0hc3DlbjQUP6nvJcjV30ayy_fyxCo9aEGaxhiL3xAhy3HlnyJswtaw-34z5Ir6sXQ3Td_KHo6CqeRqTTYmlovZ5xhRl0nnKVzMm4dLMSHj99U7KZ5rcEc7HHHtTK8v-7RwVaPaurgheXE5gtyMI-exEGikLLE0VfopsjOf463PP3s_uHt5vBYL1U04gx_DI03xtsXKUmq_A%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNplkMFu4kAMQH_F6_PkUGlPHFYCCrRS9wLq7iFwMImTjOqMoxmHLAX-vUrg1D3aenrP8gULLRlnWIkORUPR4G27DwAA83yhBidPMN9k768HyLJfV1MVKEjkCot8Q8YDnQ93fjECsMy3nFRODEaxZoMqagspUJcatQe6nNDny-pE0pMxLFdv0Kn44ny7E89TjUR04PIKq_xv9MZAfekNog4P0WoSrXOqOVhWaNv1xhEKDUY-cHxg68lWkZd0hc3DlbjQUP6nvJcjV30ayy_fyxCo9aEGaxhiL3xAhy3HlnyJswtaw-34z5Ir6sXQ3Td_KHo6CqeRqTTYmlovZ5xhRl0nnKVzMm4dLMSHj99U7KZ5rcEc7HHHtTK8v-7RwVaPaurgheXE5gtyMI-exEGikLLE0VfopsjOf463PP3s_uHt5vBYL1U04gx_DI03xtsXKUmq_A%3Ftype%3Dpng" alt="Mermaid Diagram" width="1790" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One computer per Bot
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;COMPUTER_SUPERVISOR_URL&lt;/code&gt; set, every Bot gets its own container: its own Chromium, its own &lt;code&gt;/workspace&lt;/code&gt;, its own logins. Without it, Bots share one computer, which is a meaningfully weaker guarantee. Nothing on the host network can simply guess its way into a Bot's logged-in browser, either: computers only listen on the loopback address, and every request to one has to present a token scoped to that specific container.&lt;/p&gt;

&lt;p&gt;Setting &lt;code&gt;COMPUTER_RUNTIME&lt;/code&gt; to &lt;code&gt;runsc&lt;/code&gt; switches these containers to gVisor on hosts that support it, trading a bit of performance for a stronger sandbox boundary. A set of SPIRE services for workload identity is already defined in the compose file, though the shipped start script leaves them off for now.&lt;/p&gt;

&lt;p&gt;Browser navigation is limited to plain &lt;code&gt;http&lt;/code&gt; and &lt;code&gt;https&lt;/code&gt;, and anything that resembles a cloud metadata address is refused no matter how the rest of the deployment is configured, closing off a well-known trick for getting an agent to leak cloud credentials it was never supposed to touch. The same validation applies to custom agent endpoints, not just page navigation: pointing a coworker at your own AG-UI server runs through those identical checks, and any authorization header attached to it is stored write-only, never returned by the API afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a Bot has to stop
&lt;/h2&gt;

&lt;p&gt;When a Bot runs into something it shouldn't push through alone, a sign-in page or a two-factor prompt, it stops and flags a person rather than guessing at a way around it. Control then shifts to that same person inside the live-view panel the Bot had been using, and each stage of that handoff becomes its own logged event: &lt;code&gt;computer.help_requested&lt;/code&gt;, &lt;code&gt;computer.control_taken&lt;/code&gt;, &lt;code&gt;computer.control_released&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For as long as a human holds the controls, anything the Bot itself attempts is turned away outright rather than held for later. Entering a secret gets the same careful handling: what ends up in the audit trail is the fact that one was requested and its length in characters, not the value a person typed in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools are governed twice
&lt;/h2&gt;

&lt;p&gt;MCP connections and skills draw from the same underlying grant table, but who is allowed to create each one differs. Adding an MCP server is an administrator's call alone, since it reaches outside systems using stored credentials, and the built-in catalogue tops out at Atlassian, Box, Slack, Salesforce, and ServiceNow. Anything not explicitly confirmed as read-only, including tools from custom servers, defaults to being treated as a write.&lt;/p&gt;

&lt;p&gt;Skills are just instructions, invoked with a slash in the composer. A skill can declare which tools it expects, but declaring one grants nothing: the actual offer is still intersected with whatever that Bot was separately granted.&lt;/p&gt;

&lt;p&gt;That distinction mattered for a scaling problem CopilotKit found in practice. A model's accuracy at picking the correct tool drops off sharply once the list of options grows: reliable around ten, shaky by the time it reaches thirty. As of v0.0.3, that ceiling is handled directly: a Bot holding more than a dozen granted tools is now shown only the subset that the skills matching each message actually declare, not its entire grant list.&lt;/p&gt;

&lt;p&gt;A new audit event, &lt;code&gt;mcp.tools_discovered&lt;/code&gt;, records how many tools were offered out of how many were granted, and why. That answers the harder failure mode of a Bot that silently calls nothing at all and answers from memory instead, which is a worse outcome than calling the wrong tool.&lt;/p&gt;

&lt;p&gt;That narrowing had nothing to draw on until v0.0.4, though: a brand-new deployment ships with no skills at all, so on a fresh clone there was nothing to match incoming messages against, and the feature never actually switched on. v0.0.4 added an optional &lt;code&gt;skills.yaml&lt;/code&gt; a tenant package can ship, seeding a starting set of skills at boot so the narrowing has something to work with from the very first run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Answers can be a component, not just prose
&lt;/h2&gt;

&lt;p&gt;Bots can also answer with a rendered component instead of only prose, the same generative-UI pitch CopilotKit makes to enterprises elsewhere: a revenue question comes back as an interactive chart the company itself designed, not a wall of text. Compiled components publish automatically the first time a catalogue sync sees them; ones built by hand in &lt;code&gt;/admin/playground&lt;/code&gt; stay drafts until someone explicitly publishes them. Either way, every render call still asks the server whether that component exists, is published, and hasn't been withheld from the calling Bot specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What four days of patches reveal
&lt;/h2&gt;

&lt;p&gt;Two changes between v0.0.1 and v0.0.4 are worth reading closely, because they show what this kind of gateway has to get right that an ordinary CRUD app does not. The first, in v0.0.2, is a deliberate architectural reversal: OpenBot stopped maintaining its own index of company documents. Early on, a Google Drive connector had run as a single privileged service account, impersonating whichever user asked, and copying files it found into a local vector store.&lt;/p&gt;

&lt;p&gt;Under that design, two different people asking the same question got an answer shaped by what one shared credential could see, not by what either of them was individually allowed to see. Removing someone's Drive access did nothing to the copy already sitting in OpenBot's own database. The fix was to delete the index rather than patch it: the rebuilt connector holds no local copy of anything, and each person links their own Google account instead of relying on the shared one.&lt;/p&gt;

&lt;p&gt;A Bot now reads Drive scoped to whichever person is asking, requesting a fresh access token on every call instead of reusing a cached one. That means revoking someone's access at Google shows up the moment they try again, not whenever some cached copy of their permission finally times out. Keeping a second, separately-permissioned copy of someone else's data turned out to be a bigger liability than the latency it was saving.&lt;/p&gt;

&lt;p&gt;The second change, landing in v0.0.4, is a straightforward security bug rather than a design choice. When a Bot wants to click something on a page, it points at that element using a reference saved in an earlier snapshot, and the server has to look that reference up before any policy rule can weigh in. Before v0.0.4, a lookup that failed to resolve was not treated as a reason to stop.&lt;/p&gt;

&lt;p&gt;Instead the action proceeded with an empty description of what was being clicked, so a deny rule written against &lt;code&gt;element.name&lt;/code&gt; or &lt;code&gt;element.role&lt;/code&gt; had nothing to compare against and simply never matched. The one case this slipped through on was a computer container restarting under an old, previously-taken snapshot after a redeploy. The computer's own staleness check only flags a mismatch between its internal counter and the reference, so a reference that was outdated on both sides looked perfectly consistent to it.&lt;/p&gt;

&lt;p&gt;Practically, that meant the exact same click, against the exact same policy, could be refused before a redeploy and silently allowed right after one. As of v0.0.4, any reference the server cannot actually resolve is refused outright rather than passed through with a blank element description, and the person driving that Bot is asked to capture a fresh snapshot before continuing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "open source" does not quite mean here
&lt;/h2&gt;

&lt;p&gt;The code is MIT licensed, but a working deployment is not fully self-contained. Durable threads and memory live in CopilotKit Intelligence, an external service that requires its own project and a license token obtained through &lt;code&gt;npx copilotkit login&lt;/code&gt;, not in the PostgreSQL instance sitting next to everything else. There is also no hosted option yet: the &lt;a href="https://github.com/CopilotKit/OpenBot/releases" rel="noopener noreferrer"&gt;release notes&lt;/a&gt; list it as local-only, and the default run mode, &lt;code&gt;OPENBOT_DEV_NO_AUTH&lt;/code&gt;, admits every request as a single administrator until Google sign-in is wired up separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;A deployment needs Docker, Bun 1.3 or newer, a CopilotKit Intelligence project and license, and a model key: OpenAI for the built-in Bot, or OpenAI, Anthropic, or Google for the LangGraph one. Copy &lt;code&gt;.env.example&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt;, then run &lt;code&gt;npx copilotkit login&lt;/code&gt;, &lt;code&gt;project select&lt;/code&gt;, and &lt;code&gt;license --write&lt;/code&gt; to populate the Intelligence credentials, fill in a model key, and generate a &lt;code&gt;KEY_ENCRYPTION_KEY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bun install&lt;/code&gt; followed by &lt;code&gt;bash scripts/start.sh&lt;/code&gt; brings up Postgres, the computers, and both servers, reachable at &lt;code&gt;localhost:3010&lt;/code&gt;. The README's suggested first test is asking a Bot to open Hacker News and summarize the top story, then checking &lt;code&gt;/admin/audit&lt;/code&gt; to see the decision it recorded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;Four tagged releases in five days is a sign of a team actively shepherding something rather than one that shipped and walked away, but it is also a sign this is genuinely alpha: the stale-citation fix in v0.0.4 was a real gap in the exact mechanism the whole project is sold on. Teams evaluating agent governance patterns right now have more to learn from reading &lt;code&gt;docs/architecture.md&lt;/code&gt; than from running it in anything resembling production this early.&lt;/p&gt;

&lt;p&gt;The idea worth taking regardless of whether OpenBot itself gets used is the ordering it enforces: resolve the target, decide against policy, record the decision, only then act, with a refusal that names the exact rule that caused it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/openbot-a-technical-architecture-review-of-copilotkit-s-governed-agent-runtime-rg1sa?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication-footer" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>copilotkit</category>
      <category>openbot</category>
      <category>agentgovernance</category>
      <category>aguiprotocol</category>
    </item>
    <item>
      <title>Nvidia Just Bought Hugging Face for $12.9 Billion — Here's What It Means for Developers</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Fri, 04 Sep 2026 05:09:52 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/nvidia-just-bought-hugging-face-for-129-billion-heres-what-it-means-for-developers-amk</link>
      <guid>https://dev.to/sanjay_singh_1/nvidia-just-bought-hugging-face-for-129-billion-heres-what-it-means-for-developers-amk</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; On September 2, 2026, Nvidia signed a definitive agreement to acquire Hugging Face — comprising an $11.9 billion cash payment plus up to $1 billion in employee retention equity, roughly $12.9 billion total. It's Nvidia's second-biggest acquisition ever, behind only the $20 billion Groq deal from December 2025, and well ahead of its previous largest deal, the $7 billion Mellanox acquisition in 2019. Nvidia says the platform stays open — no requirement to use Nvidia hardware, continued support for AMD and Intel libraries. The real question, according to nearly every analyst covering the deal, isn't the stated policy. It's whether a platform owned by the dominant GPU vendor stays neutral by default, or drifts Nvidia-first through ordinary product prioritization. Expect a genuine antitrust review — this is the first Nvidia AI acquisition structured as a full merger rather than the licensing deals that avoided one.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Was Actually Agreed
&lt;/h2&gt;

&lt;p&gt;Nvidia's September 2, 2026 SEC filing confirms a definitive agreement to acquire Hugging Face, described as operating a platform and community for developing, sharing, and deploying open-source models, datasets, and applications. The transaction includes an approximately $11.9 billion purchase price to Hugging Face stockholders, plus an equity-based retention program of up to approximately $1 billion for Hugging Face employees joining Nvidia. That combination is where the widely reported $12.9 billion figure comes from.&lt;/p&gt;

&lt;p&gt;The deal is expected to close in the first half of 2027, pending regulatory approval. Until then, both companies operate independently.&lt;/p&gt;

&lt;p&gt;This is Nvidia's second-biggest acquisition on record, trailing only the roughly $20 billion purchase of assets from chipmaker Groq in December 2025. It dwarfs Nvidia's other recent deals, including the $700 million purchase of Israeli AI infrastructure firm Run:ai in 2024, and comfortably exceeds what was previously Nvidia's largest acquisition — the nearly $7 billion purchase of chipmaker Mellanox in 2019.&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNpNj8FOwkAQhl9lnHjcmtKgxh5MAIOYyAWil66HaRnKxO1u3V0UJLy7aZsYjjP5vn_mP2HlNow51p7aHbyutAUAmBQal2wMWXfQ2mbp6EFre30PpRgjzmr8gCR5hGmhcbW3OUlPZeOeSlNo_rkhcNrjs0Ljs3dfQCFwDINz2zlZehE9KLNeeSo0LvZ1LbaGOVU8OHedM8puHi4sVNiwb0g2mJ8w7rjpem14S3sTUQ2bd_JCpeHQMVtn45waMUfMMaG2NZyEY4jcKJgasZ9Lqtb9PHc2KtC45toxvL1oVLBypYtOwYLNN0epSMHECxkFgWxIAnvZouqPrOW3-2U0bg94Piss65kzzmOOVz87iYznP_5qfZI%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNpNj8FOwkAQhl9lnHjcmtKgxh5MAIOYyAWil66HaRnKxO1u3V0UJLy7aZsYjjP5vn_mP2HlNow51p7aHbyutAUAmBQal2wMWXfQ2mbp6EFre30PpRgjzmr8gCR5hGmhcbW3OUlPZeOeSlNo_rkhcNrjs0Ljs3dfQCFwDINz2zlZehE9KLNeeSo0LvZ1LbaGOVU8OHedM8puHi4sVNiwb0g2mJ8w7rjpem14S3sTUQ2bd_JCpeHQMVtn45waMUfMMaG2NZyEY4jcKJgasZ9Lqtb9PHc2KtC45toxvL1oVLBypYtOwYLNN0epSMHECxkFgWxIAnvZouqPrOW3-2U0bg94Piss65kzzmOOVz87iYznP_5qfZI%3Ftype%3Dpng" alt="Mermaid Diagram" width="699" height="109"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At an annualized revenue of roughly $150 million recently disclosed by The Information, the deal values Hugging Face at somewhere between 79× and 86× that figure — 79× if you count only the $11.9 billion cash portion, 86× including the retention equity. Either way, a multiple that only makes sense as an infrastructure bet, not a near-term earnings multiple.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Hugging Face Said Yes
&lt;/h2&gt;

&lt;p&gt;This wasn't Nvidia cold-approaching a reluctant target. Hugging Face CEO Clément Delangue told CNBC he approached Nvidia first, describing the summer conversations as a realization that Hugging Face and open-source AI had reached a turning point requiring more resources and scale. Delangue called Nvidia "a perfect home" for the company.&lt;/p&gt;

&lt;p&gt;That framing matters given the history. Nvidia already held a stake in Hugging Face from a 2023 Series D round worth $235 million alongside Salesforce and Google, which valued the company at $4.5 billion. Earlier in 2026, Hugging Face had turned down a separate $500 million investment offer from Nvidia that would have valued the company at $7 billion.&lt;/p&gt;

&lt;p&gt;Eighteen months later, the number is roughly 1.8× that rejected valuation — and this time Hugging Face brought the conversation to Nvidia, not the other way around.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNpVkMFOwkAQhl9lnPS4NaWAxh5MWpRgIh5AvbAclnZaVra7ze4WRYLPblqCiceZfPP_X-aIuSkIE6ysaLbwvOAaACBdcYyjeMi5ftnLQgr4MFI7COLheA7WtLrgXAej63EGe6Fa4aXRHNcQhveQrTg-CqsOEEfxDed6NoWCciU1OQjGUTQHU5Zku4Tb__fn9qyPmaw4pm11CXkVaufAUmOsp-KPnfTsw4rjkhqI2QVPK0tUk_bgZKWp0_0JBvH1XcZxjQxrsrWQBSZH9FuquxcUVIpWeWTnzbuwUmwUuY4pjfZTUUt1wARD0TSKQndwnmoGmZJ6Nxf5sp-nRnsGnU5lCN6eODJYmI3xhsGM1J68zAWD1EqhGDihXejIyhJZX7KU353LYNR84enEcFNNjDIWE7z63EpPePoFevqK9g%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNpVkMFOwkAQhl9lnPS4NaWAxh5MWpRgIh5AvbAclnZaVra7ze4WRYLPblqCiceZfPP_X-aIuSkIE6ysaLbwvOAaACBdcYyjeMi5ftnLQgr4MFI7COLheA7WtLrgXAej63EGe6Fa4aXRHNcQhveQrTg-CqsOEEfxDed6NoWCciU1OQjGUTQHU5Zku4Tb__fn9qyPmaw4pm11CXkVaufAUmOsp-KPnfTsw4rjkhqI2QVPK0tUk_bgZKWp0_0JBvH1XcZxjQxrsrWQBSZH9FuquxcUVIpWeWTnzbuwUmwUuY4pjfZTUUt1wARD0TSKQndwnmoGmZJ6Nxf5sp-nRnsGnU5lCN6eODJYmI3xhsGM1J68zAWD1EqhGDihXejIyhJZX7KU353LYNR84enEcFNNjDIWE7z63EpPePoFevqK9g%3Ftype%3Dpng" alt="Mermaid Diagram" width="918" height="109"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Nvidia Wants This
&lt;/h2&gt;

&lt;p&gt;The stated rationale is straightforward. Nvidia CEO Jensen Huang wrote in a blog post that the companies will scale Hugging Face's platform, strengthen its infrastructure, and expand access to AI for developers and institutions worldwide.&lt;/p&gt;

&lt;p&gt;The strategic rationale underneath is less about Hugging Face's revenue and more about defending Nvidia's position further up the stack. Every major closed-source AI lab is working to reduce its dependence on Nvidia silicon, to varying degrees — Google has run its own TPU line for years, Amazon designs the Trainium chips that Anthropic trains much of its work on, and OpenAI has reportedly pursued custom silicon of its own.&lt;/p&gt;

&lt;p&gt;None of that spending disappears because of this deal. But a thriving open-source ecosystem that still runs overwhelmingly on Nvidia hardware is a hedge against how fast any of it matters — it keeps enterprise customers with a credible alternative to the closed labs tethered to Nvidia, regardless of what those labs do with their own chips.&lt;/p&gt;

&lt;p&gt;Nvidia had also scaled back its own DGX Cloud business about a year earlier. This deal reads as a return to that platform ambition — this time by owning the distribution layer rather than building it from scratch.&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNoljsFugzAQRH9lu2dzqNQTh0ppaEqlpIqSJj1kczCwwKrGRrahTaP8ewU5zmjezFyxdBVjio3XfQvrHVkAgPzrRPgxSiWayL5tDwG0rWB5yBaEZ0iSZ9iuF58nwnxoGrENrHTJRHbjKjbQDsWcN1J47YUD4flePFEznr0eT4QZj2xczz4Q2UxC6Ub2CmqxnMTBsoKKe-MuhGdU2LHvtFSYXjG23E23K671YCKqu3PUXnRhOEyZ2tm40p2YC6aY6L43nIRLiNwpeDFivze63M965WxUQLjnxjEc3gkV7FzholOQsxk5SqkVLLxooyBoG5LAXmpU88he_qYvj0_9L95uCotm6YzzmOLDTyuR8fYP3dR3fA%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNoljsFugzAQRH9lu2dzqNQTh0ppaEqlpIqSJj1kczCwwKrGRrahTaP8ewU5zmjezFyxdBVjio3XfQvrHVkAgPzrRPgxSiWayL5tDwG0rWB5yBaEZ0iSZ9iuF58nwnxoGrENrHTJRHbjKjbQDsWcN1J47YUD4flePFEznr0eT4QZj2xczz4Q2UxC6Ub2CmqxnMTBsoKKe-MuhGdU2LHvtFSYXjG23E23K671YCKqu3PUXnRhOEyZ2tm40p2YC6aY6L43nIRLiNwpeDFivze63M965WxUQLjnxjEc3gkV7FzholOQsxk5SqkVLLxooyBoG5LAXmpU88he_qYvj0_9L95uCotm6YzzmOLDTyuR8fYP3dR3fA%3Ftype%3Dpng" alt="Mermaid Diagram" width="715" height="88"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Owning the platform layer directly above the chip means Nvidia doesn't need to win every individual model or framework fight — it only needs the layer where developers make that choice to keep running on its hardware by default.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Nvidia Has Promised — And the Catch
&lt;/h2&gt;

&lt;p&gt;Huang said Hugging Face will remain an open platform for the entire AI ecosystem, with developers free to choose their own models, frameworks, clouds, inference providers, and computing hardware — Nvidia systems will not be required. Nvidia's SEC filing formalizes this commitment, stating the company will keep Hugging Face's platform open, consistent with its existing practices, including support for other silicon vendors.&lt;/p&gt;

&lt;p&gt;That's a real, filed commitment — not just a talking point in a blog post. But nearly every analyst covering the deal points to the same structural tension underneath it.&lt;/p&gt;

&lt;p&gt;Hugging Face currently maintains Optimum AMD and Optimum Intel as first-class libraries enabling developers to run models on non-Nvidia hardware. Under Nvidia ownership, those libraries could see reduced maintenance investment, slower support for new models, or lower visibility in Hub documentation — not through any explicit policy, but through the ordinary resource allocation decisions of a company whose core revenue depends on Nvidia GPU adoption.&lt;/p&gt;

&lt;p&gt;If new library features, quantization formats, or serving optimizations quietly ship Nvidia-first, with other hardware backends catching up months later, that lag compounds — developers gravitate toward the fastest-supported path, and the fastest-supported path becomes the one that runs best on the owner's hardware. No policy decision required. Just normal product prioritization, repeated for a few release cycles.&lt;/p&gt;

&lt;p&gt;The closest precedent is Microsoft's 2018 acquisition of GitHub. GitHub kept supporting non-Microsoft integrations, and the platform didn't visibly close off — but critics have long pointed to subtler forms of the same dynamic: which tools get first-class support, which get maintained at arm's length.&lt;/p&gt;




&lt;h2&gt;
  
  
  This Deal Will Actually Face Antitrust Review
&lt;/h2&gt;

&lt;p&gt;This point separates the Hugging Face deal from Nvidia's recent acquisition pattern. Its deals with Groq and Poolside — an AI coding startup Nvidia has an investment and technology partnership with — were structured in ways that avoided triggering mandatory Hart-Scott-Rodino review, the standard US pre-merger antitrust filing.&lt;/p&gt;

&lt;p&gt;The Hugging Face deal is different: an outright acquisition of the platform millions of developers rely on to distribute and discover models, it cannot be structured the same way. It triggers mandatory merger review in both the US and EU.&lt;/p&gt;

&lt;p&gt;The FTC and DOJ opened a joint inquiry into competitive AI partnerships in February 2026, specifically examining whether "quasi-merger" structures were designed to sidestep standard antitrust review — making this the first real test of that scrutiny against Nvidia's current acquisition strategy.&lt;/p&gt;

&lt;p&gt;For comparison: Nvidia's much smaller $700 million Run:ai acquisition took months of EU review before receiving unconditional clearance, despite Nvidia's roughly 80% share of the AI GPU market being a central point of regulatory concern even at that deal size. A $12.9 billion platform acquisition sitting squarely on top of that same market share is a materially bigger question for regulators to work through.&lt;/p&gt;

&lt;p&gt;Don't expect a fast close. The stated H1 2027 timeline already assumes a real review process, not a formality.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Means If You Build on Hugging Face Today
&lt;/h2&gt;

&lt;p&gt;For the next several months — likely through the H1 2027 close, and probably well beyond it — practically nothing changes for day-to-day usage. The Transformers library, model hub, and Spaces continue operating as they do now. The acquisition doesn't touch model licensing terms.&lt;/p&gt;

&lt;p&gt;What's worth actually watching:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardware-neutral tooling investment.&lt;/strong&gt; If you rely on Optimum AMD, Optimum Intel, or other non-Nvidia deployment paths, watch their release cadence over the next 12–18 months relative to Nvidia-first tooling. A slowdown wouldn't be announced — it would just show up as slower issue resolution and fewer supported model architectures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alternatives exist, but none fully match Hugging Face's combined scale.&lt;/strong&gt; Ollama offers neutral local inference, AWS Bedrock and Google's Vertex AI Model Garden provide managed hosting tied to their own hardware, and ModelScope serves a similar role in the Chinese market — but none currently matches Hugging Face's combined reach across model hosting, datasets, and the Transformers ecosystem. Diversifying your deployment tooling now costs little and reduces platform-risk exposure later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regulatory conditions could reshape the deal before it closes.&lt;/strong&gt; If antitrust review results in behavioral commitments — guaranteed non-Nvidia library support levels, for instance — those terms would matter more to working developers than anything in Nvidia's current messaging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Licensing hasn't changed and there's no indication it will.&lt;/strong&gt; Nothing reported so far suggests Hugging Face's open-source licensing model changes as a result of this acquisition — the concern raised by analysts is about hardware neutrality and platform incentives, not the licenses attached to hosted models.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is the deal final?&lt;/strong&gt; No. A definitive agreement is signed, but it's subject to regulatory approval in the US and EU, with an expected close in the first half of 2027. Signed agreements at this scale can still be modified, delayed, or in rare cases blocked during review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this affect the Hugging Face Transformers library or model licenses?&lt;/strong&gt; Not based on anything disclosed so far. The library remains open source, and existing model licenses are unaffected by a change in company ownership.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will Hugging Face still work well with AMD and Intel hardware?&lt;/strong&gt; Nvidia's SEC filing commits to it. The more useful question is what a slow drift away from that commitment would actually look like in practice — not an announced policy reversal, but Optimum AMD and Optimum Intel quietly falling behind on release cadence and model coverage over several quarters. That's the signal worth tracking, not the press release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does this compare to Nvidia's other recent AI deals?&lt;/strong&gt; It's structurally different. The Groq and Poolside transactions avoided triggering standard merger review. This is a full acquisition of a widely used developer platform, which means mandatory antitrust review in both the US and EU — the first time Nvidia's AI dealmaking faces that level of scrutiny.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;📖 &lt;a href="https://www.sec.gov/Archives/edgar/data/0001045810/000104581026000078/nvda-20260902.htm" rel="noopener noreferrer"&gt;Nvidia 8-K Filing — SEC EDGAR&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;📰 &lt;a href="https://www.cnbc.com/2026/09/03/nvidia-agrees-to-buy-hugging-face-for-almost-13-billion-ai-expansion.html" rel="noopener noreferrer"&gt;Hugging Face Approached Nvidia's Huang Weeks Ahead of Deal — CNBC&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;📰 &lt;a href="https://variety.com/2026/digital/news/nvidia-acquires-hugging-face-12-9-billion-1236850349/" rel="noopener noreferrer"&gt;AI Giant Nvidia to Buy Hugging Face for $12.9 Billion — Variety&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;📰 &lt;a href="https://www.techtimes.com/articles/325863/20260828/nvidias-129b-hugging-face-deal-must-pass-antitrust-review-its-quasi-mergers-dodged.htm" rel="noopener noreferrer"&gt;Nvidia's $12.9B Deal Must Pass the Antitrust Review Its Quasi-Mergers Dodged — Tech Times&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;📰 &lt;a href="https://shattered.io/hugging-face-neutrality-nvidia-deal-2026/" rel="noopener noreferrer"&gt;Nvidia's $12.9B Deal Sparks Bias Fears — Shattered.io&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;📰 &lt;a href="https://techcrunch.com/2026/08/26/nvidia-closes-in-on-hugging-face-acquisition/" rel="noopener noreferrer"&gt;Nvidia Closes in on Hugging Face Acquisition — TechCrunch&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;📰 &lt;a href="https://qz.com/nvidia-hugging-face-acquisition-12-billion-082726" rel="noopener noreferrer"&gt;Nvidia Agrees to Buy Hugging Face for $12.9 Billion — Quartz&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;📰 &lt;a href="https://www.barchart.com/story/news/30246725/nvidia-nvda-finalizes-700-million-acquisition-of-runai-after-eu-approval" rel="noopener noreferrer"&gt;Nvidia Finalizes $700 Million Run:ai Acquisition After EU Approval — Barchart&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Published: September 3, 2026 · Read time: ~9 minutesTags: Nvidia, Hugging Face, AI Infrastructure, Open Source AI, Machine Learning, M&amp;amp;A, AI Backend, Developer Tools, 2026&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The commitments in Nvidia's SEC filing are real and legally meaningful — but they describe today's policy, not a guarantee about release priorities eighteen months from now. If your stack depends on non-Nvidia inference paths through Hugging Face, that's worth tracking as its own signal, independent of anything either company says publicly.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/nvidia-just-bought-hugging-face-for-12-9-billion-here-s-what-it-means-for-developers-6k93d" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>nvidia</category>
      <category>huggingface</category>
      <category>aiinfrastructure</category>
      <category>opensourceai</category>
    </item>
    <item>
      <title>Implementing Passkey Authentication in NestJS and PostgreSQL</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Thu, 03 Sep 2026 04:24:16 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/implementing-passkey-authentication-in-nestjs-and-postgresql-38cc</link>
      <guid>https://dev.to/sanjay_singh_1/implementing-passkey-authentication-in-nestjs-and-postgresql-38cc</guid>
      <description>&lt;h2&gt;
  
  
  Why now
&lt;/h2&gt;

&lt;p&gt;In May 2026 the FIDO Alliance put a number on something that had been a trend for a few years: &lt;a href="https://fidoalliance.org/fido-alliance-reports-accelerating-global-passkey-adoption-on-world-passkey-day-2026/" rel="noopener noreferrer"&gt;5 billion passkeys are now in active use worldwide&lt;/a&gt;. The same report found 90 percent consumer awareness, 75 percent of people with a passkey enabled on at least one account, and 49 percent who use one regularly when it is offered.&lt;/p&gt;

&lt;p&gt;That adoption is happening while the threat model underneath it is shifting. &lt;a href="https://www.verizon.com/about/news/breach-industry-wide-dbir-finds" rel="noopener noreferrer"&gt;Verizon's 2026 Data Breach Investigations Report&lt;/a&gt; found that vulnerability exploitation, at 31 percent of breaches, overtook stolen credentials as the leading breach entry point for the first time in the report's 19-year history. Credential theft did not go away; it was simply overtaken.&lt;/p&gt;

&lt;p&gt;Passwords are still doing plenty of damage in the meantime. &lt;a href="https://spycloud.com/blog/top-takeaways-from-the-2026-verizon-data-breach-investigations-report/" rel="noopener noreferrer"&gt;SpyCloud's 2026 Identity Exposure Report&lt;/a&gt; counted 5.3 billion credential pairs circulating in criminal marketplaces over the previous year, and found that roughly four in ten corporate users had reused an exposed password. None of that risk disappears just because exploitation edged ahead in the rankings.&lt;/p&gt;

&lt;p&gt;None of this makes passkeys a silver bullet. It makes them the highest-leverage fix available for the credential half of that risk, and one a small backend team can ship in a weekend rather than a quarter. The rest of this post builds real passkey support into a NestJS and PostgreSQL API, using the current release of SimpleWebAuthn (v13.3.3) and TypeScript 7.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a passkey actually is
&lt;/h2&gt;

&lt;p&gt;Strip away the marketing and a passkey is a discoverable, resident public-key credential defined by the &lt;code&gt;WebAuthn&lt;/code&gt; specification. Registration generates a key pair on the authenticator; the private key never leaves it and the server only ever stores the public half. There is no shared secret to phish, leak, or reuse across sites, which is the entire security argument in one sentence.&lt;/p&gt;

&lt;p&gt;Authenticators come in two flavors that matter for how you design an app. A device-bound passkey has a private key locked inside one piece of hardware, a security key or a TPM, and it never leaves. A syncable passkey has its private key replicated across a user's devices through a platform's encrypted sync fabric, such as iCloud Keychain or Google Password Manager, trading a little assurance for a lot fewer lockouts.&lt;/p&gt;

&lt;p&gt;That distinction is not academic if you sell into regulated customers. &lt;a href="https://pages.nist.gov/800-63-4/sp800-63b.html" rel="noopener noreferrer"&gt;NIST's SP 800-63B-4&lt;/a&gt; puts phishing-resistant authentication into the baseline for Authenticator Assurance Level 2, and it explicitly recognizes synced passkeys at AAL2. Device-bound passkeys can reach the stricter AAL3, but the specification is direct about the tradeoff: syncable authenticators "SHALL NOT be used at AAL3" because AAL3 requires a key that can never be exported.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Assurance level&lt;/th&gt;
&lt;th&gt;Syncable passkey&lt;/th&gt;
&lt;th&gt;Device-bound passkey&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AAL2 (phishing-resistant baseline)&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AAL3 (hardware-isolated key required)&lt;/td&gt;
&lt;td&gt;Not allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Two ceremonies, one round trip each
&lt;/h2&gt;

&lt;p&gt;Both registration and login are two-step ceremonies, and the two steps happen in separate HTTP requests. The server issues a random challenge and a set of options; the browser calls the &lt;code&gt;WebAuthn&lt;/code&gt; API with those options and gets back a signed response; the server verifies that response against the challenge it issued. Nothing about the second step can be trusted unless the first step's challenge is still provably the one the server generated.&lt;/p&gt;

&lt;p&gt;That means the challenge has to live somewhere between the two requests, and it cannot simply be a value the client hands back to you: an attacker intercepting the flow could hand back whatever challenge you gave them too. This implementation stores each challenge in Redis, keyed by user ID and ceremony type, with a two-minute TTL, which keeps the API itself fully stateless.&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;Injectable&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;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ioredis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CHALLENGE_TTL_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Registration and authentication are two-step ceremonies: generate options&lt;/span&gt;
&lt;span class="c1"&gt;// (which include a fresh challenge), then verify a response against that&lt;/span&gt;
&lt;span class="c1"&gt;// same challenge. The challenge has to live somewhere between those two&lt;/span&gt;
&lt;span class="c1"&gt;// requests, and it must not be trusted if the client hands it back to you.&lt;/span&gt;
&lt;span class="c1"&gt;// A short-lived Redis key keyed by user ID does the job without adding a&lt;/span&gt;
&lt;span class="c1"&gt;// stateful session cookie for a service that is otherwise stateless.&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&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;class&lt;/span&gt; &lt;span class="nc"&gt;WebAuthnChallengeStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`webauthn:challenge:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;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;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;key&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="nx"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EX&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;CHALLENGE_TTL_SECONDS&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;async&lt;/span&gt; &lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;key&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="nx"&gt;purpose&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="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing here is passkey-specific; it is a short-lived key-value pair with a purpose in the key so registration and login challenges cannot collide for a user doing both in the same window. If your app does not already run Redis, a signed, encrypted cookie with the same TTL works too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The schema
&lt;/h2&gt;

&lt;p&gt;The schema needs to capture more than just the public key. The counter and the device and backed-up flags are what let you detect a cloned credential later; skipping them now means you cannot add that check without a migration and a gap in your audit trail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;webauthn_credentials&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;credential_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;public_key&lt;/span&gt; &lt;span class="n"&gt;BYTEA&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;transports&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;device_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;backed_up&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;aaguid&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;last_used_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_webauthn_credentials_user_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;webauthn_credentials&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&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;credential_id&lt;/code&gt; is unique across the whole table, not just per user, because a login lookup only has the credential ID the browser returned to work with. public_key is stored as raw bytes rather than a base64 string: encoding it yourself just adds a lossy round trip that verification code then has to undo.&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;Column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;CreateDateColumn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;JoinColumn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ManyToOne&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;PrimaryGeneratedColumn&lt;/span&gt;&lt;span class="p"&gt;,&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;typeorm&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;User&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;./user.entity&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="nd"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webauthn_credentials&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;class&lt;/span&gt; &lt;span class="nc"&gt;WebAuthnCredentialEntity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;PrimaryGeneratedColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uuid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;ManyToOne&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;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;onDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CASCADE&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="nd"&gt;JoinColumn&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;user_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;user&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="nd"&gt;Column&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;user_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;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="c1"&gt;// Base64url credential ID from the authenticator. Unique across all users&lt;/span&gt;
  &lt;span class="c1"&gt;// so a lookup by ID alone is enough to find the owning row during login.&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;unique&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="nd"&gt;Column&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;credential_id&lt;/span&gt;&lt;span class="dl"&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;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;credentialId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// COSE public key, stored as raw bytes. Never store this as text: base64&lt;/span&gt;
  &lt;span class="c1"&gt;// encoding it yourself just adds a lossy round trip you have to reverse.&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Column&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;public_key&lt;/span&gt;&lt;span class="dl"&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;bytea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Signature counter reported by the authenticator. A stalled or&lt;/span&gt;
  &lt;span class="c1"&gt;// decreasing counter across authentications is the standard signal that&lt;/span&gt;
  &lt;span class="c1"&gt;// a credential was cloned; verifyAuthenticationResponse checks this,&lt;/span&gt;
  &lt;span class="c1"&gt;// this column is what makes that check possible run over run.&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Column&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;bigint&lt;/span&gt;&lt;span class="dl"&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="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="nd"&gt;Column&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;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;array&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;default&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="nx"&gt;transports&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="nd"&gt;Column&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;device_type&lt;/span&gt;&lt;span class="dl"&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;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;deviceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;singleDevice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;multiDevice&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="nd"&gt;Column&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;backed_up&lt;/span&gt;&lt;span class="dl"&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;boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;backedUp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Column&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;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;nullable&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="nx"&gt;aaguid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;CreateDateColumn&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;created_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Column&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;last_used_at&lt;/span&gt;&lt;span class="dl"&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;timestamptz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;nullable&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="nx"&gt;lastUsedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This maps directly onto the schema above. Two fields are worth a second look: counter is stored as a string because a Postgres bigint round-trips through the pg driver as a string once it exceeds JavaScript's safe integer range, and transports is a plain text array rather than a join table, since it is metadata about one credential rather than data with its own lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The service
&lt;/h2&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;Injectable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UnauthorizedException&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;@nestjs/common&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;InjectRepository&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;@nestjs/typeorm&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;Repository&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;typeorm&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;generateRegistrationOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;verifyRegistrationResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;generateAuthenticationOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;verifyAuthenticationResponse&lt;/span&gt;&lt;span class="p"&gt;,&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;@simplewebauthn/server&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;isoUint8Array&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;@simplewebauthn/server/helpers&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;AuthenticationResponseJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;AuthenticatorTransportFuture&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;RegistrationResponseJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;WebAuthnCredential&lt;/span&gt;&lt;span class="p"&gt;,&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;@simplewebauthn/server&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;User&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;./user.entity&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;WebAuthnCredentialEntity&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;./webauthn-credential.entity&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;WebAuthnChallengeStore&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;./webauthn-challenge.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;const&lt;/span&gt; &lt;span class="nx"&gt;RP_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Example App&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;RP_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WEBAUTHN_RP_ID&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;example.com&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;ORIGIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WEBAUTHN_ORIGIN&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&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="nd"&gt;Injectable&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;class&lt;/span&gt; &lt;span class="nc"&gt;WebAuthnService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;InjectRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WebAuthnCredentialEntity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Repository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WebAuthnCredentialEntity&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;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;challenges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WebAuthnChallengeStore&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;// Step 1 of registration: build the options object the browser needs to&lt;/span&gt;
  &lt;span class="c1"&gt;// call navigator.credentials.create(). excludeCredentials stops a user&lt;/span&gt;
  &lt;span class="c1"&gt;// from registering the same authenticator twice.&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getRegistrationOptions&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;User&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;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentials&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="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&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;generateRegistrationOptions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;rpName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RP_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;rpID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RP_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;userName&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;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;userDisplayName&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;displayName&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;isoUint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromUTF8String&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;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;attestationType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;excludeCredentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cred&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentialId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;transports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transports&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;AuthenticatorTransportFuture&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="p"&gt;})),&lt;/span&gt;
      &lt;span class="na"&gt;authenticatorSelection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;residentKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preferred&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;userVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preferred&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="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;challenges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&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;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;reg&lt;/span&gt;&lt;span class="dl"&gt;'&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;challenge&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;options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 2 of registration: verify the signed attestation the browser&lt;/span&gt;
  &lt;span class="c1"&gt;// returned, then persist the credential. Nothing is written to the&lt;/span&gt;
  &lt;span class="c1"&gt;// database until verification succeeds.&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;verifyRegistration&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;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RegistrationResponseJSON&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;expectedChallenge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;challenges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;consume&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;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;reg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;expectedChallenge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Registration challenge expired or missing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verification&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;verifyRegistrationResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;expectedChallenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;expectedOrigin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ORIGIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;expectedRPID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RP_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;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;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verified&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registrationInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;credentialDeviceType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;credentialBackedUp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aaguid&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registrationInfo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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;user&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;credentialId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;credential&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;publicKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;credential&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="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;transports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transports&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="na"&gt;deviceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;credentialDeviceType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;backedUp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;credentialBackedUp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;aaguid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;aaguid&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="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;verified&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="c1"&gt;// Step 1 of login: list the credential IDs already on file for this&lt;/span&gt;
  &lt;span class="c1"&gt;// account so the browser only prompts for one of those authenticators.&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getAuthenticationOptions&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;User&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;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentials&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="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No passkeys registered for this account&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&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;generateAuthenticationOptions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;rpID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RP_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;allowCredentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cred&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentialId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;transports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transports&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;AuthenticatorTransportFuture&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="p"&gt;})),&lt;/span&gt;
      &lt;span class="na"&gt;userVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preferred&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;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;challenges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&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;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;auth&lt;/span&gt;&lt;span class="dl"&gt;'&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;challenge&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;options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 2 of login: verify the signed assertion, then persist the new&lt;/span&gt;
  &lt;span class="c1"&gt;// counter value. Persisting newCounter is what lets the next login&lt;/span&gt;
  &lt;span class="c1"&gt;// detect a counter that went backwards, one signal a credential was&lt;/span&gt;
  &lt;span class="c1"&gt;// cloned onto a second, unauthorized authenticator.&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;verifyAuthentication&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;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AuthenticationResponseJSON&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;expectedChallenge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;challenges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;consume&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;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;auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;expectedChallenge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authentication challenge expired or missing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credentialId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;user&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;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;stored&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unrecognized credential for this account&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WebAuthnCredential&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentialId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&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="na"&gt;transports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transports&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;AuthenticatorTransportFuture&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;verification&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;verifyAuthenticationResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;expectedChallenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;expectedOrigin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ORIGIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;expectedRPID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RP_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;credential&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;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;stored&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="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authenticationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;newCounter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backedUp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authenticationInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentialBackedUp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastUsedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verified&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;generateRegistrationOptions&lt;/code&gt; needs the internal user ID as bytes, not a string, which is what isoUint8Array.fromUTF8String is for. excludeCredentials stops someone from registering the same authenticator twice, and residentKey: preferred asks the authenticator to create a discoverable credential, the property that lets a user sign in by picking an account rather than typing one first.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;verifyAuthenticationResponse&lt;/code&gt; takes the stored credential, including its last known counter, and returns a newCounter you are responsible for saving. Most platform authenticators report a counter of zero forever, so the check does little there, but for the security keys and older authenticators that do increment it, a counter that goes backward is the textbook sign of a cloned credential and should trigger revoking it, not just logging it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The controller
&lt;/h2&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;Body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Post&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&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;Request&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;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;AuthenticationResponseJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;RegistrationResponseJSON&lt;/span&gt;&lt;span class="p"&gt;,&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;@simplewebauthn/server&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;WebAuthnService&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;./webauthn.service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// CurrentUser stands in for whatever request-scoped auth decorator an app&lt;/span&gt;
&lt;span class="c1"&gt;// already uses (session, access token, etc). Passkeys replace the *login&lt;/span&gt;
&lt;span class="c1"&gt;// step*, not the fact that a request has an authenticated subject; the&lt;/span&gt;
&lt;span class="c1"&gt;// registration endpoints in particular must run behind existing auth so a&lt;/span&gt;
&lt;span class="c1"&gt;// stranger cannot add a passkey to someone else's account.&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AuthedRequest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;user&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;email&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;displayName&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webauthn&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;class&lt;/span&gt; &lt;span class="nc"&gt;WebAuthnController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;webauthn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WebAuthnService&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="nd"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;registration/options&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;getRegistrationOptions&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Req&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AuthedRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webauthn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRegistrationOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;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="nd"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;registration/verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;verifyRegistration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Req&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AuthedRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RegistrationResponseJSON&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webauthn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyRegistration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authentication/options&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;getAuthenticationOptions&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Req&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AuthedRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webauthn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthenticationOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;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="nd"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authentication/verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;verifyAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Req&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AuthedRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AuthenticationResponseJSON&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webauthn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller is intentionally thin. Every method reads req.user, which stands in for whatever session or access-token middleware the app already has; the important detail is that the registration endpoints sit behind that same authentication, so a passkey can only ever be added to the account that is already logged in, never to an arbitrary one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you ship it
&lt;/h2&gt;

&lt;p&gt;Do not remove password login the day this ships. Passkeys fail closed in ways passwords do not: a lost phone with no synced backup, a corporate device wiped on offboarding, a user who never finishes the registration flow. Offer passkeys as the default and keep an existing fallback until account recovery for the passkey-only path has actually been tested end to end.&lt;/p&gt;

&lt;p&gt;Let users register more than one authenticator, and say so in the UI. A single passkey tied to a single phone is a single point of failure; a phone and a hardware key, or a phone and a laptop's platform authenticator, means losing one device is an inconvenience instead of a lockout ticket.&lt;/p&gt;

&lt;p&gt;Two details surfaced only by actually compiling this against current packages. TypeScript 7 removed the legacy node moduleResolution setting outright, so a tsconfig carried over from an older NestJS project needs module and moduleResolution both set to nodenext, or both to node16, not left on the old default. TypeORM entities also need strictPropertyInitialization turned off under strict mode, since the ORM populates columns after construction, not inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;None of this closes the door Verizon flagged: vulnerability exploitation is the bigger single vector now, and passkeys will not patch a server. What they do is take the credential half of the risk, the half behind SpyCloud's 5.3 billion exposed pairs, and remove it for every account that enables one. Ship it as an addition, not a replacement, and the registration friction pays for itself the first time it blocks a credential-stuffing run.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/implementing-passkey-authentication-in-nestjs-and-postgresql-dy8q7" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>webauthn</category>
      <category>authentication</category>
      <category>postgres</category>
      <category>security</category>
    </item>
    <item>
      <title>Gemini 3.8 Flash and 3.8 Flash Cyber: Google Ships Its Third Flash Release in Six Weeks</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Thu, 03 Sep 2026 04:15:09 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/gemini-38-flash-and-38-flash-cyber-google-ships-its-third-flash-release-in-six-weeks-5bp</link>
      <guid>https://dev.to/sanjay_singh_1/gemini-38-flash-and-38-flash-cyber-google-ships-its-third-flash-release-in-six-weeks-5bp</guid>
      <description>&lt;p&gt;On September 2, 2026, Google introduced two new models on the Gemini blog: Gemini 3.8 Flash, a general-purpose workhorse model, and Gemini 3.8 Flash Cyber, a cybersecurity-focused variant with restricted access. The announcement came from Tulsee Doshi, Senior Director of Product Management, and Raluca Ada Popa, Gemini Security Lead at Google DeepMind, on &lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/" rel="noopener noreferrer"&gt;blog.google&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three releases, six weeks
&lt;/h2&gt;

&lt;p&gt;Google frames this as its third Flash-tier release in six weeks. The cadence started with &lt;a href="https://datanorth.ai/news/google-releases-gemini-3-6-flash" rel="noopener noreferrer"&gt;Gemini 3.6 Flash on July 21, 2026&lt;/a&gt;, continued with &lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/introducing-gemini-3-7-flash/" rel="noopener noreferrer"&gt;Gemini 3.7 Flash on August 13, 2026&lt;/a&gt;, and now lands on 3.8 Flash and 3.8 Flash Cyber. Google describes 3.8 as building on the momentum of 3.7 Flash, released three weeks earlier, per the &lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/" rel="noopener noreferrer"&gt;announcement&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpkzzy6n8bmpflv6qbiwp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpkzzy6n8bmpflv6qbiwp.webp" alt="Gemini Flash Release Cadence" width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gemini 3.6 Flash launched alongside &lt;a href="https://datanorth.ai/news/google-releases-gemini-3-6-flash" rel="noopener noreferrer"&gt;Gemini 3.5 Flash Cyber&lt;/a&gt;, the model that 3.8 Flash Cyber now replaces. The cyber-focused line has been iterating on the same six-week schedule as the general-purpose Flash models.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in Gemini 3.8 Flash
&lt;/h2&gt;

&lt;p&gt;Google positions 3.8 Flash as its most intelligent workhorse model yet, with gains in software engineering, agentic tasks, and multi-step reasoning over 3.7 Flash, according to the &lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/" rel="noopener noreferrer"&gt;official announcement&lt;/a&gt;. On DeepSWE v1.1, a long-horizon software engineering benchmark, 3.8 Flash scores 73.7 percent, up from 65.3 percent for 3.7 Flash. That places it ahead of GPT-5.6 Sol, GPT-5.6 Terra, and Claude Sonnet 5 in Google's own comparison table, though Claude Opus 5 still leads the group at 74.0 percent.&lt;/p&gt;

&lt;p&gt;The model also gained ground on general reasoning and agentic coding. Gemini 3.8 Flash scores 54.9 percent on &lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/" rel="noopener noreferrer"&gt;HLE-Verified&lt;/a&gt;, a benchmark spanning STEM, humanities, and professional fields, edging out every model in the same table, including Claude Opus 5's 54.4 percent and GPT-5.6 Sol's 54.5 percent. On Terminal-bench 2.1, agentic terminal coding, 3.8 Flash posts 89.4 percent against 85.8 percent for 3.7 Flash.&lt;/p&gt;

&lt;p&gt;Google also credits 3.8 Flash with beating every other model in its comparison table on two specialized agent benchmarks. On &lt;a href="https://www.vals.ai/benchmarks/fabv2" rel="noopener noreferrer"&gt;Vals Finance Agent V2&lt;/a&gt;, 3.8 Flash scores 61.4 percent against 59.0 percent for 3.7 Flash and 58.6 percent for the next-best model, Claude Opus 5. On &lt;a href="https://www.vals.ai/benchmarks/hlab" rel="noopener noreferrer"&gt;Harvey's Legal Agent Benchmark&lt;/a&gt;, 3.8 Flash reaches a 10.0 percent full pass rate, roughly double GPT-5.6 Sol's 2.5 percent.&lt;/p&gt;

&lt;p&gt;Google attributes these gains to the model working harder: taking extra reasoning steps and calling tools iteratively on complex tasks, at the cost of more tokens at higher effort settings. Developers who need lower latency can drop the effort level, or stay on 3.7 Flash, which remains fully supported.&lt;/p&gt;

&lt;p&gt;Pricing stays where 3.7 Flash left it, for now. 3.8 Flash costs $0.75 per million input tokens and $3.75 per million output tokens through December 31, 2026, per the &lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/" rel="noopener noreferrer"&gt;pricing footnote&lt;/a&gt; on the announcement. Starting January 1, 2027, that rate doubles to $1.50 and $7.50 per million tokens, the same rate &lt;a href="https://mezha.ua/en/news/google-predstavila-gemini-3-6-flash-3-5-flash-lite-ta-3-5-flash-cyber-313466/amp/" rel="noopener noreferrer"&gt;3.6 Flash charged back in July&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Gemini 3.8 Flash stacks up against rivals
&lt;/h2&gt;

&lt;p&gt;Google's own announcement includes a head-to-head table against Gemini 3.7 Flash, Claude Opus 5, Claude Sonnet 5, GPT-5.6 Sol, and GPT-5.6 Terra across fifteen benchmarks. These are Google's self-reported numbers, so treat them the way you would any vendor's own comparison chart, pending independent replication.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benchmark&lt;/th&gt;
&lt;th&gt;Gemini 3.8 Flash&lt;/th&gt;
&lt;th&gt;Gemini 3.7 Flash&lt;/th&gt;
&lt;th&gt;Claude Opus 5&lt;/th&gt;
&lt;th&gt;Claude Sonnet 5&lt;/th&gt;
&lt;th&gt;GPT-5.6 Sol&lt;/th&gt;
&lt;th&gt;GPT-5.6 Terra&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input price, $ per 1M tokens&lt;/td&gt;
&lt;td&gt;$0.75*&lt;/td&gt;
&lt;td&gt;$0.75*&lt;/td&gt;
&lt;td&gt;$5.00&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;td&gt;$4.00&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output price, $ per 1M tokens&lt;/td&gt;
&lt;td&gt;$3.75*&lt;/td&gt;
&lt;td&gt;$3.75*&lt;/td&gt;
&lt;td&gt;$25.00&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;td&gt;$20.00&lt;/td&gt;
&lt;td&gt;$12.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSWE v1.1, long-horizon SWE&lt;/td&gt;
&lt;td&gt;73.7%&lt;/td&gt;
&lt;td&gt;65.3%&lt;/td&gt;
&lt;td&gt;74.0%&lt;/td&gt;
&lt;td&gt;53.8%&lt;/td&gt;
&lt;td&gt;72.7%&lt;/td&gt;
&lt;td&gt;69.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GDPVal-AA v2, knowledge work (Elo)&lt;/td&gt;
&lt;td&gt;1545&lt;/td&gt;
&lt;td&gt;1482&lt;/td&gt;
&lt;td&gt;1824&lt;/td&gt;
&lt;td&gt;1584&lt;/td&gt;
&lt;td&gt;1710&lt;/td&gt;
&lt;td&gt;1528&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vals Finance Agent v2&lt;/td&gt;
&lt;td&gt;61.4%&lt;/td&gt;
&lt;td&gt;59.0%&lt;/td&gt;
&lt;td&gt;58.6%&lt;/td&gt;
&lt;td&gt;53.9%&lt;/td&gt;
&lt;td&gt;53.8%&lt;/td&gt;
&lt;td&gt;54.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Harvey's Legal Agent Benchmark, full pass&lt;/td&gt;
&lt;td&gt;10.0%&lt;/td&gt;
&lt;td&gt;8.8%&lt;/td&gt;
&lt;td&gt;6.7%&lt;/td&gt;
&lt;td&gt;5.0%&lt;/td&gt;
&lt;td&gt;2.5%&lt;/td&gt;
&lt;td&gt;0.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminal-bench 2.1, agentic terminal coding&lt;/td&gt;
&lt;td&gt;89.4%&lt;/td&gt;
&lt;td&gt;85.8%&lt;/td&gt;
&lt;td&gt;89.1%&lt;/td&gt;
&lt;td&gt;80.4%&lt;/td&gt;
&lt;td&gt;88.8%&lt;/td&gt;
&lt;td&gt;87.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminal-bench 4.0, general agent&lt;/td&gt;
&lt;td&gt;19.1%&lt;/td&gt;
&lt;td&gt;11.2%&lt;/td&gt;
&lt;td&gt;51.8%&lt;/td&gt;
&lt;td&gt;12.4%&lt;/td&gt;
&lt;td&gt;37.3%&lt;/td&gt;
&lt;td&gt;23.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GDP.PDF, expert PDF comprehension, full pass&lt;/td&gt;
&lt;td&gt;35.0%&lt;/td&gt;
&lt;td&gt;34.0%&lt;/td&gt;
&lt;td&gt;37.0%&lt;/td&gt;
&lt;td&gt;28.0%&lt;/td&gt;
&lt;td&gt;40.0%&lt;/td&gt;
&lt;td&gt;29.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CharXiv Reasoning, no tools&lt;/td&gt;
&lt;td&gt;86.2%&lt;/td&gt;
&lt;td&gt;84.5%&lt;/td&gt;
&lt;td&gt;83.7%&lt;/td&gt;
&lt;td&gt;70.1%&lt;/td&gt;
&lt;td&gt;85.8%&lt;/td&gt;
&lt;td&gt;85.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LVBench, agentic / static, long video&lt;/td&gt;
&lt;td&gt;87.8% / 87.1%&lt;/td&gt;
&lt;td&gt;85.4%&lt;/td&gt;
&lt;td&gt;75.4%&lt;/td&gt;
&lt;td&gt;68.5%&lt;/td&gt;
&lt;td&gt;82.1%&lt;/td&gt;
&lt;td&gt;78.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HLE-Verified, multidisciplinary reasoning&lt;/td&gt;
&lt;td&gt;54.9%&lt;/td&gt;
&lt;td&gt;53.6%&lt;/td&gt;
&lt;td&gt;54.4%&lt;/td&gt;
&lt;td&gt;31.0%&lt;/td&gt;
&lt;td&gt;54.5%&lt;/td&gt;
&lt;td&gt;51.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSWorld-2.0, agentic computer use, partial&lt;/td&gt;
&lt;td&gt;59.0%&lt;/td&gt;
&lt;td&gt;50.6%&lt;/td&gt;
&lt;td&gt;75.4%&lt;/td&gt;
&lt;td&gt;42.6%&lt;/td&gt;
&lt;td&gt;62.6%&lt;/td&gt;
&lt;td&gt;50.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BioMysteryBench, human-solvable&lt;/td&gt;
&lt;td&gt;88.8%&lt;/td&gt;
&lt;td&gt;87.1%&lt;/td&gt;
&lt;td&gt;90.1%&lt;/td&gt;
&lt;td&gt;87.5%&lt;/td&gt;
&lt;td&gt;79.5%&lt;/td&gt;
&lt;td&gt;83.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BioMysteryBench, human-difficult&lt;/td&gt;
&lt;td&gt;56.5%&lt;/td&gt;
&lt;td&gt;43.5%&lt;/td&gt;
&lt;td&gt;49.4%&lt;/td&gt;
&lt;td&gt;34.1%&lt;/td&gt;
&lt;td&gt;44.7%&lt;/td&gt;
&lt;td&gt;49.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LABBench2, biology research tasks&lt;/td&gt;
&lt;td&gt;86.2%&lt;/td&gt;
&lt;td&gt;82.1%&lt;/td&gt;
&lt;td&gt;84.2%&lt;/td&gt;
&lt;td&gt;80.1%&lt;/td&gt;
&lt;td&gt;82.1%&lt;/td&gt;
&lt;td&gt;81.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;* Introductory price through Dec 31, 2026. Regular price is $1.50 input and $7.50 output per 1M tokens.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/" rel="noopener noreferrer"&gt;Google's Gemini 3.8 Flash announcement&lt;/a&gt;; methodology listed at deepmind.google/models/evals-methodology/gemini-3-8-flash.&lt;/p&gt;

&lt;p&gt;Claude Opus 5 leads on five of the fourteen benchmark rows: DeepSWE v1.1, GDPVal-AA v2, Terminal-bench 4.0, OSWorld-2.0, and the human-solvable split of BioMysteryBench. GPT-5.6 Sol leads on one, GDP.PDF. Gemini 3.8 Flash leads the remaining eight rows, and undercuts every rival on both price columns by a wide margin.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in Gemini 3.8 Flash Cyber, and who can use it
&lt;/h2&gt;

&lt;p&gt;Gemini 3.8 Flash Cyber is not a public model. Access runs through the newly launched &lt;a href="https://blog.google/innovation-and-ai/technology/safety-security/fairwind-program" rel="noopener noreferrer"&gt;Fairwind Program&lt;/a&gt;, which Google says prioritizes trusted government authorities, critical infrastructure operators, and software maintainers.&lt;/p&gt;

&lt;p&gt;On &lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/" rel="noopener noreferrer"&gt;CyberGym&lt;/a&gt;, the standard industry benchmark for vulnerability discovery, Google reports that 3.8 Flash Cyber delivers frontier-level performance, surpassing both 3.5 Flash Cyber and larger frontier models. Because CyberGym leans heavily on C and C++ codebases, Google also ran an &lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/" rel="noopener noreferrer"&gt;internal benchmark spanning 20 programming languages&lt;/a&gt;, where 3.8 Flash Cyber crossed a 70 percent vulnerability discovery success rate.&lt;/p&gt;

&lt;p&gt;Patching is where the numbers get most specific. On &lt;a href="https://cwe-bench.com/#leaderboard" rel="noopener noreferrer"&gt;CWE-Bench&lt;/a&gt;, an external benchmark run by Collinear, 3.8 Flash Cyber posted a pass@1 of 47.2 percent, just behind a leading frontier model's 47.8 percent, but at meaningfully lower cost per rollout, per &lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/" rel="noopener noreferrer"&gt;Google's writeup&lt;/a&gt; of the result. Google calls this landing on the Pareto frontier for the benchmark.&lt;/p&gt;

&lt;p&gt;According to the &lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/" rel="noopener noreferrer"&gt;primary announcement&lt;/a&gt;, real-world results reinforce the benchmark story. Google's own Chrome Security team found 3.8 Flash Cyber produced 2.6 times more correct vulnerability patches than larger commercial models. Wiz reported 7.5 to 9.7 percent higher recall on its internal penetration-testing benchmark, at 2.3 to 5.2 times lower cost than other leading frontier models. Google's Cloud Vulnerability Research team used the model to find a critical foundational vulnerability in under two hours, a process that normally takes months.&lt;/p&gt;

&lt;h2&gt;
  
  
  One core model, two access envelopes
&lt;/h2&gt;

&lt;p&gt;Both variants share the same foundational intelligence, refined through what Google calls long-running agentic loops that recursively evaluate and improve the underlying model. What separates them is the safety envelope wrapped around each release channel, not the underlying architecture.&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNplkU-P00AMxb-KmfMUhOCAckDqn213DyshwnJJe3AzTmIxGQeP01JW_e4ooVq04mj75_ee7GdXSyBXuCbKue5QDb5t9gkAYFmVHSoFaGRMAY0lYYReAkUPSg0nCnBiBGwpGddgipw4tRBFhnyAxeIzrKod9ZwYPrz9BNuIuTvc1Ofx-r8xrC9H0hu0mqFNtaETRRlIcwHLZNwqntguHpYPUNoYWDwsU1Dh8FKXxlZ3r4TuqrtkpINyplzAzfpf7xW8rdaS8tjPpjcUh2H2fJRAHsqOyLKHLyrvnqIpgqR4uYmsZ5FdtUXWM6cwUa1iX0wikev5nqD0c2SlcFvazUv31U5OpKmnNMnXysY1RuDUKGbTsbZRCaZ7oIlmD1kaO6MS9MjJkBNpPjjvetIeObji2VlH_fTnQA2O0Zz_2_mOyniMlCemkWRb7DleXOEWU05a5Es26j2sIqcfj1iXc72VZB72rqRWCJ4e9s7DVzmKiYd7iieaEntYKmP0kDHlRSblxvnZpOTfU5b3H4df7nr17tiuJYq6wr05d2zkrn8AuenfVg%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNplkU-P00AMxb-KmfMUhOCAckDqn213DyshwnJJe3AzTmIxGQeP01JW_e4ooVq04mj75_ee7GdXSyBXuCbKue5QDb5t9gkAYFmVHSoFaGRMAY0lYYReAkUPSg0nCnBiBGwpGddgipw4tRBFhnyAxeIzrKod9ZwYPrz9BNuIuTvc1Ofx-r8xrC9H0hu0mqFNtaETRRlIcwHLZNwqntguHpYPUNoYWDwsU1Dh8FKXxlZ3r4TuqrtkpINyplzAzfpf7xW8rdaS8tjPpjcUh2H2fJRAHsqOyLKHLyrvnqIpgqR4uYmsZ5FdtUXWM6cwUa1iX0wikev5nqD0c2SlcFvazUv31U5OpKmnNMnXysY1RuDUKGbTsbZRCaZ7oIlmD1kaO6MS9MjJkBNpPjjvetIeObji2VlH_fTnQA2O0Zz_2_mOyniMlCemkWRb7DleXOEWU05a5Es26j2sIqcfj1iXc72VZB72rqRWCJ4e9s7DVzmKiYd7iieaEntYKmP0kDHlRSblxvnZpOTfU5b3H4df7nr17tiuJYq6wr05d2zkrn8AuenfVg%3Ftype%3Dpng" alt="Mermaid Diagram" width="1197" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;3.8 Flash ships broadly across developer, enterprise, and consumer surfaces. 3.8 Flash Cyber ships to a narrow, vetted list of defenders, reflecting the dual-use risk of a model built to find and patch vulnerabilities at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Gemini 3.7 Flash&lt;/th&gt;
&lt;th&gt;Gemini 3.8 Flash&lt;/th&gt;
&lt;th&gt;Gemini 3.8 Flash Cyber&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Release date&lt;/td&gt;
&lt;td&gt;Aug 13, 2026&lt;/td&gt;
&lt;td&gt;Sep 2, 2026&lt;/td&gt;
&lt;td&gt;Sep 2, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access&lt;/td&gt;
&lt;td&gt;General availability&lt;/td&gt;
&lt;td&gt;General availability&lt;/td&gt;
&lt;td&gt;Fairwind Program only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price, input / output per 1M tokens&lt;/td&gt;
&lt;td&gt;$0.75 / $3.75&lt;/td&gt;
&lt;td&gt;$0.75 / $3.75 through Dec 31, 2026&lt;/td&gt;
&lt;td&gt;Not publicly listed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSWE v1.1&lt;/td&gt;
&lt;td&gt;65.3%&lt;/td&gt;
&lt;td&gt;73.7%&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminal-bench 2.1&lt;/td&gt;
&lt;td&gt;85.8%&lt;/td&gt;
&lt;td&gt;89.4%&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HLE-Verified&lt;/td&gt;
&lt;td&gt;53.6%&lt;/td&gt;
&lt;td&gt;54.9%&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CyberGym, vulnerability discovery&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Frontier-level, beats 3.5 Flash Cyber&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CWE-Bench patching, pass@1&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;47.2%, vs 47.8% for top frontier model&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source: &lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/" rel="noopener noreferrer"&gt;blog.google&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safety notes worth reading
&lt;/h2&gt;

&lt;p&gt;3.8 Flash ships with safeguards against misuse in chemical, biological, radiological, and nuclear domains, plus cyber offense, under Google's Frontier Safety Framework, per the &lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/" rel="noopener noreferrer"&gt;announcement&lt;/a&gt;. 3.8 Flash Cyber carries a more permissive set of mitigations, which is why it stays behind the Fairwind Program gate rather than shipping broadly.&lt;/p&gt;

&lt;p&gt;Google also reports a significant jump in prompt injection robustness for the 3.8 generation, measured against Gray Swan's indirect prompt injection benchmark. The company frames this as protection for everyday Gemini users against prompt-injection attacks, not only a cyber-specialist concern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to find it
&lt;/h2&gt;

&lt;p&gt;Developers reach 3.8 Flash through Google Antigravity, Google AI Studio, and Android Studio, or generate interfaces with it in Stitch. Enterprises get access through Gemini Enterprise. Consumers on a Google AI Pro or Ultra plan can reach it inside the Gemini app, AI Mode in Search, or Gemini in Sheets.&lt;/p&gt;

&lt;p&gt;3.8 Flash Cyber only reaches people who apply through the &lt;a href="https://blog.google/innovation-and-ai/technology/safety-security/fairwind-program" rel="noopener noreferrer"&gt;Fairwind Program&lt;/a&gt; and get accepted as a trusted defender. Google lists early Fairwind partners including Armadin, Palo Alto Networks, Snowflake, and Wiz in the &lt;a href="https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/" rel="noopener noreferrer"&gt;announcement&lt;/a&gt;, though it has not published the program's exact acceptance criteria.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern to watch
&lt;/h2&gt;

&lt;p&gt;Three Flash-tier releases in six weeks is a fast cadence for any model family, and it says as much about Google's release strategy as about any single model. If the pattern holds, 3.8 Flash will not be the last word here for long. Developers building on Gemini should treat these version numbers as a moving target and budget review time whenever a new one drops.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/gemini-3-8-flash-and-3-8-flash-cyber-google-ships-its-third-flash-release-in-six-weeks-csgem" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>gemini</category>
      <category>llmbenchmarks</category>
      <category>cybersecurity</category>
      <category>developertools</category>
    </item>
    <item>
      <title>GitOps for Technical Writers: Continuous Publishing with the ZyVop CLI and GitHub Actions</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Wed, 02 Sep 2026 13:57:59 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/gitops-for-technical-writers-continuous-publishing-with-the-zyvop-cli-and-github-actions-3605</link>
      <guid>https://dev.to/sanjay_singh_1/gitops-for-technical-writers-continuous-publishing-with-the-zyvop-cli-and-github-actions-3605</guid>
      <description>&lt;h1&gt;
  
  
  GitOps for Technical Writers: Continuous Publishing with the ZyVop CLI and GitHub Actions
&lt;/h1&gt;

&lt;p&gt;For software engineers, writing code and writing technical articles should feel like the same discipline. Both require structural hierarchy, precise syntax, logical proofs, and iterative refinement. Yet the developer experience of publishing an article has historically diverged from our software development lifecycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Traditional Publishing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="s"&gt;Local Editor (Markdown) ──&amp;gt; Copy/Paste ──&amp;gt; Web CMS Dashboard ──&amp;gt; Manual Formatting ──&amp;gt; Publish ──&amp;gt; Repeat for 4 Platforms&lt;/span&gt;

&lt;span class="na"&gt;GitOps Publishing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="s"&gt;Local Editor (Markdown) ──&amp;gt; git commit &amp;amp; push ──&amp;gt; GitHub Actions (CI/CD) ──&amp;gt; ZyVop API ──&amp;gt; Automated Multi-Platform Fanout&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When writing in a proprietary browser-based CMS, we surrender the tools we rely on daily:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No Real Version Control:&lt;/strong&gt; Revisions are stored in proprietary database snapshots rather than immutable Git commits with atomic diffs and clear commit messages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No Peer Review Infrastructure:&lt;/strong&gt; Collaboration happens via clunky comment sidebars instead of standard GitHub Pull Requests, branch previews, and automated linting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context Switching:&lt;/strong&gt; We are forced out of our configured local environments (Neovim, VS Code, Helix) into browser textareas with fragile clipboard handling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Manual Multi-Platform Duplication:&lt;/strong&gt; Distributing an article to Dev.to, Hashnode, Medium, and Bluesky means manually copy-pasting Markdown, re-uploading cover images, re-tagging, and hoping canonical URLs were configured correctly to avoid search engine penalties.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To solve this, we applied the principles of &lt;strong&gt;GitOps&lt;/strong&gt; and &lt;strong&gt;Continuous Delivery&lt;/strong&gt; to technical blogging. By combining the ZyVop CLI, headless REST/GraphQL APIs, and GitHub Actions, you can manage your blog as an open-source repository and automate the entire lifecycle from local Markdown file to globally distributed, SEO-canonicalized publication.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. System Architecture: The End-to-End Pipeline
&lt;/h2&gt;

&lt;p&gt;At its core, GitOps publishing treats a directory of Markdown files as the &lt;strong&gt;single source of truth&lt;/strong&gt; for your published technical content. A &lt;code&gt;git push&lt;/code&gt; to your repository's &lt;code&gt;main&lt;/code&gt; branch acts as the deployment trigger.&lt;/p&gt;

&lt;p&gt;Here is the architectural lifecycle of an article moving through the pipeline:&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNp1lN1uIkcQhV-lMhdedhdMHOVqLpD4MyYCLQbsVSJLUdFTQIWertnuGjBZ-d2jHgZ7FzlXo-7-TtdR1en5nhjJKEmTQN9KcoYGjBuP-ZMDwFLFlfmKfLUyKh4GtAcM8UNWCvLQmIhBC-PB8GOkCvTKhgt0CiPWyI5Y78oVzKmQwCr-eMn1xz9gXaMsLsC8dO5U-Ee0O6vYv46PUsCsXFk2MB8ulvHgkh30IjqToBtPi_sJDFBxhYEuwfuSSopsr7R2eg9_yOq0dwl-Fb8jX5FodhsvpcvqzXDJDp-1btS1CrThDsPWSUbQhillXObQhp4tKeyOTy6KB7RvdToj1hQ2rFCUYQviecMOcmQHjUKChnbOxksgv2dD4TrPqq6PWFudTn-cwtLzZkMeDuJ3aysHEHe6qlGgbkMKH07XfPp0nWcfKnF_XGv7WzI7KRX866jgCjJSMgq5ZLxmymDNlsKrrjsbpzD7slhCGwtu72_aVQ8sBWj0CD15WMqOHHyGKfpdJgcHMzxawZPz7uz1lke0nKFSLbiCGfpA8Gd3OoFbL05zVCUP3cXyTTnopfBQBPJaTRqGTlmj7b7kOSv00YnjmNApKWao-KatZpzC0H2rAhCOLmODMX7wj6xixyqfKcSWjd_81u367dcb6HtCpew0HAi23MAVWN4TPMwn5-5Gm7RPzwn_eh7NojSGKKMscpWZVqdzilMKfXGhzH92pRh2dc7gFl0rzmrAQT2vygjEM6gD2ep0hs-angMYH07jwLoFc-7I36W3H9-TvEZ15LHY3k9qIYc5FfHJhS1l7wrrYL9T6-F_StUvALpLmHlRMWJr6ZzNdknPCms0pKFSk8uSZpKTz5GzJP2e6Jby-PPKaI2l1aR52nlEz7iyFCKzFqe3mLM9JmnSwqKw1ArHoJQ3oWfZ7aZoFtX6Vpw24SlZ0EYIHsZPSRPmshKVJtyR3ZOywSZ0PaNtQkAXWoE8r5NmVWTB_0YvN78Xz8nLSzNZbfpixSdp8sthy0rJy3-Jctf3%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNp1lN1uIkcQhV-lMhdedhdMHOVqLpD4MyYCLQbsVSJLUdFTQIWertnuGjBZ-d2jHgZ7FzlXo-7-TtdR1en5nhjJKEmTQN9KcoYGjBuP-ZMDwFLFlfmKfLUyKh4GtAcM8UNWCvLQmIhBC-PB8GOkCvTKhgt0CiPWyI5Y78oVzKmQwCr-eMn1xz9gXaMsLsC8dO5U-Ee0O6vYv46PUsCsXFk2MB8ulvHgkh30IjqToBtPi_sJDFBxhYEuwfuSSopsr7R2eg9_yOq0dwl-Fb8jX5FodhsvpcvqzXDJDp-1btS1CrThDsPWSUbQhillXObQhp4tKeyOTy6KB7RvdToj1hQ2rFCUYQviecMOcmQHjUKChnbOxksgv2dD4TrPqq6PWFudTn-cwtLzZkMeDuJ3aysHEHe6qlGgbkMKH07XfPp0nWcfKnF_XGv7WzI7KRX866jgCjJSMgq5ZLxmymDNlsKrrjsbpzD7slhCGwtu72_aVQ8sBWj0CD15WMqOHHyGKfpdJgcHMzxawZPz7uz1lke0nKFSLbiCGfpA8Gd3OoFbL05zVCUP3cXyTTnopfBQBPJaTRqGTlmj7b7kOSv00YnjmNApKWao-KatZpzC0H2rAhCOLmODMX7wj6xixyqfKcSWjd_81u367dcb6HtCpew0HAi23MAVWN4TPMwn5-5Gm7RPzwn_eh7NojSGKKMscpWZVqdzilMKfXGhzH92pRh2dc7gFl0rzmrAQT2vygjEM6gD2ep0hs-angMYH07jwLoFc-7I36W3H9-TvEZ15LHY3k9qIYc5FfHJhS1l7wrrYL9T6-F_StUvALpLmHlRMWJr6ZzNdknPCms0pKFSk8uSZpKTz5GzJP2e6Jby-PPKaI2l1aR52nlEz7iyFCKzFqe3mLM9JmnSwqKw1ArHoJQ3oWfZ7aZoFtX6Vpw24SlZ0EYIHsZPSRPmshKVJtyR3ZOywSZ0PaNtQkAXWoE8r5NmVWTB_0YvN78Xz8nLSzNZbfpixSdp8sthy0rJy3-Jctf3%3Ftype%3Dpng" alt="Mermaid Diagram" width="1904" height="653"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Architectural Tenets:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Decoupled Synchronous Validation:&lt;/strong&gt; The GitHub Actions runner communicates with the fast REST endpoint, which validates frontmatter, stores the post in PostgreSQL, and returns within ~250ms.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Asynchronous Multi-Platform Fan-Out:&lt;/strong&gt; External platform APIs (which often experience variable latency or strict rate limits) are handled by dedicated background BullMQ workers. This guarantees that slow third-party networks never fail or block your CI/CD build.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deterministic Canonical SEO:&lt;/strong&gt; The root post URL is automatically computed and injected into the metadata headers of all syndication targets, ensuring Google and Bing attribute domain authority to your primary source.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. The Anatomy of a Headless Markdown Post
&lt;/h2&gt;

&lt;p&gt;In a GitOps workflow, your Markdown files must declare both their &lt;strong&gt;content&lt;/strong&gt; and their &lt;strong&gt;deployment configuration&lt;/strong&gt;. We use standard YAML frontmatter parsed at the AST level via &lt;code&gt;gray-matter&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is an example production post configuration (&lt;code&gt;posts/distributed-queues.md&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Anatomy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Resilient&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Distributed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Task&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Queue"&lt;/span&gt;
&lt;span class="na"&gt;subtitle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dive&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;into&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;NestJS,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Fastify,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;BullMQ,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Redis&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;worker&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;processes"&lt;/span&gt;
&lt;span class="na"&gt;excerpt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Learn&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;how&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;architect&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;high-throughput&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;asynchronous&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;job&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pipelines&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;that&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;survive&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;network&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;partitions&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;crashes."&lt;/span&gt;
&lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;typescript&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;architecture&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;devops&lt;/span&gt;
&lt;span class="na"&gt;canonical_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://myblog.com/posts/distributed-queues&lt;/span&gt;
&lt;span class="na"&gt;cover_image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://assets.myblog.com/covers/task-queue.webp&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PUBLISHED&lt;/span&gt;
&lt;span class="na"&gt;generate_toc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;cross_post&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;devto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;hashnode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;medium&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;bluesky&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Introduction&lt;/span&gt;

When designing scalable web architectures, separating synchronous request-response cycles from background task execution is essential...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Frontmatter Schema Reference
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;title&lt;/td&gt;
&lt;td&gt;string (Required)&lt;/td&gt;
&lt;td&gt;The primary headline of the article.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;subtitle&lt;/td&gt;
&lt;td&gt;string (Optional)&lt;/td&gt;
&lt;td&gt;Secondary description or tagline.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;excerpt&lt;/td&gt;
&lt;td&gt;string (Optional)&lt;/td&gt;
&lt;td&gt;Short summary used for RSS feeds, newsletter preheaders, and preview cards.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tags&lt;/td&gt;
&lt;td&gt;string[]&lt;/td&gt;
&lt;td&gt;Up to 5 category tags (automatically mapped across syndication platforms).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;canonical_url&lt;/td&gt;
&lt;td&gt;string (Optional)&lt;/td&gt;
&lt;td&gt;Custom origin URL if you are syndicating from a personal standalone domain.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;status&lt;/td&gt;
&lt;td&gt;PUBLISHED | DRAFT&lt;/td&gt;
&lt;td&gt;When set to DRAFT, the post is created without triggering public feeds or syndication.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;generate_toc&lt;/td&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;Automatically calculates heading levels (&amp;lt;h2&amp;gt;, &amp;lt;h3&amp;gt;) and renders a floating Table of Contents.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cross_post&lt;/td&gt;
&lt;td&gt;object&lt;/td&gt;
&lt;td&gt;Boolean flags dictating which downstream syndication adapters should run.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  3. Inside the ZyVop CLI: AST Parsing &amp;amp; Token Authentication
&lt;/h2&gt;

&lt;p&gt;The ZyVop CLI was designed with two modes of execution:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Interactive Developer Mode:&lt;/strong&gt; For local terminal testing with real-time spinners (&lt;code&gt;ora&lt;/code&gt;), colored diff logs (&lt;code&gt;picocolors&lt;/code&gt;), and session validation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Headless CI/CD Mode:&lt;/strong&gt; For non-interactive runners utilizing Personal Access Tokens (&lt;code&gt;zv_...&lt;/code&gt;) passed via environment variables.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  How the CLI Parses and Dispatches
&lt;/h3&gt;

&lt;p&gt;When you execute &lt;code&gt;npx zyvop publish ./posts/my-article.md&lt;/code&gt;, the CLI performs the following operations:&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="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;matter&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gray-matter&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;marked&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;marked&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;publishArticleRestApi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../api.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;publishCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolvedPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;filePath&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;rawFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolvedPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 1. Extract frontmatter and raw Markdown AST&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;matter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawFile&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;frontmatter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Resolve authentication credentials&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ZYVOP_TOKEN&lt;/span&gt; &lt;span class="o"&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;token&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;endpoint&lt;/span&gt; &lt;span class="o"&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;endpoint&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.zyvop.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Dispatch to the Headless REST Endpoint&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;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zv_&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&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;publishArticleRestApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`✅ Live URL: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="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;
  
  
  The AST Code Fence Protection Challenge
&lt;/h3&gt;

&lt;p&gt;One significant technical hurdle when converting Markdown for multi-platform delivery is &lt;strong&gt;nested code fences&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If an article includes Markdown tutorials illustrating triple-backtick fences (&lt;code&gt;&lt;/code&gt;`&lt;code&gt;&lt;/code&gt;), naive Markdown parsers misinterpret closing boundaries. Furthermore, platforms like Dev.to's Forem engine treat nested triple-backticks as Liquid template syntax errors, throwing unhandled exceptions such as &lt;code&gt;"Unknown tag 'endraw'"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The cross-posting engine dynamically calculates the fence depth:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;typescript&lt;br&gt;
this.turndown.addRule('fencedCodeBlock', {&lt;br&gt;
  filter: ['pre'],&lt;br&gt;
  replacement: (_content: any, node: any) =&amp;gt; {&lt;br&gt;
    const code = node.querySelector ? node.querySelector('code') : null;&lt;br&gt;
    const text = code ? code.textContent : node.textContent;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Dynamically increase fence length if the content contains triple backticks
let fence = '```';
while (text.includes(fence)) {
  fence += '`';
}
return `\n\n${fence}${lang}\n${text}\n${fence}\n\n`;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;},&lt;br&gt;
});&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
This ensures that your code snippets—no matter how complex or nested—remain syntactically intact across all platforms.

* * *

## 4\. Setting Up the Production GitHub Actions Workflow

To achieve true GitOps publishing, we don't want to re-publish every single post on every commit. We only want to publish **new or modified Markdown files** in the current push.

Here is the production-ready GitHub Actions workflow.

### File: `.github/workflows/publish.yml`

```

yaml
name: Continuous Publishing (GitOps)

on:
  push:
    branches:
      - main
    paths:
      - 'posts/**.md'
  workflow_dispatch: # Allows manual trigger from the GitHub Actions UI

concurrency:
  group: publishing-${{ github.ref }}
  cancel-in-progress: false

jobs:
  publish-articles:
    name: Validate &amp;amp; Publish to ZyVop
    runs-on: ubuntu-latest

    steps:
      - name: 📥 Checkout Repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 2 # Fetch the previous commit for accurate git diffing

      - name: ⚙️ Setup Node.js Runtime
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: 🔍 Detect Changed Markdown Posts
        id: changed-files
        run: |
          # If initial commit or forced push, fallback to all posts
          if [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then
            FILES=$(git ls-files 'posts/*.md')
          else
            FILES=$(git diff --name-only --diff-filter=ACMR ${{ github.event.before }} ${{ github.sha }} | grep '^posts/.*\.md$' || true)
          fi

          if [ -z "$FILES" ]; then
            echo "No Markdown files modified."
            echo "has_changes=false" &amp;gt;&amp;gt; $GITHUB_OUTPUT
          else
            echo "Files to publish:"
            echo "$FILES"
            # Format files into a space-separated list
            FILES_CLEAN=$(echo "$FILES" | tr '\n' ' ')
            echo "files=$FILES_CLEAN" &amp;gt;&amp;gt; $GITHUB_OUTPUT
            echo "has_changes=true" &amp;gt;&amp;gt; $GITHUB_OUTPUT
          fi

      - name: 🚀 Run ZyVop CLI Publisher
        if: steps.changed-files.outputs.has_changes == 'true'
        env:
          ZYVOP_TOKEN: ${{ secrets.ZYVOP_TOKEN }}
        run: |
          for file in ${{ steps.changed-files.outputs.files }}; do
            if [ -f "$file" ]; then
              echo "──────────────────────────────────────────────"
              echo "📦 Deploying: $file"
              npx zyvop publish "$file"
            fi
          done

      - name: 📊 Summary Report
        if: steps.changed-files.outputs.has_changes == 'true'
        run: |
          echo "### 🚀 GitOps Publishing Complete" &amp;gt;&amp;gt; $GITHUB_STEP_SUMMARY
          echo "The following articles were verified and deployed:" &amp;gt;&amp;gt; $GITHUB_STEP_SUMMARY
          for file in ${{ steps.changed-files.outputs.files }}; do
            echo "- \`$file\`" &amp;gt;&amp;gt; $GITHUB_STEP_SUMMARY
          done



```

* * *

## 5\. Securing the Pipeline with Personal Access Tokens

Authentication in CI/CD pipelines requires zero interactive prompts. ZyVop uses cryptographically hashed **Developer Personal Access Tokens** (`zv_live_...`).

![Mermaid Diagram](https://mermaid.ink/img/pako:eNqFklFv2jAUhf_K3X3YU0jVrQ8bQkhpRwGNrhFhSGsyIce5EK-OHdlOWij898kBsW5StSdfXx37O_fYL8h1QdjHtdRPvGTGwWyeKQDb5BvD6hJmmjMJaYaXIXyhlqSuycAd46VQlOFPLwaIo0Wa4ZgUGeYIFvqRFAgFCTkn1MYOcnMxHHjWcNeupGhp9Xn9iYdhOLjouqebSBWZ-os_nnj4hxDGwk2aHOZUayucNtszPCFuyKUZjhQ329pRcWq9oj78WN7Hq8X919G3_xHnjVJkPPXjmRpxJ7SyZ-RItWmGU_WLuMeNVCuMVhWp18zaaE7WhqTa8E0-wM1smmb4sF3q2tdv-krItEdfVyEc5VE8PV8TNa5MM1wyKQr_BgnXNVl4D3O_m4lKuD_-4yaXwnr9qYJYW_cvOo4W0OsN91FRgNPnx4ThKV-7PxVefKw6_TEXYM5n6URFe5-QF41U6xXdnKob_QhoXKmN2DGfch-uiRkysGtXYRjuu8m82q_d6ZNnDLAiUzFRYP8FXUmV_8kFrVkjHQbHzpIZwXJJ1mvWWrlbVgm5xT72WF1L6tmtdVQFcC2FerxjPOn2t1q5ADJMaKMJvk8zDGCuc-10ABOSLTnBWQCREUwGYJmyPUtGrDHoIInYeS-XV_UzHg4B5psbLbXBPr57KoUjPPwGR8Yolw?type=png)

### Steps to Configure:

1.  Log in to your **Dashboard** and navigate to **Settings &amp;gt; Developer API**.

2.  Click **Generate New Token**, assign a descriptive name (e.g., `github-actions-blog`), and copy the generated key.

3.  In your GitHub repository, navigate to **Settings &amp;gt; Secrets and variables &amp;gt; Actions**.

4.  Click **New repository secret**, set the name to `ZYVOP_TOKEN`, and paste the token string.


* * *

## 6\. Engineering Best Practices for Repository Structure

When managing your publication as code, organizing your directory structure helps maintain readability and simplifies pre-commit validations.

### Recommended Repository Layout:

```

yaml
my-tech-blog/
├── .github/
│   └── workflows/
│       ├── publish.yml          # Automated deployment pipeline
│       └── lint.yml             # Pre-merge validation (Markdownlint, CSpell)
├── .markdownlint.json           # Style consistency rules
├── posts/
│   ├── 2026-08-20-distributed-queues.md
│   ├── 2026-08-22-at-protocol-internals.md
│   └── drafts/
│       └── upcoming-raft-consensus.md
├── static/
│   └── diagrams/
│       └── queue-architecture.png
└── README.md



```

### Pre-Merge Quality Gates (Pull Request Workflow)

Before an article is merged into `main`, you can enforce the same quality checks you use on software projects:

-   **Markdown Linting (**`markdownlint-cli2`**):** Ensures heading hierarchies are semantically correct (e.g., single `h1`, no skipped header levels).

-   **Spell Checking (**`cspell`**):** Catches typographical errors and unknown terminology before publication.

-   **Link Validation (**`lychee`**):** Verifies that all outbound references, documentation links, and image URLs are reachable and return HTTP 200.


```

yaml
# .github/workflows/lint.yml
name: Content Verification

on:
  pull_request:
    paths:
      - 'posts/**.md'

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Check Markdown formatting
        uses: DavidAnson/markdownlint-cli2-action@v16
        with:
          globs: 'posts/**/*.md'
      - name: Check spelling
        uses: streetsidesoftware/cspell-action@v6
        with:
          files: 'posts/**/*.md'



```

* * *

## 7\. Summary &amp;amp; Getting Started

By shifting technical blogging to a GitOps workflow:

-   **Your content stays in your hands:** You own the raw Markdown files, version history, and branch reviews in your repository.

-   **You write where you are productive:** No more pasting between browser tabs. Stay in your terminal, IDE, and Git workflow.

-   **Continuous syndication happens automatically:** One `git push` simultaneously publishes your post across Dev.to, Hashnode, Medium, and Bluesky, while maintaining your canonical SEO ranking.


To test publishing an article directly from your terminal:

```

bash
# 1. Login to your account
npx zyvop login

# 2. Test publishing any local Markdown file
npx zyvop publish ./posts/my-article.md



```



---

*Originally published on [ZyVOP](https://zyvop.com/gitops-for-technical-writers-continuous-publishing-with-the-zyvop-cli-and-github-actions-fb690)*

💡 For more articles like this, [subscribe to the ZyVOP newsletter](https://zyvop.com/newsletter)!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>gitops</category>
      <category>githubactions</category>
      <category>cicd</category>
      <category>automation</category>
    </item>
    <item>
      <title>Claude Fable 5.1 and Mythos 5.1: One Model, Two Access Levels</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Wed, 02 Sep 2026 06:03:43 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/claude-fable-51-and-mythos-51-one-model-two-access-levels-2gga</link>
      <guid>https://dev.to/sanjay_singh_1/claude-fable-51-and-mythos-51-one-model-two-access-levels-2gga</guid>
      <description>&lt;p&gt;Anthropic &lt;a href="https://www.anthropic.com/claude-fable-and-mythos-5-1" rel="noopener noreferrer"&gt;released Claude Fable 5.1 and Claude Mythos 5.1&lt;/a&gt; on September 1.&lt;/p&gt;

&lt;p&gt;Calling them two models is slightly misleading.&lt;/p&gt;

&lt;p&gt;Underneath, &lt;strong&gt;Fable 5.1 and Mythos 5.1 are the same model&lt;/strong&gt;. The difference is what Anthropic allows that model to do, and who gets access when the work crosses into cybersecurity or advanced life sciences.&lt;/p&gt;

&lt;p&gt;Fable is the commercial release. Mythos is the less-restricted version behind a verification gate.&lt;/p&gt;

&lt;p&gt;That makes this launch more interesting than the usual frontier-model cycle of benchmark charts, coding demos, and claims about better reasoning. Anthropic is effectively productizing two things separately: &lt;strong&gt;capability and permission&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And there is a reason for that.&lt;/p&gt;

&lt;p&gt;Earlier this year, the first Fable and Mythos release became entangled with a U.S. government intervention after researchers demonstrated that the public model could be pushed further into vulnerability research than regulators were comfortable with.&lt;/p&gt;

&lt;p&gt;So while 5.1 is faster, cheaper in some workloads, and apparently better at coding and science, a large part of the release is really about cleaning up the boundary between useful capability and capability Anthropic does not want available by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same weights, different guardrails
&lt;/h2&gt;

&lt;p&gt;Fable 5.1 is available through the usual Anthropic channels: Claude.ai, the API, Claude Code, Claude Cowork, and major cloud providers.&lt;/p&gt;

&lt;p&gt;It runs with Anthropic's standard production safeguards.&lt;/p&gt;

&lt;p&gt;Mythos 5.1 uses the same underlying model but loosens some of those restrictions in two areas where the line between legitimate research and dangerous use is unusually thin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;cybersecurity&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;life sciences&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A model that is unusually good at finding software vulnerabilities is useful to a security team trying to harden infrastructure.&lt;/p&gt;

&lt;p&gt;The same capability can also help someone weaponize a vulnerability.&lt;/p&gt;

&lt;p&gt;A model capable of reasoning deeply about protein design can accelerate legitimate drug research. The capability becomes much harder to release casually once the same system begins approaching work that previously required specialized laboratories and expert teams.&lt;/p&gt;

&lt;p&gt;Anthropic's answer is not to cripple the underlying model. It is to gate the more permissive version.&lt;/p&gt;

&lt;p&gt;The company had already been moving in this direction before 5.1.&lt;/p&gt;

&lt;p&gt;Earlier in 2026, &lt;a href="https://www.anthropic.com/project/glasswing" rel="noopener noreferrer"&gt;Project Glasswing&lt;/a&gt; gave organizations including AWS, Microsoft, Google, Apple, Cisco, CrowdStrike, and JPMorganChase access to an unreleased system called Claude Mythos Preview.&lt;/p&gt;

&lt;p&gt;Anthropic said that model could hunt down serious software vulnerabilities — including flaws affecting major operating systems and browsers — at a level approaching elite human security researchers.&lt;/p&gt;

&lt;p&gt;Instead of shipping that capability directly into Claude, Anthropic put it in the hands of vetted defensive partners first and backed the program with up to $100 million in usage credits.&lt;/p&gt;

&lt;p&gt;At the time, it looked like an unusually cautious research preview.&lt;/p&gt;

&lt;p&gt;With Mythos 5.1, it now looks more like the blueprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then June happened
&lt;/h2&gt;

&lt;p&gt;The first branded Fable 5 and Mythos 5 models launched on June 9.&lt;/p&gt;

&lt;p&gt;Three days later, both disappeared.&lt;/p&gt;

&lt;p&gt;On June 12, the U.S. Department of Commerce ordered Anthropic to prevent foreign nationals from accessing the models under national-security authorities. Because Anthropic had no practical way to verify the nationality of every user in real time, it &lt;a href="https://www.anthropic.com/news/fable-mythos-access" rel="noopener noreferrer"&gt;suspended access globally&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The episode reportedly followed research from Amazon showing that Fable 5 could be jailbroken into identifying known vulnerabilities and, in one case, producing exploit code.&lt;/p&gt;

&lt;p&gt;Anthropic's position was that the jailbreak was narrow rather than universal, and that the government's response was disproportionate.&lt;/p&gt;

&lt;p&gt;That distinction mattered less to the regulator than it did to Anthropic.&lt;/p&gt;

&lt;p&gt;Mythos returned first, on June 26, for approved critical-infrastructure organizations. The broader order was lifted on June 30. Fable returned to general availability on July 1. Anthropic published a detailed &lt;a href="https://www.anthropic.com/news/redeploying-fable-5" rel="noopener noreferrer"&gt;timeline and explanation of the redeployment&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That brief shutdown exposed a problem frontier-model developers are going to keep running into: &lt;strong&gt;a vendor's internal definition of acceptable risk is not necessarily the definition regulators will use&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Anthropic can classify a jailbreak as narrow, non-universal, or below some internal severity threshold. A government agency looking at the same capability can still decide it has crossed a line.&lt;/p&gt;

&lt;p&gt;Fable 5.1 is, in part, Anthropic's attempt to make that line less ambiguous.&lt;/p&gt;

&lt;p&gt;Cybersecurity interventions in Claude Code are now down roughly 60% per session, according to the company. Fable is allowed to identify vulnerabilities for defensive purposes, but more aggressive activity — including exploit generation and certain forms of binary vulnerability analysis — remains restricted or routed elsewhere.&lt;/p&gt;

&lt;p&gt;Anthropic has also published more detail on its &lt;a href="https://www.anthropic.com/news/fable-safeguards-jailbreak-framework" rel="noopener noreferrer"&gt;cyber safeguards and jailbreak-severity framework&lt;/a&gt;, which tries to distinguish minor bypasses from genuinely dangerous jailbreaks.&lt;/p&gt;

&lt;p&gt;Biology restrictions have also been narrowed. Anthropic says ordinary medical and basic biology questions trigger safeguards around 85% less often than before, following an August update to &lt;a href="https://www.anthropic.com/news/improving-fable-5-s-biology-safeguards" rel="noopener noreferrer"&gt;Fable's biology safeguards&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The direction is clear: fewer annoying false positives for normal users, while the genuinely sensitive capability moves behind a more explicit gate.&lt;/p&gt;

&lt;p&gt;That is a harder engineering problem than simply making the model refuse more often.&lt;/p&gt;

&lt;h2&gt;
  
  
  The coding gains look real. The benchmark story is less interesting.
&lt;/h2&gt;

&lt;p&gt;Anthropic reports improvements across coding, research, and general knowledge workloads.&lt;/p&gt;

&lt;p&gt;On GDPval-AA v2:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fable 5.1&lt;/td&gt;
&lt;td&gt;1,853&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5&lt;/td&gt;
&lt;td&gt;1,824&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fable 5&lt;/td&gt;
&lt;td&gt;1,723&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On Terminal-Bench 4.0, the difference between the public and gated variants becomes particularly useful:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mythos 5.1&lt;/td&gt;
&lt;td&gt;60.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fable 5.1&lt;/td&gt;
&lt;td&gt;55.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Anthropic attributes much of that gap to cases where Fable's safety layer intervenes. The company publishes the broader &lt;a href="https://www.anthropic.com/claude-fable-and-mythos-5-1" rel="noopener noreferrer"&gt;benchmark table and evaluation notes&lt;/a&gt; alongside the release.&lt;/p&gt;

&lt;p&gt;That is arguably more informative than another leaderboard win. It gives us a rough idea of the performance tax imposed by policy rather than model intelligence.&lt;/p&gt;

&lt;p&gt;Anthropic also reports Fable 5.1 ahead of GPT-5.6 Sol on the benchmarks where it published results for both models, although vendor-run comparisons deserve the usual caveat: benchmark selection, prompts, effort settings, tool configuration, and evaluation harnesses can all change the shape of the result.&lt;/p&gt;

&lt;p&gt;The more useful signal comes from workloads outside the standard tables.&lt;/p&gt;

&lt;p&gt;At Millennium, a senior portfolio manager described a system crash that appeared roughly once in a million runs and had resisted investigation for years. Fable 5.1 reportedly worked backward through a third-party binary, correlated its behavior with crash data, and isolated the bug inside the dependency.&lt;/p&gt;

&lt;p&gt;That is the kind of task where agentic models are becoming genuinely interesting: not autocomplete, not another greenfield React app, but messy diagnosis across binaries, logs, tools, and incomplete evidence.&lt;/p&gt;

&lt;p&gt;Cognition says it is moving Devin traffic from Opus 5 to Fable 5.1 because of the price-to-performance tradeoff. MongoDB described a prototype that ran unattended for hours and finished overnight. Canva reported improvements in writing and creative work.&lt;/p&gt;

&lt;p&gt;These are still vendor-selected customer stories, not controlled independent evaluations. But they point toward the workload Anthropic seems to be optimizing for: &lt;strong&gt;long-running, tool-heavy tasks where the model has to stay coherent across a large amount of context&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That leads to one of the more practical changes in the release.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pricing table barely changed. The agent economics did.
&lt;/h2&gt;

&lt;p&gt;Base pricing remains:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Usage&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;$10 / million tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;$50 / million tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cached-context reads&lt;/td&gt;
&lt;td&gt;$0.25 / million tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important number is the last one.&lt;/p&gt;

&lt;p&gt;Cached reads previously cost $1 per million tokens. Fable 5.1 cuts that to $0.25 — a 75% reduction.&lt;/p&gt;

&lt;p&gt;For ordinary chat, that may not matter much.&lt;/p&gt;

&lt;p&gt;For agents, it can matter a lot.&lt;/p&gt;

&lt;p&gt;A coding agent rarely reads a repository once. It repeatedly revisits system instructions, source files, conversation state, tool output, plans, and previous context over dozens or hundreds of turns.&lt;/p&gt;

&lt;p&gt;Suppose an agent carries a 50,000-token codebase context and re-reads it during 100 tool-call turns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;50,000 × 100 = 5,000,000 cached tokens

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the previous cache-read rate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5M × $1 / 1M = $5.00

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the new rate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5M × $0.25 / 1M = $1.25

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run 50 sessions like that per day:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cache rate&lt;/th&gt;
&lt;th&gt;Daily cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Previous rate&lt;/td&gt;
&lt;td&gt;$250.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fable 5.1&lt;/td&gt;
&lt;td&gt;$62.50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Over a 30-day month, the difference is roughly &lt;strong&gt;$5,625 on cache reads alone&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That number is more useful than Anthropic's headline claim of 25–45% lower workload costs because teams can plug their own cache-hit patterns into the same arithmetic.&lt;/p&gt;

&lt;p&gt;An agent that constantly churns fresh context will save less.&lt;/p&gt;

&lt;p&gt;One that repeatedly works over the same large repository may save considerably more.&lt;/p&gt;

&lt;p&gt;For production teams, &lt;strong&gt;context reuse is becoming an architectural cost variable&lt;/strong&gt;, not just an API optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Science is where Anthropic is making the biggest bets
&lt;/h2&gt;

&lt;p&gt;The most aggressive claims in the release have little to do with software engineering.&lt;/p&gt;

&lt;p&gt;Anthropic says Mythos 5.1 used open-source protein-design tools to produce drug-binder candidates that bound roughly &lt;strong&gt;10× more tightly&lt;/strong&gt; than the best public entries in &lt;a href="https://proteinbase.com/competitions" rel="noopener noreferrer"&gt;Adaptyv Bio's protein-design competitions&lt;/a&gt; for three targets.&lt;/p&gt;

&lt;p&gt;Across 12 targets, it reports a hit rate approaching &lt;strong&gt;50%&lt;/strong&gt;, compared with a typical 10–15% range in the field.&lt;/p&gt;

&lt;p&gt;Those numbers are large enough that independent replication matters far more than another internal benchmark.&lt;/p&gt;

&lt;p&gt;The Venus work is unusual for a different reason.&lt;/p&gt;

&lt;p&gt;Fable 5.1 reportedly trained a neural network over decades-old Magellan radar observations to produce an elevation map covering roughly one-third of the planet, taking effective resolution from around 10–20 km to 2–3 km.&lt;/p&gt;

&lt;p&gt;Anthropic has &lt;a href="https://zenodo.org/records/22164484" rel="noopener noreferrer"&gt;released the Venus elevation-map artifact on Zenodo&lt;/a&gt; under a Creative Commons license ahead of upcoming NASA and ESA Venus missions.&lt;/p&gt;

&lt;p&gt;Mythos also reportedly generated custom GPU kernels for seven open-source genomics models, producing speedups of up to 2.5× while preserving identical outputs. Anthropic estimates that could cut the cost of some large-scale genetic analyses by 30–60%.&lt;/p&gt;

&lt;p&gt;Any one of these results would be interesting on its own.&lt;/p&gt;

&lt;p&gt;Together, they explain why Anthropic is uncomfortable treating Mythos like another premium subscription tier.&lt;/p&gt;

&lt;p&gt;Once the same general-purpose model is debugging binaries, searching for vulnerabilities, designing proteins, optimizing genomics workloads, and building planetary models, the question is no longer just whether the system is more capable than the previous release.&lt;/p&gt;

&lt;p&gt;The question becomes: &lt;strong&gt;which capabilities should inherit normal API access?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Mythos is starting to look less like a product tier
&lt;/h2&gt;

&lt;p&gt;Anthropic says &lt;a href="https://www.anthropic.com/claude/mythos" rel="noopener noreferrer"&gt;Mythos 5.1&lt;/a&gt; will remain available only through vetted programs.&lt;/p&gt;

&lt;p&gt;The Cyber Verification Program covers approved security work and is expanding toward Mythos-class access.&lt;/p&gt;

&lt;p&gt;A separate Life Sciences Verification Program, developed with the U.S. government, handles advanced biology research.&lt;/p&gt;

&lt;p&gt;For now, access is limited to a relatively small set of U.S. organizations, although Anthropic says international expansion is planned.&lt;/p&gt;

&lt;p&gt;This is a very different model from the normal software ladder.&lt;/p&gt;

&lt;p&gt;Haiku, Sonnet, and Opus are primarily products differentiated by capability, latency, and cost.&lt;/p&gt;

&lt;p&gt;Mythos is increasingly something else: &lt;strong&gt;a policy boundary around the frontier checkpoint&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That boundary may become more common across the industry.&lt;/p&gt;

&lt;p&gt;The more general these systems become, the harder it is to create a clean separation between "cyber model," "biology model," "coding model," and "research model." The underlying model can increasingly do all of those things.&lt;/p&gt;

&lt;p&gt;So access control moves upward.&lt;/p&gt;

&lt;p&gt;Instead of asking which specialized model a user can access, providers begin asking which parts of a general model's capability envelope that user should be allowed to exercise.&lt;/p&gt;

&lt;p&gt;That is effectively what Anthropic is testing here.&lt;/p&gt;

&lt;p&gt;There is another small but telling change in the same direction.&lt;/p&gt;

&lt;p&gt;Anthropic has published an explanation of &lt;a href="https://www.anthropic.com/news/claude-text-watermark" rel="noopener noreferrer"&gt;how Claude's text watermark works&lt;/a&gt;. The change sits alongside the EU AI Act's &lt;a href="https://digital-strategy.ec.europa.eu/en/policies/code-practice-ai-generated-content" rel="noopener noreferrer"&gt;Code of Practice on Transparency of AI-Generated Content&lt;/a&gt;, whose Article 50-related transparency obligations began applying on August 2, 2026.&lt;/p&gt;

&lt;p&gt;Anthropic says the watermark does not encode information about the user or conversation and does not affect output quality. Detection tooling is initially being made available to regulators, researchers, media organizations, and companies with compliance obligations.&lt;/p&gt;

&lt;p&gt;Individually, that may look like a compliance footnote.&lt;/p&gt;

&lt;p&gt;Placed next to verification programs, reduced-safeguard access tiers, government coordination, jailbreak evaluations, and gated biological capability, it fits the larger pattern.&lt;/p&gt;

&lt;p&gt;Anthropic is no longer treating model deployment as simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;train model → test model → expose API

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stack is becoming closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;train
→ evaluate capability
→ classify risk
→ choose safeguards
→ verify user
→ expose capability
→ monitor use

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That infrastructure may end up being as consequential as the model itself.&lt;/p&gt;

&lt;p&gt;Fable 5.1 will get most of the usage because it is the version developers can actually deploy.&lt;/p&gt;

&lt;p&gt;Mythos 5.1 is the more revealing release.&lt;/p&gt;

&lt;p&gt;It shows what happens when a frontier lab decides that the strongest version of a general-purpose model is no longer something everyone should automatically receive just because the weights are ready.&lt;/p&gt;

&lt;p&gt;The benchmark race is still happening.&lt;/p&gt;

&lt;p&gt;But another race has started beside it: &lt;strong&gt;who can build a workable control plane for models whose useful and dangerous capabilities increasingly come from the same underlying intelligence?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After June, Anthropic has learned that it will not be the only party deciding where that line sits.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/claude-fable-5-1-and-mythos-5-1-one-model-two-access-levels-2lm3e" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>aisafety</category>
      <category>anthropic</category>
      <category>fable51</category>
      <category>mythos51</category>
    </item>
    <item>
      <title>Apple's Mac Studio Is Becoming an AI Workstation as Enterprise AI Demand Grows</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Tue, 01 Sep 2026 04:40:47 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/apples-mac-studio-is-becoming-an-ai-workstation-as-enterprise-ai-demand-grows-115m</link>
      <guid>https://dev.to/sanjay_singh_1/apples-mac-studio-is-becoming-an-ai-workstation-as-enterprise-ai-demand-grows-115m</guid>
      <description>&lt;p&gt;Apple's August 25 Mac announcement was unusual.&lt;/p&gt;

&lt;p&gt;The company introduced new Mac mini and Mac Studio systems earlier than expected, and reporting from &lt;strong&gt;The Information&lt;/strong&gt;, relayed by &lt;a href="https://www.macrumors.com/2026/08/30/apple-unexpected-mac-mini-and-studio-demand/" rel="noopener noreferrer"&gt;MacRumors&lt;/a&gt;, suggests unexpectedly strong enterprise demand for Macs capable of running AI workloads locally helped influence the timing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.reuters.com/business/retail-consumer/apple-launches-faster-mac-mini-mac-studio-tap-ai-boom-2026-08-25/" rel="noopener noreferrer"&gt;Reuters&lt;/a&gt; separately reported that Apple launched the new desktops amid growing demand for AI-capable hardware.&lt;/p&gt;

&lt;p&gt;Apple has not publicly confirmed that enterprise demand dictated the timing.&lt;/p&gt;

&lt;p&gt;What it has confirmed is more important: the Mac is being positioned far more aggressively as an &lt;strong&gt;on-device AI platform&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The new &lt;a href="https://www.apple.com/newsroom/2026/08/apple-introduces-new-mac-studio-with-m5-max-and-m5-ultra/" rel="noopener noreferrer"&gt;Mac Studio with M5 Max and M5 Ultra&lt;/a&gt; is explicitly aimed at demanding AI workloads, while the Mac mini is being pushed toward local models and agentic computing.&lt;/p&gt;

&lt;p&gt;The Mac Studio is no longer simply a workstation for developers, designers, and video professionals.&lt;/p&gt;

&lt;p&gt;It is becoming a serious desktop AI machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 512GB Advantage
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;M5 Ultra&lt;/strong&gt; is where Apple's strategy becomes clearest.&lt;/p&gt;

&lt;p&gt;At the top end, Mac Studio offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;36-core CPU&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;80-core GPU&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Up to 512GB of unified memory&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;1.2TB/s memory bandwidth&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apple also adds Neural Accelerators to every GPU core and claims up to &lt;strong&gt;4.3× the peak AI compute performance of M3 Ultra&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That performance figure is Apple's own benchmark claim.&lt;/p&gt;

&lt;p&gt;For local AI, the more consequential number may be &lt;strong&gt;512GB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Consider a 70-billion-parameter model stored at 16-bit precision:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;70 billion × 2 bytes = roughly 140GB&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And that only accounts for the weights.&lt;/p&gt;

&lt;p&gt;Inference also needs memory for the KV cache, context, runtime overhead, and other working data.&lt;/p&gt;

&lt;p&gt;This is where Apple's unified-memory architecture becomes unusually useful.&lt;/p&gt;

&lt;p&gt;Instead of maintaining separate pools of system RAM and comparatively limited GPU VRAM, Apple silicon allows the CPU and GPU to operate from the same large memory pool.&lt;/p&gt;

&lt;p&gt;That can make workloads practical on a single machine that might otherwise require aggressive quantization, model sharding, CPU offloading, or multiple discrete GPUs.&lt;/p&gt;

&lt;p&gt;But there is an important distinction:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More memory does not automatically mean more tokens per second.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A smaller-memory accelerator may still be considerably faster when a model fits comfortably inside its available memory.&lt;/p&gt;

&lt;p&gt;Apple's advantage is different.&lt;/p&gt;

&lt;p&gt;It is increasingly about &lt;strong&gt;how large an AI workload can fit inside one desktop&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Apple says the new Mac Studio will support up to 512GB of unified memory, although the &lt;strong&gt;512GB configuration is scheduled to arrive in late October&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mac Mini Is Following the Same Strategy
&lt;/h2&gt;

&lt;p&gt;Apple is bringing the same idea downmarket with the &lt;a href="https://www.apple.com/newsroom/2026/08/apple-unveils-a-more-powerful-mac-mini-featuring-the-all-new-m6-and-m5-pro/" rel="noopener noreferrer"&gt;new Mac mini&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The M6 model supports up to &lt;strong&gt;32GB of unified memory&lt;/strong&gt; and &lt;strong&gt;170GB/s of memory bandwidth&lt;/strong&gt;, while integrating Neural Accelerators into its GPU.&lt;/p&gt;

&lt;p&gt;For heavier AI development, the M5 Pro configuration is more interesting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;64GB unified memory307GB/s memory bandwidth&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That creates a clear hierarchy:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;M6 Mac mini:&lt;/strong&gt; lightweight local AI and agentic workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;M5 Pro Mac mini:&lt;/strong&gt; larger models and professional AI development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;M5 Ultra Mac Studio:&lt;/strong&gt; high-memory inference and very large local models.&lt;/p&gt;

&lt;p&gt;Mac mini remains Apple's entry-level desktop.&lt;/p&gt;

&lt;p&gt;But it is increasingly also the entry point into Apple's local-AI stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Businesses Care About Local AI
&lt;/h2&gt;

&lt;p&gt;The attraction of local inference is straightforward.&lt;/p&gt;

&lt;p&gt;For some organizations, sending proprietary information to an external AI service is undesirable or impractical.&lt;/p&gt;

&lt;p&gt;Local models can be useful when working with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Confidential source code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Proprietary documents&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Internal research&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Customer information&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sensitive datasets&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Regulated workloads&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also latency and cost considerations.&lt;/p&gt;

&lt;p&gt;Cloud inference remains easier to scale and avoids a large upfront hardware purchase. For many workloads, it will continue to be the better option.&lt;/p&gt;

&lt;p&gt;Local hardware shifts the economics from &lt;strong&gt;per-call cloud spending toward upfront infrastructure and ongoing operating costs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For teams continuously experimenting with coding agents, retrieval systems, open-weight models, or private datasets, that trade-off can become attractive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mac Studio vs. NVIDIA DGX Spark
&lt;/h2&gt;

&lt;p&gt;A natural comparison is NVIDIA's &lt;a href="https://www.nvidia.com/en-us/products/workstations/dgx-spark/" rel="noopener noreferrer"&gt;DGX Spark&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Built around NVIDIA's GB10 Grace Blackwell platform, DGX Spark offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;128GB coherent unified system memory&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;273GB/s memory bandwidth&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Up to 1 PFLOP of FP4 Tensor performance&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Support for local inference with models up to &lt;strong&gt;200 billion parameters&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The comparison is striking.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Specification&lt;/th&gt;
&lt;th&gt;Mac Studio M5 Ultra&lt;/th&gt;
&lt;th&gt;NVIDIA DGX Spark&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Maximum memory&lt;/td&gt;
&lt;td&gt;512GB&lt;/td&gt;
&lt;td&gt;128GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory bandwidth&lt;/td&gt;
&lt;td&gt;1.2TB/s&lt;/td&gt;
&lt;td&gt;273GB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;M5 Ultra / Apple silicon&lt;/td&gt;
&lt;td&gt;GB10 Grace Blackwell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Published AI claim&lt;/td&gt;
&lt;td&gt;Up to 4.3× vs. M3 Ultra*&lt;/td&gt;
&lt;td&gt;Up to 1 PFLOP FP4**&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Core AI ecosystem&lt;/td&gt;
&lt;td&gt;MLX, Metal, Core AI&lt;/td&gt;
&lt;td&gt;CUDA, NVIDIA AI stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Positioning&lt;/td&gt;
&lt;td&gt;Large local models + general workstation&lt;/td&gt;
&lt;td&gt;Purpose-built AI development&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;  Apple's figure compares M5 Ultra with M3 Ultra.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;** NVIDIA's figure refers to theoretical FP4 Tensor performance using sparsity.&lt;/p&gt;

&lt;p&gt;These performance claims are &lt;strong&gt;not directly comparable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The maximum Mac Studio configuration has four times DGX Spark's memory capacity and roughly 4.4 times its published memory bandwidth.&lt;/p&gt;

&lt;p&gt;That does not make it four times faster.&lt;/p&gt;

&lt;p&gt;The platforms use fundamentally different accelerator architectures, numerical formats, frameworks, and optimization stacks.&lt;/p&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which constraint matters more — memory capacity or software compatibility?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  NVIDIA Still Owns the Ecosystem Advantage
&lt;/h2&gt;

&lt;p&gt;If Apple's strongest argument is memory, NVIDIA's strongest argument remains &lt;strong&gt;CUDA&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;CUDA is deeply embedded across AI research and production. Optimized kernels, inference engines, training libraries, and deployment systems frequently assume NVIDIA hardware.&lt;/p&gt;

&lt;p&gt;DGX Spark therefore fits into an ecosystem that stretches from a developer's desk to cloud GPUs and large data-center systems.&lt;/p&gt;

&lt;p&gt;Apple's AI software ecosystem is much younger than CUDA's.&lt;/p&gt;

&lt;p&gt;Its counterargument is integration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ml-explore/mlx" rel="noopener noreferrer"&gt;MLX&lt;/a&gt; is Apple's open-source machine-learning framework designed specifically around Apple silicon and its unified-memory architecture.&lt;/p&gt;

&lt;p&gt;Apple's newer &lt;a href="https://developer.apple.com/core-ai/" rel="noopener noreferrer"&gt;Core AI&lt;/a&gt; framework provides native tools for building and running AI models on-device across Apple silicon.&lt;/p&gt;

&lt;p&gt;Together with Metal, macOS, and Apple's broader developer tooling, they create a different kind of AI workstation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A development machine, general-purpose computer, and large-memory local AI system in one box.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For some developers, that combination may matter more than CUDA compatibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apple Is Thinking Beyond One Mac
&lt;/h2&gt;

&lt;p&gt;Perhaps the most interesting change is that Apple is no longer treating Mac Studio strictly as a single-machine system.&lt;/p&gt;

&lt;p&gt;Multiple Mac Studios can be connected through &lt;strong&gt;Thunderbolt 5 and RDMA&lt;/strong&gt; for distributed AI workloads.&lt;/p&gt;

&lt;p&gt;In Apple's testing, a four-Mac Studio cluster delivered up to &lt;strong&gt;3× the AI inference performance of a single system&lt;/strong&gt;.&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNp9kMFugkAQhl9lOtcuh7b2QhoTBE1N5CJtL8JhYQfZdNk1y1Jr1XdvgJrixeM_35f5J3PEwghCH0tl9kXFrYPVOtUAAMFmxe2WIFhCbASpDDxvCrNNzAtIXCukgYfsT-1ROEaPVygao6crNB-jSZbqAc7gxfOmp7eq1YJsbpSDZ7iHdRQHJwgHJ7zlRIMT3XLm_23dKYtNJBtnZd46ErDUJVnSBWWXtt65rB2F-RCQYU225lKgf0RXUd09VlDJW-WQDZMPbiXPFTWdUxrtFryW6oA-eny3U-Q1h8ZRzWCmpP6MeZH0eWG0Y5BiQltD8L5MkcHa5MYZBq-kvsjJgjMIrOSKQcN14zVkZYmsL0nkT3fLw2T3jeczw3wbGmUs-ni3r6QjPP8COZ-eLg%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNp9kMFugkAQhl9lOtcuh7b2QhoTBE1N5CJtL8JhYQfZdNk1y1Jr1XdvgJrixeM_35f5J3PEwghCH0tl9kXFrYPVOtUAAMFmxe2WIFhCbASpDDxvCrNNzAtIXCukgYfsT-1ROEaPVygao6crNB-jSZbqAc7gxfOmp7eq1YJsbpSDZ7iHdRQHJwgHJ7zlRIMT3XLm_23dKYtNJBtnZd46ErDUJVnSBWWXtt65rB2F-RCQYU225lKgf0RXUd09VlDJW-WQDZMPbiXPFTWdUxrtFryW6oA-eny3U-Q1h8ZRzWCmpP6MeZH0eWG0Y5BiQltD8L5MkcHa5MYZBq-kvsjJgjMIrOSKQcN14zVkZYmsL0nkT3fLw2T3jeczw3wbGmUs-ni3r6QjPP8COZ-eLg%3Ftype%3Dpng" alt="Mermaid Diagram" width="1607" height="295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Distributed inference is not equivalent to placing several GPUs inside one server.&lt;/p&gt;

&lt;p&gt;Communication between machines introduces overhead, and real performance depends heavily on the model, framework, and implementation.&lt;/p&gt;

&lt;p&gt;But the direction matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One Mac Studio is a workstation. Several interconnected Mac Studios begin to resemble a small AI cluster.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is a very different conversation from the one Apple was having about this machine only a few years ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Does a $5,499 Mac Make Sense?
&lt;/h2&gt;

&lt;p&gt;Mac Studio with M5 Max starts at &lt;strong&gt;$2,499&lt;/strong&gt;, while M5 Ultra starts at &lt;strong&gt;$5,499&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A $5,499 machine makes little sense if the goal is occasionally running a small local model.&lt;/p&gt;

&lt;p&gt;But that is not necessarily the workload Apple is targeting.&lt;/p&gt;

&lt;p&gt;For teams running large private models regularly, the comparison becomes broader:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mac Studio versus the infrastructure required to achieve the same result another way.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That could involve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Multiple GPUs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;High-capacity system memory&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A dedicated inference server&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Networking&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Storage&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Recurring cloud inference costs&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The economics depend entirely on the workload.&lt;/p&gt;

&lt;p&gt;For some teams, NVIDIA hardware or cloud inference will remain the obvious choice.&lt;/p&gt;

&lt;p&gt;For others, a compact machine capable of keeping extremely large models local could be surprisingly attractive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mac Studio or DGX Spark?
&lt;/h2&gt;

&lt;p&gt;The decision ultimately comes down to priorities.&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%2Fmermaid.ink%2Fimg%2Fpako%3AeNpNkcGO2jAQhl_lr8-OqkrthUOrkJTdSIVDU7ZdQQ5DMhALJxPZZmkKvHtl9hCOM_r9fTOei6qlYTVTeyvnuiUX8Cvf9gCQbrJWxDMIP6Qmi7TAb3FHHygY6SskyVfML4VHts5T1NINFMzOWBNG1M4EU5P9dtv277h5zF9f2V-RbVYvRV6kyJ_-oBzIHavHzEquyCP3jd0IS-7AsPcJOu7EjQgtY3BGnAnjJMgnwffNkmqU4dQYwfIL1jY4qh5j0bG4rJgbEA7csyObDCc3xIXj4yAyoRcT-umO7kxv8BGTpXoMRvjzJosf4hi1-ADqG5zFHa1QUymtOnYdmUbNLiq03MULNLynkw1Kv3deyBnaWfYxs5c-LKgzdlQzldAwWE786AN3GnNr-uOS6vJeL6QPGltV8kEY62KrNH7KToJoPLN943gWjdQZshqeep94dmav9F1Smn9xlk-fh7_qdtNqd8jEilMz9eHcmsDq9h-ab7nW%3Ftype%3Dpng" 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%2Fmermaid.ink%2Fimg%2Fpako%3AeNpNkcGO2jAQhl_lr8-OqkrthUOrkJTdSIVDU7ZdQQ5DMhALJxPZZmkKvHtl9hCOM_r9fTOei6qlYTVTeyvnuiUX8Cvf9gCQbrJWxDMIP6Qmi7TAb3FHHygY6SskyVfML4VHts5T1NINFMzOWBNG1M4EU5P9dtv277h5zF9f2V-RbVYvRV6kyJ_-oBzIHavHzEquyCP3jd0IS-7AsPcJOu7EjQgtY3BGnAnjJMgnwffNkmqU4dQYwfIL1jY4qh5j0bG4rJgbEA7csyObDCc3xIXj4yAyoRcT-umO7kxv8BGTpXoMRvjzJosf4hi1-ADqG5zFHa1QUymtOnYdmUbNLiq03MULNLynkw1Kv3deyBnaWfYxs5c-LKgzdlQzldAwWE786AN3GnNr-uOS6vJeL6QPGltV8kEY62KrNH7KToJoPLN943gWjdQZshqeep94dmav9F1Smn9xlk-fh7_qdtNqd8jEilMz9eHcmsDq9h-ab7nW%3Ftype%3Dpng" alt="Mermaid Diagram" width="772" height="1162"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choose &lt;strong&gt;DGX Spark&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;CUDA compatibility is critical.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your existing AI stack already depends on NVIDIA.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You rely on CUDA-specific libraries or optimized kernels.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Workloads are likely to move to NVIDIA servers or cloud GPUs.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choose a &lt;strong&gt;high-memory Mac Studio&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Memory capacity is the primary constraint.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You want to experiment with very large open-weight models locally.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;macOS is already part of your development workflow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You want one system for both general development and AI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Local processing and data control are important.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither machine is universally better.&lt;/p&gt;

&lt;p&gt;They optimize for different constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Shift
&lt;/h2&gt;

&lt;p&gt;The most important development here is not Apple's latest benchmark.&lt;/p&gt;

&lt;p&gt;It is the change in what the Mac represents.&lt;/p&gt;

&lt;p&gt;For years, the standard answer to a demanding AI workload was simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run it in the cloud.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now another question is becoming increasingly practical:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can we keep it local?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The M5 Ultra Mac Studio will offer up to &lt;strong&gt;512GB of unified memory and 1.2TB/s of bandwidth&lt;/strong&gt; in a compact desktop.&lt;/p&gt;

&lt;p&gt;DGX Spark offers a different proposition: less memory, but direct access to CUDA and NVIDIA's much larger AI ecosystem.&lt;/p&gt;

&lt;p&gt;Apple does not need to beat NVIDIA at every benchmark.&lt;/p&gt;

&lt;p&gt;It needs to make local AI on Apple silicon capable and convenient enough that developers and businesses begin treating Macs as part of their AI infrastructure.&lt;/p&gt;

&lt;p&gt;The reported enterprise demand suggests that shift may already be underway.&lt;/p&gt;

&lt;p&gt;And that is more significant than any single benchmark number.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/apple-s-mac-studio-is-becoming-an-ai-workstation-as-enterprise-ai-demand-grows-y3djf" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>ondeviceai</category>
      <category>macstudio</category>
      <category>macmini</category>
      <category>localllminference</category>
    </item>
    <item>
      <title>REST vs gRPC vs GraphQL in NestJS: What the Numbers Actually Show</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Sun, 30 Aug 2026 06:17:14 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/rest-vs-grpc-vs-graphql-in-nestjs-what-the-numbers-actually-show-2koe</link>
      <guid>https://dev.to/sanjay_singh_1/rest-vs-grpc-vs-graphql-in-nestjs-what-the-numbers-actually-show-2koe</guid>
      <description>&lt;p&gt;Comparisons of these three protocols tend to cite specific performance multipliers without saying much about what was actually measured. &lt;a href="https://dasroot.net/posts/2026/04/graphql-vs-rest-vs-grpc-api-architecture-comparison-2026/" rel="noopener noreferrer"&gt;One recent writeup&lt;/a&gt; put gRPC's median latency at roughly a third of REST's for the same operation. I wanted to know if that held up in the stack I actually ship — NestJS, on Node — so I built the same &lt;code&gt;GET /users&lt;/code&gt;-shaped endpoint three times: once as a REST controller, once as a GraphQL resolver, once as a gRPC service. Same 100-record dataset, same machine, same load test harness as far as the transport allows.&lt;/p&gt;

&lt;p&gt;REST won — and not narrowly. REST served roughly 2.8-3x &lt;em&gt;more&lt;/em&gt; requests per second than gRPC on my machine, almost the exact inverse of the ratio quoted above. That's not me disputing the other measurement; it's a different stack, different data, different network conditions, and both can be true. The reasons why are more useful than either number. This post is the benchmark, the code, and what I'd actually pick and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question everyone asks in the wrong order
&lt;/h2&gt;

&lt;p&gt;Most REST vs gRPC vs GraphQL comparisons start with the protocol and work backward to your problem: here are three boxes, here's what's inside each one, pick a box. That's backward. The protocol is a consequence of decisions you've usually already made — who's calling this API, how many services sit behind it, and what your actual performance budget is — not an input to them.&lt;/p&gt;

&lt;p&gt;So instead of another feature matrix, I built the same thing three ways and measured it. Not because benchmarks settle the argument — they don't, a synthetic localhost test is not your production traffic — but because "gRPC is faster" is a claim you can actually check, and checking it surfaces the real trade-offs better than restating them.&lt;/p&gt;

&lt;h2&gt;
  
  
  REST, gRPC, and GraphQL in 30 seconds each
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;REST&lt;/strong&gt; sends and receives JSON over plain HTTP. Every resource is a URL, every operation is a verb, and the server always returns the full shape of whatever it decides a "user" is. It's the default because there's nothing to explain — any client, in any language, on any platform, can call it with nothing but an HTTP library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;gRPC&lt;/strong&gt; replaces JSON with Protocol Buffers, a binary format, and plain HTTP/1.1 with HTTP/2. Every message is defined in a &lt;code&gt;.proto&lt;/code&gt; schema up front, both sides generate strongly-typed client and server code from it, and calls run over persistent, multiplexed connections instead of a new request each time. It's built for service-to-service traffic where both ends are code you control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GraphQL&lt;/strong&gt; keeps JSON and HTTP but replaces fixed endpoints with a single query language: the client states exactly which fields it wants, and the server resolves only those fields. It trades REST's simplicity for the ability to shape the response per request — which matters most when one screen needs data assembled from several underlying sources.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually built and measured
&lt;/h2&gt;

&lt;p&gt;I built one dataset — 100 users, each with a nested address (street, city, zip) — and served it identically through three servers: Express for REST, &lt;a href="https://the-guild.dev/graphql/yoga-server" rel="noopener noreferrer"&gt;graphql-yoga&lt;/a&gt; for GraphQL, and &lt;code&gt;@grpc/grpc-js&lt;/code&gt; with a hand-written &lt;code&gt;.proto&lt;/code&gt; schema for gRPC. All three read from the same in-memory array, so none of them do any real work beyond serializing and sending it — this measures transport and serialization overhead, not database time.&lt;/p&gt;

&lt;p&gt;For load testing, REST and GraphQL got hit with &lt;a href="https://github.com/mcollina/autocannon" rel="noopener noreferrer"&gt;autocannon&lt;/a&gt; — 10 concurrent connections, 10 seconds, keep-alive. Autocannon doesn't speak gRPC, so I wrote a matching harness: 10 concurrent workers calling the gRPC client in a loop for the same 10 seconds, timing each call with &lt;code&gt;process.hrtime&lt;/code&gt;. Payload sizes came from a separate, deterministic measurement — no load test needed, just encoding the identical 100-record dataset as JSON versus Protobuf and comparing byte counts directly.&lt;/p&gt;

&lt;p&gt;Everything ran on one machine, Node 22, localhost — meaning zero network latency, no TLS handshake cost, and no cross-language client. That matters for reading the results honestly, and I'll come back to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payload size: the protobuf number that surprised me
&lt;/h2&gt;

&lt;p&gt;The pitch for Protobuf is that binary encoding beats JSON's text encoding by a wide margin. Here's what encoding the same 100 users actually produced:&lt;/p&gt;

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

&lt;p&gt;Protobuf came in at 8,922 bytes against REST's 16,415 — smaller, but by 1.84x, not the 3-11x figure that gets thrown around. I re-ran the same comparison at 2,000 records to check whether the ratio widens at scale: it didn't. Protobuf stayed at 1.81x smaller. That's because this dataset is mostly strings — names, emails, addresses — and Protobuf's real advantage is in numeric fields, where varint encoding beats JSON's decimal text representation by a much wider margin, and in not repeating field names on every record. A schema that's mostly IDs, timestamps, and enums will show a bigger gap than one that's mostly text. Know your data before you trust a generic ratio.&lt;/p&gt;

&lt;p&gt;The bigger number on that chart is the one nobody markets: GraphQL asking for only &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt; came back at 2,704 bytes — 83.5% smaller than asking for everything. That's not a protocol-level trick, it's just not fetching data you don't need, but it's the one place in this whole comparison where the savings are dramatic instead of incremental.&lt;/p&gt;

&lt;h2&gt;
  
  
  Throughput and latency: REST wins on my machine
&lt;/h2&gt;

&lt;p&gt;Here's what 10 concurrent connections hammering each server for 10 seconds actually produced:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhybo3w1ifpy734trxxy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhybo3w1ifpy734trxxy.webp" width="800" height="510"&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;Protocol&lt;/th&gt;
&lt;th&gt;100 records: req/s&lt;/th&gt;
&lt;th&gt;100 records: avg / p95 latency&lt;/th&gt;
&lt;th&gt;2,000 records: req/s&lt;/th&gt;
&lt;th&gt;2,000 records: avg / p95 latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;REST&lt;/td&gt;
&lt;td&gt;4,785&lt;/td&gt;
&lt;td&gt;1.6 ms / 6 ms&lt;/td&gt;
&lt;td&gt;515&lt;/td&gt;
&lt;td&gt;18.9 ms / 33 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gRPC&lt;/td&gt;
&lt;td&gt;1,712&lt;/td&gt;
&lt;td&gt;5.8 ms / 15 ms&lt;/td&gt;
&lt;td&gt;168&lt;/td&gt;
&lt;td&gt;59.5 ms / 82 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GraphQL&lt;/td&gt;
&lt;td&gt;1,624&lt;/td&gt;
&lt;td&gt;5.7 ms / 14 ms&lt;/td&gt;
&lt;td&gt;133&lt;/td&gt;
&lt;td&gt;74.4 ms / 95 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;REST beat gRPC by roughly 2.8x at 100 records and 3.1x at 2,000. Three honest reasons, not one:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@grpc/grpc-js&lt;/code&gt; &lt;strong&gt;is pure JavaScript.&lt;/strong&gt; The performance case for gRPC is usually built on Go, C++, or Java implementations that call into a native core library. Node's official gRPC client is a pure-JS reimplementation — it doesn't get that native speed, and the well-known cross-language benchmarks don't transfer to it automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Express serving static JSON is about as close to the metal as Node gets.&lt;/strong&gt; There's no query parsing, no schema validation, no protobuf encode step — just &lt;code&gt;JSON.stringify&lt;/code&gt; and a socket write. It's a low bar for the other two to clear, and in this test, neither did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero network latency erases gRPC's actual advantage.&lt;/strong&gt; gRPC's HTTP/2 multiplexing and binary framing pay off when you're crossing a real network with real round-trip time and many concurrent streams — that's the internal-microservices case it's built for. On localhost, there's no latency to hide behind, so the picture flips.&lt;/p&gt;

&lt;p&gt;The 2,000-record run adds one more data point worth noting: gRPC's lead over GraphQL widened from a 5% edge to a 26% edge as payload size grew. GraphQL's per-field resolver execution scales with the number of fields resolved; gRPC's schema-fixed encode/decode scales more predictably. Neither caught REST.&lt;/p&gt;

&lt;p&gt;Take this for what a single-machine benchmark is: a controlled comparison of Node-specific implementations, not a verdict on the protocols themselves. If you're calling gRPC services written in Go from a Node gateway over a real network, expect a very different result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring up each transport in NestJS
&lt;/h2&gt;

&lt;p&gt;The benchmark used plain Express, graphql-yoga, and grpc-js directly — that's what let me measure transport overhead in isolation, since NestJS's own layer adds a small, roughly constant cost on top of all three. Here's the shape of the same endpoint idiomatically wired into NestJS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;REST&lt;/strong&gt; is a controller, nothing more:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// users.controller.ts&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsersController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersService&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="nd"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;findAll&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findAll&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;GraphQL&lt;/strong&gt;, code-first with &lt;code&gt;@nestjs/graphql&lt;/code&gt;, needs a resolver plus a typed 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="c1"&gt;// users.resolver.ts&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Resolver&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;User&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;class&lt;/span&gt; &lt;span class="nc"&gt;UsersResolver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersService&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="nd"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="nf"&gt;users&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findAll&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;// user.model.ts&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;ObjectType&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;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Field&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;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Field&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="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="nd"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;email&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;gRPC&lt;/strong&gt; is the one that needs real setup — a &lt;code&gt;.proto&lt;/code&gt; file, a microservice bootstrap, and &lt;code&gt;@GrpcMethod&lt;/code&gt; instead of a controller decorator, per &lt;a href="https://docs.nestjs.com/microservices/grpc" rel="noopener noreferrer"&gt;NestJS's own docs&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;NestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createMicroservice&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MicroserviceOptions&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;AppModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Transport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GRPC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;protoPath&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="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users.proto&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// users.controller.ts&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Controller&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;class&lt;/span&gt; &lt;span class="nc"&gt;UsersGrpcController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersService&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="nd"&gt;GrpcMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UserService&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ListUsers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;listUsers&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;users&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="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;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That extra setup is the real cost of gRPC in NestJS — not runtime speed, but a schema file, a codegen or dynamic-load step, and a client that has to inject a &lt;code&gt;ClientGrpc&lt;/code&gt; and call &lt;code&gt;getService()&lt;/code&gt; instead of just hitting a URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// api-gateway/users.controller.ts (calling the gRPC service from a REST gateway)&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GatewayUsersController&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;OnModuleInit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USER_PACKAGE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ClientGrpc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;userService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserServiceClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;onModuleInit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserServiceClient&lt;/span&gt;&lt;span class="o"&gt;&amp;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;UserService&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="nd"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&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;listUsers&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;REST needed one decorator. GraphQL needed a resolver and a typed model. gRPC needed a schema file, a second bootstrap function, and a client wrapper. That ordering held up in the benchmark too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each one actually wins
&lt;/h2&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;REST&lt;/th&gt;
&lt;th&gt;gRPC&lt;/th&gt;
&lt;th&gt;GraphQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Public APIs, simple CRUD, anything a stranger's HTTP client needs to call&lt;/td&gt;
&lt;td&gt;Internal service-to-service calls where both ends are your code&lt;/td&gt;
&lt;td&gt;Frontends assembling data from several sources with different shapes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What it costs you&lt;/td&gt;
&lt;td&gt;Over-fetching, one endpoint per shape&lt;/td&gt;
&lt;td&gt;A schema file, codegen, and — in Node specifically — a slower client than the marketing suggests&lt;/td&gt;
&lt;td&gt;Resolver complexity, N+1 query risk, a slower ceiling under load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What my numbers back up&lt;/td&gt;
&lt;td&gt;Fastest raw throughput, by a wide margin, in this stack&lt;/td&gt;
&lt;td&gt;Modest payload savings on this data shape; the throughput case needs a real network to prove out&lt;/td&gt;
&lt;td&gt;The only place a "protocol" choice cut payload size dramatically — by not asking for fields you don't need&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these are new conclusions. What's different is having a number attached to each one instead of a plausible-sounding claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern that shows up in real systems
&lt;/h2&gt;

&lt;p&gt;Few real systems pick just one. The common shape is REST or GraphQL at the edge, talking to gRPC internally: an external-facing layer that public and mobile clients can call with an ordinary HTTP client, backed by internal services that talk to each other over gRPC's faster, strongly-typed connections where the caller is also your code.&lt;/p&gt;

&lt;p&gt;NestJS makes this specific composition cheap because its microservices layer is transport-agnostic: the same message-handler pattern that talks gRPC can talk TCP, Redis, NATS, Kafka, or RabbitMQ without changing the handler logic, so a gateway can front several backend transports without a rewrite for each one. The &lt;code&gt;ClientGrpc&lt;/code&gt; snippet above is exactly that seam — a REST controller at the edge, calling into a gRPC service behind it. GraphQL slots into the same position when the edge needs to assemble one response from several backend calls instead of proxying a single one straight through.&lt;/p&gt;

&lt;h2&gt;
  
  
  A decision framework for your next service
&lt;/h2&gt;

&lt;p&gt;Skip the protocol question and answer these first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Who's calling this?&lt;/strong&gt; A browser, mobile app, or third party you don't control → REST or GraphQL. Only your own services → gRPC is on the table.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Is the client shape fixed or variable?&lt;/strong&gt; One consistent shape per resource → REST is simplest. Several different screens need different slices of the same data → GraphQL earns its complexity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What's actually slow?&lt;/strong&gt; If it's your database or a downstream call, changing transport protocol won't fix it — my own numbers show transport differences in the single-digit milliseconds at this scale, dwarfed by anything hitting disk or another network hop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Are you willing to own a schema file and codegen step?&lt;/strong&gt; gRPC's contract is also its tax. If your team won't maintain &lt;code&gt;.proto&lt;/code&gt; files, you'll fight the tooling more than you benefit from the wire format.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The usual expectation — that gRPC comfortably outruns REST — didn't hold on Node: REST beat gRPC by roughly 2.8-3x in my own benchmark, largely because &lt;code&gt;@grpc/grpc-js&lt;/code&gt; is a pure-JS client and localhost erases the network-latency advantage gRPC is actually built for.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Protobuf's payload savings were real but modest — 1.84x smaller than JSON — for data that's mostly strings. Numeric-heavy schemas will see a bigger gap.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The most dramatic savings in this whole comparison came from GraphQL simply not fetching fields nobody asked for: 83.5% smaller than the equivalent full REST response.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In NestJS specifically, gRPC's real cost is setup — a proto schema, a second bootstrap path, and a client wrapper — not runtime speed on a single machine.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;None of this replaces testing your own service, your own data shape, and your own network. Benchmark it before you build your architecture around someone else's numbers, including mine.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/rest-vs-grpc-vs-graphql-in-nestjs-what-the-numbers-actually-show-7qmte" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>apidesign</category>
      <category>nestjs</category>
      <category>graphql</category>
      <category>grpc</category>
    </item>
    <item>
      <title>Implementing Full-Text Search in NestJS with TypeORM and PostgreSQL</title>
      <dc:creator>Sanjay Singh</dc:creator>
      <pubDate>Sat, 29 Aug 2026 16:02:33 +0000</pubDate>
      <link>https://dev.to/sanjay_singh_1/implementing-full-text-search-in-nestjs-with-typeorm-and-postgresql-5d7i</link>
      <guid>https://dev.to/sanjay_singh_1/implementing-full-text-search-in-nestjs-with-typeorm-and-postgresql-5d7i</guid>
      <description>&lt;p&gt;Search is one of those features that looks simple until you actually have to build it. A naive &lt;code&gt;LIKE '%term%'&lt;/code&gt; query works for a demo, but it ignores word forms, ranking, and typos, and it gets slow fast as your table grows. The good news: if you're already running PostgreSQL, you don't need Elasticsearch or Algolia to get real search. Postgres has a mature full-text search engine built in, and pairing it with TypeORM in a NestJS app is far more straightforward than most tutorials make it look.&lt;/p&gt;

&lt;p&gt;This post walks through building a production-ready search feature — from schema design to a ranked, paginated API endpoint — using NestJS, TypeORM, and PostgreSQL's native full-text search.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Reach for PostgreSQL Instead of a Dedicated Search Engine
&lt;/h2&gt;

&lt;p&gt;Elasticsearch and similar tools are excellent, but they come with real costs: another service to deploy, monitor, and keep in sync with your primary database. For most applications — blogs, admin panels, marketplaces, internal tools — that overhead isn't justified.&lt;/p&gt;

&lt;p&gt;PostgreSQL's full-text search gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stemming and language awareness&lt;/strong&gt; — matching "running" to "run"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Relevance ranking&lt;/strong&gt; out of the box&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Typo tolerance&lt;/strong&gt; when combined with the &lt;code&gt;pg_trgm&lt;/code&gt; extension&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zero replication lag&lt;/strong&gt; — search results are always as fresh as your data, because they live in the same transaction&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You lose some of the advanced faceting and horizontal scalability of a dedicated search cluster, but for the majority of apps, Postgres FTS comfortably handles millions of rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick Primer on Postgres Full-Text Search
&lt;/h2&gt;

&lt;p&gt;Two types anchor everything: &lt;code&gt;tsvector&lt;/code&gt; and &lt;code&gt;tsquery&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;tsvector&lt;/code&gt; is a preprocessed, normalized representation of your text — lowercased, stemmed, and stripped of stop words like "the" and "and".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;tsquery&lt;/code&gt; is a parsed search query in the same normalized form.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You match them with the &lt;code&gt;@@&lt;/code&gt; operator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'nestjs &amp;amp; search'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Computing &lt;code&gt;to_tsvector()&lt;/code&gt; on every row for every query is expensive at scale, so the standard pattern is to store a precomputed &lt;code&gt;tsvector&lt;/code&gt; column and index it with a &lt;strong&gt;GIN index&lt;/strong&gt;, which is what makes searches over large tables fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Entity
&lt;/h2&gt;

&lt;p&gt;Assume you already have a NestJS project with &lt;code&gt;@nestjs/typeorm&lt;/code&gt; and &lt;code&gt;pg&lt;/code&gt; configured. Here's an &lt;code&gt;Article&lt;/code&gt; entity we'll add search to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// article.entity.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PrimaryGeneratedColumn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CreateDateColumn&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;typeorm&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="nd"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;articles&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;class&lt;/span&gt; &lt;span class="nc"&gt;Article&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;PrimaryGeneratedColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uuid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="nd"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;nullable&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="nx"&gt;author&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="nd"&gt;CreateDateColumn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Column&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;tsvector&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;insert&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;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;searchVector&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;select: false&lt;/code&gt; keeps the raw vector out of normal queries, and &lt;code&gt;insert: false, update: false&lt;/code&gt; tell TypeORM never to try writing to it — Postgres will generate that value itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating the Search Vector
&lt;/h2&gt;

&lt;p&gt;The cleanest approach, available since Postgres 12, is a &lt;strong&gt;generated column&lt;/strong&gt; — Postgres recomputes it automatically whenever the source columns change, no application code or triggers required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// migrations/1700000000000-AddSearchVectorToArticles.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MigrationInterface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;QueryRunner&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;typeorm&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;class&lt;/span&gt; &lt;span class="nc"&gt;AddSearchVectorToArticles1700000000000&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;MigrationInterface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;up&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QueryRunner&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
      ALTER TABLE "articles"
      ADD COLUMN "searchVector" tsvector
      GENERATED ALWAYS AS (
        setweight(to_tsvector('english', coalesce("title", '')), 'A') ||
        setweight(to_tsvector('english', coalesce("content", '')), 'B') ||
        setweight(to_tsvector('english', coalesce("author", '')), 'C')
      ) STORED;
    `&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
      CREATE INDEX "IDX_articles_search_vector"
      ON "articles" USING GIN ("searchVector");
    `&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;down&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QueryRunner&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`DROP INDEX IF EXISTS "IDX_articles_search_vector";`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`ALTER TABLE "articles" DROP COLUMN "searchVector";`&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Production note:&lt;/strong&gt; If &lt;code&gt;articles&lt;/code&gt; is already populated and serving real traffic, consider creating the GIN index with &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt; so the index build does not block writes. PostgreSQL does not allow &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt; inside a transaction, so the TypeORM migration must opt out of the default transaction with &lt;code&gt;transaction = false&lt;/code&gt;. For example:&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&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;class&lt;/span&gt; &lt;span class="nc"&gt;AddSearchVectorToArticles1700000000000&lt;/span&gt; &lt;span class="kr"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;MigrationInterface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="nx"&gt;This&lt;/span&gt; &lt;span class="nx"&gt;matters&lt;/span&gt; &lt;span class="nx"&gt;primarily&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;production&lt;/span&gt; &lt;span class="nx"&gt;migrations&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;actively&lt;/span&gt; &lt;span class="nx"&gt;used&lt;/span&gt; &lt;span class="nx"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;or&lt;/span&gt; &lt;span class="nx"&gt;empty&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt; &lt;span class="nx"&gt;does&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;have&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;same&lt;/span&gt; &lt;span class="nx"&gt;locking&lt;/span&gt; &lt;span class="nx"&gt;concern&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;setweight()&lt;/code&gt; assigns each field a priority — &lt;code&gt;A&lt;/code&gt; (highest) through &lt;code&gt;D&lt;/code&gt; (lowest) — so title matches will outrank body-text matches later when we rank results.&lt;/p&gt;

&lt;p&gt;If you're on Postgres &amp;lt; 12, or the vector needs to pull in data from a related table, use a trigger function that recalculates &lt;code&gt;searchVector&lt;/code&gt; &lt;code&gt;BEFORE INSERT OR UPDATE&lt;/code&gt; instead — same end result, just maintained procedurally rather than declaratively.&lt;/p&gt;

&lt;p&gt;Run the migration with &lt;code&gt;npm run typeorm migration:run&lt;/code&gt;, and every existing and future row gets an indexed search vector automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Search Service
&lt;/h2&gt;

&lt;p&gt;TypeORM's query builder doesn't have first-class full-text search helpers, but it happily accepts raw SQL fragments, which is all we need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// articles.service.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Injectable&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;@nestjs/common&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;InjectRepository&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;@nestjs/typeorm&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;Repository&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;typeorm&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;Article&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;./article.entity&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="nd"&gt;Injectable&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;class&lt;/span&gt; &lt;span class="nc"&gt;ArticlesService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;InjectRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;articlesRepository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Repository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&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;page&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="nx"&gt;limit&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;skip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&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="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;limit&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;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;articlesRepository&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createQueryBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article&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="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`article."searchVector" @@ websearch_to_tsquery('english', :term)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`ts_rank(article."searchVector", websearch_to_tsquery('english', :term))`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DESC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getManyAndCount&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="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&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;Note &lt;code&gt;websearch_to_tsquery&lt;/code&gt; rather than &lt;code&gt;to_tsquery&lt;/code&gt;. It's the function built for user-facing search boxes: it accepts plain phrases, &lt;code&gt;"quoted phrases"&lt;/code&gt;, &lt;code&gt;-exclusions&lt;/code&gt;, and &lt;code&gt;OR&lt;/code&gt;, without throwing a syntax error on unbalanced input the way &lt;code&gt;to_tsquery&lt;/code&gt; does. Use &lt;code&gt;to_tsquery&lt;/code&gt; only when you're constructing the query programmatically and can guarantee valid syntax; use &lt;code&gt;plainto_tsquery&lt;/code&gt; for the simplest case of AND-ing all terms together with no operators.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exposing the Endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// articles.controller.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Query&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;@nestjs/common&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;ArticlesService&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;./articles.service&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="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;articles&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;class&lt;/span&gt; &lt;span class="nc"&gt;ArticlesController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;articlesService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ArticlesService&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="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;search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;q&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;page&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="nd"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;limit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;limit&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;articlesService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&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;A request like &lt;code&gt;GET /articles/search?q=nestjs+migrations&lt;/code&gt; now returns matching articles ranked by relevance, with title matches surfacing above body-only matches thanks to the weighting from the migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ranking and Highlighting
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ts_rank&lt;/code&gt; scores by how often terms appear; &lt;code&gt;ts_rank_cd&lt;/code&gt; (cover density) also factors in how close together the matching terms are — often a better signal for longer documents. Swap it in the same way.&lt;/p&gt;

&lt;p&gt;To show users &lt;em&gt;why&lt;/em&gt; a result matched, use &lt;code&gt;ts_headline&lt;/code&gt; to generate a snippet with matches wrapped in a marker:&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="nf"&gt;searchWithSnippets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;articlesRepository&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createQueryBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article.id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article.title&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="nf"&gt;addSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`ts_headline('english', article.content, websearch_to_tsquery('english', :term),
        'StartSel=&amp;lt;mark&amp;gt;, StopSel=&amp;lt;/mark&amp;gt;, MaxWords=30, MinWords=15')`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;snippet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`article."searchVector" @@ websearch_to_tsquery('english', :term)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRawMany&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;getRawMany()&lt;/code&gt; is important here — &lt;code&gt;ts_headline&lt;/code&gt; output isn't a real entity column, so &lt;code&gt;getMany()&lt;/code&gt; would silently drop it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Performance note:&lt;/strong&gt; &lt;code&gt;ts_headline&lt;/code&gt; does not use the GIN index to generate the snippet; it re-processes the source text for each returned row. Always pair it with the same pagination you use for the main search query (&lt;code&gt;take&lt;/code&gt;/&lt;code&gt;skip&lt;/code&gt;, or SQL &lt;code&gt;LIMIT&lt;/code&gt;/&lt;code&gt;OFFSET&lt;/code&gt;) rather than running an unbounded snippet query against a large result set.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Tolerating Typos with pg_trgm
&lt;/h2&gt;

&lt;p&gt;Full-text search matches word &lt;em&gt;stems&lt;/em&gt;, not misspellings — "search" won't match "serach". For that, pair it with the &lt;code&gt;pg_trgm&lt;/code&gt; extension, which measures string similarity by shared three-character sequences:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="no"&gt;CREATE&lt;/span&gt; &lt;span class="no"&gt;EXTENSION&lt;/span&gt; &lt;span class="no"&gt;IF&lt;/span&gt; &lt;span class="no"&gt;NOT&lt;/span&gt; &lt;span class="no"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_trgm&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="no"&gt;CREATE&lt;/span&gt; &lt;span class="no"&gt;INDEX&lt;/span&gt; &lt;span class="s"&gt;"IDX_articles_title_trgm"&lt;/span&gt; &lt;span class="no"&gt;ON&lt;/span&gt; &lt;span class="s"&gt;"articles"&lt;/span&gt; &lt;span class="no"&gt;USING&lt;/span&gt; &lt;span class="nf"&gt;GIN&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt; &lt;span class="n"&gt;gin_trgm_ops&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fuzzySearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;articlesRepository&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createQueryBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article&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="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;similarity(article.title, :term) &amp;gt; 0.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="nx"&gt;term&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;similarity(article.title, :term)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DESC&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="nf"&gt;getMany&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;A common pattern: run the full-text query first, and only fall back to trigram similarity if it returns zero results. That way well-formed queries get accurate, ranked results, and typos get a forgiving fallback, without paying the cost of a trigram scan on every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Notes
&lt;/h2&gt;

&lt;p&gt;A few things matter more than anything else once you're past prototype scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Always query through the GIN index.&lt;/strong&gt; Make sure your &lt;code&gt;WHERE&lt;/code&gt; clause matches &lt;code&gt;@@&lt;/code&gt; against the indexed &lt;code&gt;tsvector&lt;/code&gt; column, not &lt;code&gt;to_tsvector(content) @@ ...&lt;/code&gt; computed inline, which can't use the index.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run&lt;/strong&gt; &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; on your search queries early. A missing index shows up immediately as a sequential scan.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cache popular queries&lt;/strong&gt; at the application layer (Redis works well) if your search endpoint gets heavy repeated traffic — Postgres FTS is fast, but no database beats not querying at all.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Paginate with&lt;/strong&gt; &lt;code&gt;LIMIT&lt;/code&gt;&lt;strong&gt;/&lt;/strong&gt;&lt;code&gt;OFFSET&lt;/code&gt;, as shown above, or switch to keyset pagination if you're dealing with very deep result sets.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;PostgreSQL's full-text search, generated columns, and a GIN index get you relevance-ranked, typo-tolerant search without introducing a new piece of infrastructure. Combined with NestJS's dependency injection and TypeORM's query builder, the whole feature — schema, service, and endpoint — fits comfortably in a single module. Reach for Elasticsearch when you genuinely need distributed scale or advanced faceted search; for everything else, the database you're already running is usually enough.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/implementing-full-text-search-in-nestjs-with-typeorm-and-postgresql-9qio4" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>typeorm</category>
      <category>postgres</category>
      <category>backenddevelopment</category>
    </item>
  </channel>
</rss>
