<?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: dqj</title>
    <description>The latest articles on DEV Community by dqj (@dqj1998).</description>
    <link>https://dev.to/dqj1998</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%2F3699688%2F0672afbc-fb16-4578-b8ff-d16dc237c174.png</url>
      <title>DEV Community: dqj</title>
      <link>https://dev.to/dqj1998</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dqj1998"/>
    <language>en</language>
    <item>
      <title>The Bottleneck Moved: Why a Team Topology Cannot Absorb AI</title>
      <dc:creator>dqj</dc:creator>
      <pubDate>Sat, 14 Nov 2026 03:55:46 +0000</pubDate>
      <link>https://dev.to/dqj1998/the-bottleneck-moved-why-a-team-topology-cannot-absorb-ai-2ogm</link>
      <guid>https://dev.to/dqj1998/the-bottleneck-moved-why-a-team-topology-cannot-absorb-ai-2ogm</guid>
      <description>&lt;p&gt;&lt;em&gt;SAFe's cross-functional team was built on a forty-year constraint. AI removed the constraint and left the org chart holding the bottleneck.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Every scaled agile framework rests on a single organizational axiom: the cross-functional team is the atom of value delivery. SAFe 6.0 calls it an Agile Team, sizes it at five to eleven people, and gives it a Program Increment of eight to twelve weeks to plan against. Everything else in the framework — the ceremonies, the roles, the planning board, the dependency red lines — is scaffolding erected around that atom.&lt;/p&gt;

&lt;p&gt;The axiom is not stupid. It was derived from a hard constraint that held for roughly forty years: no single engineer could simultaneously hold deep frontend craft, a distributed backend, serious quality engineering, and cloud-native operations. Breadth that large did not exist in one head, so the work had to be distributed across several heads, and once work is distributed across heads you need an interface layer. Standups, sprint planning, backlog refinement, estimation poker — these are not rituals in the religious sense. They are the protocol that lets a group of partially-informed people coordinate without sharing a brain.&lt;/p&gt;

&lt;p&gt;Then the constraint that justified the protocol went away, and the protocol stayed.&lt;/p&gt;

&lt;p&gt;This series is about what happens next. The argument is not that teams are obsolete, or that agile was a mistake, or that everything must be rebuilt on Monday. It is narrower and more uncomfortable: the unit that AI most directly attacks is the unit the entire framework is built on. When synthesis becomes nearly free, the scarce input moves from &lt;em&gt;producing&lt;/em&gt; the change to &lt;em&gt;judging&lt;/em&gt; it, and a structure whose every component is priced by the person-hour cannot absorb a resource that is priced at zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI actually changed
&lt;/h2&gt;

&lt;p&gt;Most enterprise AI programs measure the wrong thing, so start with the cost structure.&lt;/p&gt;

&lt;p&gt;Three costs matter in software delivery, and they did not move together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Synthesis.&lt;/strong&gt; Writing code, writing the tests around it, scaffolding a service, producing three alternative implementations of the same interface. This cost collapsed. Not to zero — models still need context, and context is expensive to assemble — but the marginal cost of one more candidate implementation is now low enough that "try three approaches" is a reasonable engineering move rather than a budget request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context assembly.&lt;/strong&gt; Getting the right repository, the right conventions, the right failure history, the right business rule into the model's window. This cost fell, but far less than the first one, and it falls unevenly: it is cheap for a well-instrumented codebase and brutal for a system whose rules live in six people's heads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification.&lt;/strong&gt; Deciding whether a candidate is correct, safe, and within the boundaries the organization actually cares about. This cost barely moved. In some organizations it rose, because the volume of candidates arriving at the review gate went up while the gate itself stayed the same size.&lt;/p&gt;

&lt;p&gt;A delivery system is governed by its most expensive term. For forty years that was synthesis, and the team topology was optimized for it: many hands, shared context, tight synchronization. The most expensive term is now verification and judgment, and almost no scaled framework has a structure for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottleneck displacement, and the capacity that isn't
&lt;/h2&gt;

&lt;p&gt;Here is the first structural failure, and it is arithmetic rather than cultural.&lt;/p&gt;

&lt;p&gt;The old constraint was the capacity to produce code and unit tests. Organizations encoded that constraint into the planning system: capacity is measured in person-days, the PI is forecast from team velocity, and the roadmap is a queue of work sized against both.&lt;/p&gt;

&lt;p&gt;If you add a zero-marginal-cost synthesis engine but keep planning by team capacity, you do not remove the bottleneck. You move it downstream and then fund the wrong side of the move. Teams generate more change per unit time; the review queue, the integration environment, the cross-team dependency, and the release gate do not. The organization experiences a rise in local output and a rise in global work-in-process, and it reads the first number as progress.&lt;/p&gt;

&lt;p&gt;It is not progress. In a system with a fixed downstream gate, extra upstream output is inventory. It sits in a branch, in a review queue, in a staging environment, waiting for the same number of human adjudicators it waited for before. The only thing the AI bought was a longer queue and a stronger feeling of momentum.&lt;/p&gt;

&lt;p&gt;The tell is in the metric itself. A capacity plan that measures generation is measuring the term that just became free. When a constraint moves, every plan, estimate, and dashboard still pointed at the old constraint becomes an instrument for producing confident wrong answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The velocity illusion
&lt;/h2&gt;

&lt;p&gt;The second failure follows from the first, and it corrupts the framework's forecasting layer rather than its execution layer.&lt;/p&gt;

&lt;p&gt;Story points and lines of code inflate quickly under AI assistance. That is not fraud; the work genuinely feels larger, because more artifacts exist. But the gates downstream — architecture review, cross-team environment integration, test gates, the security review nobody scheduled — are still on human calendars with human throughput.&lt;/p&gt;

&lt;p&gt;Put the two facts together and you get a system where individual generation is fast and organizational integration is slow. Raise the arrival rate at a fixed-throughput queue and work-in-process grows without bound, and by Little's law, cycle time grows with it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;cycle time = work in process / throughput&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Throughput at the gate did not change. Work in process tripled. The organization now reports "we are shipping faster than ever" and "nothing gets through review" in the same retrospective, and both are true, because they describe two different layers of the same pipeline.&lt;/p&gt;

&lt;p&gt;The deeper damage is to the forecast. PI planning is a prediction built on points, and points are a proxy for effort. If the proxy now measures generation instead of delivery, the PI is forecasting the wrong variable — precisely and at scale, which is worse than forecasting badly. A framework that plans an eight-to-twelve-week increment on inflated points is not aligning the organization to reality; it is aligning the organization to the part of the work that got cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cognitive cost of synchronization
&lt;/h2&gt;

&lt;p&gt;The third failure is the one engineers feel first and defend worst, because criticizing a standup sounds like defending isolation.&lt;/p&gt;

&lt;p&gt;Synchronization was a technology for a world where context transfer was expensive. A fifteen-minute standup across twelve people costs three person-hours a day. A two-week estimation session costs twenty-four person-hours per team per sprint. A two-day PI planning event for a hundred people costs two hundred person-days per increment. These numbers were rational when the alternative was a week of misalignment discovered at integration.&lt;/p&gt;

&lt;p&gt;They stop being rational when one senior engineer, working inside a sandbox with the repository and the tests, can validate three competing architecture options in a single hour. At that point the information payload of the standup — what I did, what I will do, what blocks me — is generated faster than it can be spoken, and the meeting becomes the slowest component in the loop. Worse than slow: it fragments the flow state that makes the hour of parallel exploration possible in the first place.&lt;/p&gt;

&lt;p&gt;This is not an argument for abolishing human contact. It is an argument about what the ceremony was buying. It was buying context transfer at a moment when context transfer was the binding cost. That is no longer the binding cost, and a protocol that is priced per person-hour to move information that is now nearly free is not a control system. It is a tax on the exact capability the organization is trying to acquire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the constraint sits
&lt;/h2&gt;

&lt;p&gt;The whole shift fits in five lines.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit of work&lt;/strong&gt; — then: a five-to-eleven-person team. Now: one engineer plus a platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scarce input&lt;/strong&gt; — then: code and test authoring. Now: contextual judgment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Planning signal&lt;/strong&gt; — then: team capacity in person-days. Now: irreversible boundary decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coordination cost&lt;/strong&gt; — then: one meeting per transfer. Now: one verification per decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure mode&lt;/strong&gt; — then: slow delivery. Now: fast generation, slow merge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read the last two lines together, because they explain each other. Coordination was cheap relative to synthesis, so organizations coordinated more. Now verification is the scarce act, so the cost of a decision is the cost of proving it — and the structure that used to prove things by putting twelve people in a room is not the structure that proves things fastest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the incrementalist move actually is
&lt;/h2&gt;

&lt;p&gt;SAFe's answer so far is to keep the container and swap the motor. Teams become "AI-Empowered" or "AI-Native"; the model is added as a tool, or in the stronger version, as a synthetic team member. The shell — five to eleven people, two-week cadence, PI planning, the dependency board — stays exactly as it was.&lt;/p&gt;

&lt;p&gt;Two things are wrong with this, and neither is about culture or change management.&lt;/p&gt;

&lt;p&gt;The first is that every component of the container is a function of headcount, not of throughput. A standup that fits in fifteen minutes fits because twelve people speak briefly. A PI that fits in two days fits because a hundred people can be in one room. Add a zero-marginal-cost generator to a system whose interfaces are all sized by the number of humans and you have not increased the system's throughput; you have increased the load on its interfaces.&lt;/p&gt;

&lt;p&gt;The second is authority. The team still owns delivery, so the individual never receives authority proportional to their new capability. You have increased the supply of decisions without changing who is allowed to make them. The engineer can now generate and evaluate three designs in an hour, and still cannot commit to one without a refinement session, because the unit of accountability never moved. What you get is a very fast producer inside an unchanged approval structure — which is exactly the configuration that produces the queue described above.&lt;/p&gt;

&lt;p&gt;None of this makes the incrementalist step irrational. Adding AI inside a two-week sprint, with a private gateway and a security baseline, is the correct move at the bottom of a maturity ladder; most organizations that skipped it are worse off. It is simply not a destination, and frameworks are treating it as one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the team still buys
&lt;/h2&gt;

&lt;p&gt;A critique that concedes nothing is not worth reading. The team is not only a coordination protocol, and pretending otherwise would be dishonest.&lt;/p&gt;

&lt;p&gt;A team buys fault isolation, so one person's bad week does not take down a service. It buys on-call rotation, redundancy of knowledge, a hiring surface, psychological safety for junior engineers, and the informal learning that happens when two people look at the same problem. All of that is real, and none of it is delivered by a model.&lt;/p&gt;

&lt;p&gt;But the word "team" is currently doing three jobs at once, and separating them dissolves most of the argument:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The unit of delivery&lt;/strong&gt; — what actually produces a change end to end. AI attacks this directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The unit of accountability&lt;/strong&gt; — who owns the outcome when it fails at 3 a.m. This should be as small as it can be made, because accountability dilutes with size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The unit of coordination&lt;/strong&gt; — how context, constraints, and standards reach the person doing the work. This does not have to be a meeting. It can be a platform.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The team topology collapses all three into one object, sized for the era when all three were expensive. AI makes the first cheap, demands the second shrink, and turns the third into a platform problem. A structure that insists they remain the same size is not preserving safety. It is preserving the queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question this forces
&lt;/h2&gt;

&lt;p&gt;If the atomic unit of delivery is a single engineer carrying end-to-end responsibility, the immediate question is not "how do we motivate them". It is: what has to be true for that to be safe?&lt;/p&gt;

&lt;p&gt;Three things, and none of them can be supplied by the engineer alone.&lt;/p&gt;

&lt;p&gt;Someone has to own the business intent and the semantic boundary — what the thing must mean, what the acceptance gate is — and must be structurally forbidden from decomposing the work into micro-tasks. Someone has to hold the global technical context and the governance constraints, and must not be the person who writes the performance review, or the engineer will optimize for the reviewer instead of the system. Someone has to own the human-AI practice itself: pairing, context engineering, hallucination control, and the conversion of every incident into a deterministic rule so the same failure cannot recur.&lt;/p&gt;

&lt;p&gt;Those are not team roles. They are roles around a cell, and the cell is one engineer with the authority to decide. That is the shape ANTcell proposes, and it is where Part 2 goes: the three-role decoupling, the physical separation that makes it more than a relabeling, and the reason a single engineer can hold accountability that a team of nine currently cannot.&lt;/p&gt;

&lt;p&gt;Part 3 takes the timing question — why a twelve-week PI and a sixty-minute pipeline are not in conflict but are the two halves of one control system. Part 4 is the part enterprises will actually use: the four-level maturity path, and why skipping levels fails in a predictable way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The failure of AI adoption inside scaled agile is usually described as a culture problem, an adoption problem, or a skills problem. It is none of those first. It is a unit-of-accountability mismatch.&lt;/p&gt;

&lt;p&gt;You cannot bolt a zero-marginal-cost synthesis engine onto a coordination structure whose every component is priced by the hour, keep the same approval topology, and call the result a transformation. What you get is more inventory, an inflated forecast, and a meeting schedule that costs more than the information it moves. The bottleneck moved. The org chart did not.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 1 of 4 in AI-Native, Deconstructed: From SAFe Incrementalism to the ANTcell Rewrite.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>agile</category>
    </item>
    <item>
      <title>Running a high-end bakery in the age of industrialized code</title>
      <dc:creator>dqj</dc:creator>
      <pubDate>Wed, 28 Jan 2026 14:16:03 +0000</pubDate>
      <link>https://dev.to/dqj1998/running-a-high-end-bakery-in-the-age-of-industrialized-code-5c9l</link>
      <guid>https://dev.to/dqj1998/running-a-high-end-bakery-in-the-age-of-industrialized-code-5c9l</guid>
      <description>&lt;p&gt;When considering productivity, this analogy always comes to mind:&lt;br&gt;
&lt;strong&gt;High-end bakeries vs. industrial bread factories.&lt;/strong&gt;&lt;br&gt;
High-end bakeries produce bread of superior quality. They are meticulous, skillfully crafted, expensive—and serve a relatively small customer base.&lt;br&gt;
Factory bread, on the other hand, mass-produces "good enough" bread.&lt;/p&gt;

&lt;p&gt;As artificial intelligence begins to generate massive amounts of production code in an industrialized manner, I can't help but wonder if the software industry is heading in a similar direction.&lt;/p&gt;

&lt;p&gt;When AI can generate code that passes most code reviews in seconds, and most users won't even notice the difference, what does it mean that we spend ten times as much time writing elegant code?&lt;/p&gt;

&lt;p&gt;Software engineers may be in a worse position than high-end bakeries. &lt;br&gt;
&lt;strong&gt;Will anyone pay ten times more for your software simply because they appreciate its beautiful code?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I genuinely want to understand in what areas human effort can still create significant value, and in what areas might this effort quietly lose its due reward.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/antcell/in-the-era-of-industrialized-code-are-you-still-planning-to-run-a-high-end-bakery-e3a777e20d0b" rel="noopener noreferrer"&gt;The full story on Medium&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>vibecoding</category>
      <category>ai</category>
    </item>
    <item>
      <title>The Regression Bug Was the Most Important Result</title>
      <dc:creator>dqj</dc:creator>
      <pubDate>Tue, 13 Jan 2026 09:55:20 +0000</pubDate>
      <link>https://dev.to/dqj1998/the-regression-bug-was-the-most-important-result-1ki4</link>
      <guid>https://dev.to/dqj1998/the-regression-bug-was-the-most-important-result-1ki4</guid>
      <description>&lt;p&gt;&lt;strong&gt;The Feature Wasn’t the Point&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two AI systems successfully implemented the requested UI feature.&lt;/p&gt;

&lt;p&gt;That part wasn’t interesting.&lt;/p&gt;

&lt;p&gt;What mattered was what broke outside the requested scope.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The Regression&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After the change:&lt;/p&gt;

&lt;p&gt;Select text in the editor&lt;/p&gt;

&lt;p&gt;Trigger the “Replace with” contextual action&lt;/p&gt;

&lt;p&gt;Nothing happened.&lt;/p&gt;

&lt;p&gt;No error.&lt;br&gt;
No exception.&lt;br&gt;
Just silent failure.&lt;/p&gt;

&lt;p&gt;This is the most dangerous class of bug in production software.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why This Happens with AI-Generated Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From an engineering perspective, this failure is predictable:&lt;/p&gt;

&lt;p&gt;The prompt emphasized new functionality&lt;/p&gt;

&lt;p&gt;Existing behavior was implicit, not asserted&lt;/p&gt;

&lt;p&gt;No test explicitly protected that interaction&lt;/p&gt;

&lt;p&gt;AI systems optimize for local correctness,&lt;br&gt;
not global behavioral invariants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the Implementations Differed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A closer look at the code revealed meaningful trade-offs:&lt;/p&gt;

&lt;p&gt;One approach prioritized UX richness and localization&lt;/p&gt;

&lt;p&gt;The other emphasized modular helpers, safer selectors, and test coverage&lt;/p&gt;

&lt;p&gt;A concrete example:&lt;/p&gt;

&lt;p&gt;encodeURIComponent(...) vs CSS.escape(...)&lt;/p&gt;

&lt;p&gt;Both work, but only one is designed for DOM and selector safety.&lt;/p&gt;

&lt;p&gt;These choices matter months later, not minutes after generation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Unit Tests Made the Difference&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Only one implementation introduced unit tests covering:&lt;/p&gt;

&lt;p&gt;State migration&lt;/p&gt;

&lt;p&gt;Regex escaping&lt;/p&gt;

&lt;p&gt;Replacement logic edge cases&lt;/p&gt;

&lt;p&gt;Those tests didn’t just validate correctness —&lt;br&gt;
they made the regression visible.&lt;/p&gt;

&lt;p&gt;Without them, the bug would likely ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you evaluate AI coding tools in real projects, ask:&lt;/p&gt;

&lt;p&gt;What existing behavior is protected?&lt;/p&gt;

&lt;p&gt;What assumptions remain implicit?&lt;/p&gt;

&lt;p&gt;What breaks quietly?&lt;/p&gt;

&lt;p&gt;Demos won’t answer those questions.&lt;br&gt;
Production code will.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Final Thought&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most valuable result wasn’t the feature.&lt;/p&gt;

&lt;p&gt;It was identifying where AI coding systems still fail like junior engineers —&lt;br&gt;
and where they don’t.&lt;/p&gt;

&lt;p&gt;That distinction is what matters in real-world software.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>testing</category>
      <category>codequality</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>Why WebAuthn Feels Easy — Until You Try to Ship It</title>
      <dc:creator>dqj</dc:creator>
      <pubDate>Thu, 08 Jan 2026 13:14:04 +0000</pubDate>
      <link>https://dev.to/dqj1998/why-webauthn-feels-easy-until-you-try-to-ship-it-5bi3</link>
      <guid>https://dev.to/dqj1998/why-webauthn-feels-easy-until-you-try-to-ship-it-5bi3</guid>
      <description>&lt;p&gt;&lt;em&gt;Every WebAuthn demo works. Production is where things quietly fall apart.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;WebAuthn demos are dangerously convincing.&lt;/p&gt;

&lt;p&gt;You follow a tutorial, register a passkey, authenticate successfully, and everything feels… solved.&lt;br&gt;&lt;br&gt;
No passwords. No OTPs. No friction.&lt;/p&gt;

&lt;p&gt;Then you ship it.&lt;/p&gt;

&lt;p&gt;And suddenly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users authenticate when they shouldn’t&lt;/li&gt;
&lt;li&gt;Counters behave strangely&lt;/li&gt;
&lt;li&gt;Different browsers disagree&lt;/li&gt;
&lt;li&gt;Enterprise customers ask questions your demo never prepared you for&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gap — between &lt;em&gt;“it works”&lt;/em&gt; and &lt;em&gt;“it survives production”&lt;/em&gt; — is where most WebAuthn implementations fail.&lt;/p&gt;

&lt;p&gt;After watching teams repeat the same mistakes for years, &lt;strong&gt;three gaps show up again and again&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Gap #1: Demo-Grade Database Access
&lt;/h2&gt;

&lt;p&gt;Most WebAuthn demos include some version of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;username&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;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`SELECT * FROM users WHERE username = '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&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="nx"&gt;query&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;It works in development.&lt;br&gt;&lt;br&gt;
It works in testing.&lt;br&gt;&lt;br&gt;
It even works in staging.&lt;/p&gt;

&lt;p&gt;Until a real username contains a quote — or someone decides to try &lt;code&gt;' OR '1'='1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This isn’t a WebAuthn problem.&lt;br&gt;&lt;br&gt;
It’s a &lt;em&gt;production engineering&lt;/em&gt; problem that demos ignore.&lt;/p&gt;

&lt;p&gt;Production systems parameterize everything, including dynamic &lt;code&gt;IN&lt;/code&gt; clauses:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildInClause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;clause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(NULL)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="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;placeholders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;values&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="o"&gt;=&amp;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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;clause&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;placeholders&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="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 difference isn’t elegance — it’s &lt;strong&gt;survivability&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gap #2: “One-Call” Verification Illusions
&lt;/h2&gt;

&lt;p&gt;Demo verification often looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fido2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Minimal. Completely insufficient.&lt;/p&gt;

&lt;p&gt;In production, this single call hides multiple attack surfaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replay attacks&lt;/li&gt;
&lt;li&gt;Counter rollback (cloned authenticators)&lt;/li&gt;
&lt;li&gt;Origin spoofing&lt;/li&gt;
&lt;li&gt;Challenge reuse across sessions&lt;/li&gt;
&lt;li&gt;Native app origin edge cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real verification logic validates &lt;em&gt;every layer&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &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;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;prevCounter&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;counterSupported&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;counter rollback detected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&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;expectedOrigin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;origin mismatch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Counters, origins, encoding, challenge binding, RP ID — none of these are optional in production.&lt;/p&gt;

&lt;p&gt;If you skip one, authentication still “works” — until it doesn’t.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gap #3: The Myth of “Single-Domain” WebAuthn
&lt;/h2&gt;

&lt;p&gt;Demos assume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One RP ID&lt;/li&gt;
&lt;li&gt;One domain&lt;/li&gt;
&lt;li&gt;One policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Production environments don’t.&lt;/p&gt;

&lt;p&gt;Real systems need to handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple subdomains&lt;/li&gt;
&lt;li&gt;Wildcard RP IDs&lt;/li&gt;
&lt;li&gt;Enterprise authenticator allowlists (AAGUID-based)&lt;/li&gt;
&lt;li&gt;Device limits per user&lt;/li&gt;
&lt;li&gt;Per-tenant timeouts&lt;/li&gt;
&lt;li&gt;Device binding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Configuration stops being a constant and becomes &lt;em&gt;data&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"device_limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"registration_session_timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;999&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn’t complexity for complexity’s sake.&lt;br&gt;&lt;br&gt;
It’s the minimum required to support real organizations.&lt;/p&gt;




&lt;h2&gt;
  
  
  What “Production-Ready” Actually Means
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Demo Reality&lt;/th&gt;
&lt;th&gt;Production Reality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;String queries&lt;/td&gt;
&lt;td&gt;Parameterized everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification&lt;/td&gt;
&lt;td&gt;Single function&lt;/td&gt;
&lt;td&gt;Multi-layer validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domains&lt;/td&gt;
&lt;td&gt;localhost&lt;/td&gt;
&lt;td&gt;Wildcards &amp;amp; subdomains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Counters&lt;/td&gt;
&lt;td&gt;Ignored&lt;/td&gt;
&lt;td&gt;Rollback detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy&lt;/td&gt;
&lt;td&gt;Hardcoded&lt;/td&gt;
&lt;td&gt;Per-tenant configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these failures are obvious in demos.&lt;br&gt;&lt;br&gt;
All of them show up &lt;strong&gt;after users trust your system&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hard Truth About WebAuthn
&lt;/h2&gt;

&lt;p&gt;WebAuthn is easy to &lt;em&gt;demonstrate&lt;/em&gt;.&lt;br&gt;&lt;br&gt;
It is hard to &lt;em&gt;operate safely&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The problem isn’t the standard — it’s the illusion that a passing demo equals a shippable system.&lt;/p&gt;

&lt;p&gt;If you’re planning to deploy passkeys beyond a prototype, &lt;strong&gt;treat demos as educational tools, not architectural references&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Because in authentication, failure rarely looks like an error.&lt;br&gt;&lt;br&gt;
It looks like success — for the wrong user.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;I work on WebAuthn systems that need to survive real-world traffic, browsers, and enterprise constraints.&lt;br&gt;&lt;br&gt;
Most of the lessons above came from fixing things that “worked fine” — until they didn’t.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webauthn</category>
      <category>security</category>
      <category>authentication</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
