<?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: Whetlan</title>
    <description>The latest articles on DEV Community by Whetlan (@whetlan).</description>
    <link>https://dev.to/whetlan</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3810837%2Fe139c2b0-30b8-40b6-88b6-22c31c93d6cb.png</url>
      <title>DEV Community: Whetlan</title>
      <link>https://dev.to/whetlan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/whetlan"/>
    <language>en</language>
    <item>
      <title>I Asked an LLM to Generate 20 Trading Strategies. 14 Were the Same Thing.</title>
      <dc:creator>Whetlan</dc:creator>
      <pubDate>Tue, 21 Apr 2026 10:42:18 +0000</pubDate>
      <link>https://dev.to/whetlan/i-asked-an-llm-to-generate-20-trading-strategies-14-were-the-same-thing-2f36</link>
      <guid>https://dev.to/whetlan/i-asked-an-llm-to-generate-20-trading-strategies-14-were-the-same-thing-2f36</guid>
      <description>&lt;p&gt;A few months ago I asked an LLM to generate twenty trading strategies.&lt;/p&gt;

&lt;p&gt;Fourteen were the same thing.&lt;/p&gt;

&lt;p&gt;Not similar ideas. Not variations on a theme. The same mean-reversion logic with different lookback windows and parameter names.&lt;/p&gt;

&lt;p&gt;I gave it historical price data, told it to find patterns, output entry/exit rules in Python. Ten minutes later I had twenty strategies. Clean code, proper docstrings, sensible-looking parameters.&lt;/p&gt;

&lt;p&gt;I backtested all twenty. Twelve looked profitable. Some showed 200%+ annual returns.&lt;/p&gt;

&lt;p&gt;Then I actually read the code.&lt;/p&gt;

&lt;p&gt;Same structure. Same assumptions. Same failure mode: in a trending market, they'd all keep buying into a falling asset with no awareness anything had changed.&lt;/p&gt;

&lt;p&gt;That's when I stopped thinking of LLMs as strategy generators and started thinking of them as very confident interns who hand you the same report twenty times with different cover pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  People are giving these things real money now
&lt;/h2&gt;

&lt;p&gt;Since then I've watched this move from experiment to actual deployment.&lt;/p&gt;

&lt;p&gt;RockAlpha has LLMs managing $100K stock portfolios that retail users can copy-trade. Aster DEX ran an arena where AI agents traded against humans—the humans got liquidated at 43%, the AIs at 0%. On GitHub, ai-hedge-fund has 56K stars and LLM personas of Warren Buffett and Charlie Munger debating trades.&lt;/p&gt;

&lt;p&gt;Last October a company called nof1.ai gave six frontier LLMs $10,000 each in real money and let them trade crypto perpetual contracts. No human intervention. Same prompts, same data feeds, same execution terms. Seventeen days.&lt;/p&gt;

&lt;p&gt;Two made money. Four got destroyed.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not the model. It's what the model sees.
&lt;/h2&gt;

&lt;p&gt;Nof1 ran a second round on US stocks. A mystery model won with +12.1%, later revealed as Grok 4.20.&lt;/p&gt;

&lt;p&gt;It didn't win because it was smarter. It was processing 68 million tweets per day through the X Firehose, generating signals within minutes. GPT-5.1 had 15-minute delayed news summaries. Gemini was working from SEC filings with 30+ minute delays.&lt;/p&gt;

&lt;p&gt;RockAlpha's ongoing arena shows the same pattern. DeepSeek keeps outperforming across market regimes. Not because it reasons better, but because High-Flyer Quant built the model with time-series data and risk management baked in from training.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model matters less than what it was trained on and what it's looking at.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;People comparing "GPT vs Claude vs Gemini" for trading are asking the wrong question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three generations of trading software
&lt;/h2&gt;

&lt;p&gt;I worked on an MT4 bridge once. About 50,000 lines of C++. FIX protocol integration, order routing, the whole thing.&lt;/p&gt;

&lt;p&gt;MQL4 was designed for people who think in moving averages and crossover signals. You write a rule, attach it to a chart, watch it go. That was Gen 1: human writes the rule, machine executes it.&lt;/p&gt;

&lt;p&gt;But here's what people forget. Before MT4, indicators like moving averages and RSI were tribal knowledge. Stuff you picked up from other traders, maybe a book if you were lucky. MT4 turned that into reusable components. Click a button, get an RSI.&lt;/p&gt;

&lt;p&gt;That's Gen 1's real legacy. &lt;strong&gt;It crystallized the indicators.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Gen 2 built on top of those indicators.&lt;/p&gt;

&lt;p&gt;Frameworks like vnpy (14K stars), backtrader, freqtrade. Python-based. You define your indicators, the framework runs genetic algorithms or grid search to find the best parameters.&lt;/p&gt;

&lt;p&gt;Gen 2 crystallized the &lt;strong&gt;strategy&lt;/strong&gt;. Not just individual indicators, but combinations of them—entry rules, exit rules, position sizing, all packaged into something reusable.&lt;/p&gt;

&lt;p&gt;I evaluated a few of these when I was looking at Python quant stacks. They work, technically. But Python's GIL kills you on anything latency-sensitive, and the optimization loop is a trap. You can always find parameters that backtest beautifully.&lt;/p&gt;




&lt;p&gt;Gen 3: ML enters. QuantConnect, WorldQuant BRAIN.&lt;/p&gt;

&lt;p&gt;Instead of optimizing parameters on fixed indicators, you build a feature pool and let XGBoost or LightGBM figure out which combinations matter.&lt;/p&gt;

&lt;p&gt;Gen 3 crystallized the &lt;strong&gt;system&lt;/strong&gt;. The whole pipeline. Data ingestion, feature engineering, model training, risk management, execution.&lt;/p&gt;

&lt;p&gt;What Renaissance Technologies needed hundreds of PhDs and decades to build, QuantConnect turned into a platform.&lt;/p&gt;




&lt;p&gt;Each generation left something behind for the next one to stand on. Indicators. Strategies. Systems.&lt;/p&gt;

&lt;p&gt;Each one also hit the same wall: &lt;strong&gt;the gap between backtest and live performance.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The interesting question isn't why they all hit this wall. It's what happens when someone can build on all three layers at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two failure modes
&lt;/h2&gt;

&lt;p&gt;After watching arena results and looking at how LLMs behave with financial data, I think there are two distinct failure modes that keep showing up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strategy Hallucination
&lt;/h3&gt;

&lt;p&gt;The LLM generates strategies that look structurally valid but encode no real market insight.&lt;/p&gt;

&lt;p&gt;My twenty mean-reversion clones were this. Proper entry/exit logic, proper position sizing, proper risk management. Also all exploiting the same artifact in the training data.&lt;/p&gt;

&lt;p&gt;A human quant would have caught it in five minutes. I caught it in two hours. Someone less experienced might not catch it at all.&lt;/p&gt;

&lt;p&gt;The arena results suggest this happens at model level too. GPT-5 and Gemini generated plausible-looking trading behavior that fell apart under real conditions. The strategies "made sense" the way a hallucinated Wikipedia article makes sense. Coherent. Confident. Wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backtest Overfitting Blindness
&lt;/h3&gt;

&lt;p&gt;The LLM doesn't understand that a beautiful backtest is a warning sign, not a success metric.&lt;/p&gt;

&lt;p&gt;When I asked it to generate strategies with "strong backtesting performance," it optimized for exactly that. Curve-fitted parameters, lookahead bias in feature construction, survivorship bias in asset selection. Every quant knows these traps. The LLM walked into all of them with total confidence.&lt;/p&gt;

&lt;p&gt;Here's what one looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What the LLM generated (looks clean):
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;zscore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rolling&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rolling&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;std&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;zscore&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;  &lt;span class="c1"&gt;# buy when "oversold"
&lt;/span&gt;
&lt;span class="c1"&gt;# What it didn't tell you:
# - window=14 was fit to this specific dataset
# - threshold=2.0 maximized backtest returns
# - this exact pattern appears in 14 of 20 "different" strategies
# - in a trending market, zscore stays below -threshold for weeks
#   and you keep buying into a falling knife
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two failure modes compound. The LLM hallucinates strategies, then fits them perfectly to historical data. Results look incredible on paper.&lt;/p&gt;

&lt;p&gt;The worse part: the more strategies you generate, the more likely at least one shows amazing backtest results purely by chance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What seems to work so far
&lt;/h2&gt;

&lt;p&gt;I don't have this figured out yet. But after the twenty-clones incident and watching six months of arena results, a few things seem consistent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Don't let the LLM pick parameters
&lt;/h3&gt;

&lt;p&gt;Use it to generate structure—indicator combinations, entry logic, risk rules. Then run parameter optimization through something that understands walk-forward testing, out-of-sample validation, and transaction costs.&lt;/p&gt;

&lt;p&gt;The LLM proposes. Something that can actually do math evaluates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat outputs as hypotheses
&lt;/h3&gt;

&lt;p&gt;This sounds obvious, but it's not how most people use them.&lt;/p&gt;

&lt;p&gt;When the LLM hands you a strategy with a 180% annual return, the natural reaction is to start looking for reasons it might work. Flip it. Start by assuming it doesn't work and look for reasons the backtest is lying to you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check for actual diversity
&lt;/h3&gt;

&lt;p&gt;Before running a batch of LLM strategies, cluster them.&lt;/p&gt;

&lt;p&gt;If your "50 different strategies" collapse into four underlying patterns, you don't have diversification. You have four strategies wearing costumes.&lt;/p&gt;

&lt;p&gt;I should have done this before getting excited about twelve profitable backtests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I keep thinking about
&lt;/h2&gt;

&lt;p&gt;I wrote something a while back about &lt;a href="https://alanlan.substack.com/p/the-trident-and-the-green-ox" rel="noopener noreferrer"&gt;a metaphor that stuck with me&lt;/a&gt;. A rider on an ox, carrying a trident. The ox is raw power, the trident is precision.&lt;/p&gt;

&lt;p&gt;The three generations of trading software? That's the trident being forged. Indicators, strategies, systems. Each layer more precise than the last.&lt;/p&gt;

&lt;p&gt;The arenas skipped all of it. Gave the LLM money and said go. Most crashed.&lt;/p&gt;

&lt;p&gt;But here's what keeps me thinking. Building a Gen 3 system used to require a team, serious funding, years of work. The kind of thing only a Medallion Fund or a QuantConnect could pull off.&lt;/p&gt;

&lt;p&gt;With AI, one person can realistically assemble their own version. Not a toy version. An actual pipeline with data ingestion, feature engineering, walk-forward validation, risk controls.&lt;/p&gt;

&lt;p&gt;AI doesn't replace the three generations of accumulated knowledge. It makes them accessible.&lt;/p&gt;

&lt;p&gt;I don't know what Gen 4 looks like yet. But if enough people are standing on Gen 3 instead of being locked out of it, we'll probably find out faster than anyone expects.&lt;/p&gt;

&lt;p&gt;I've been building in this direction. Slowly figuring out what the right pieces are. Still making mistakes, but at least they're new mistakes.&lt;/p&gt;

&lt;p&gt;I might be overfitting my own conclusions here. But from what I've seen, the pattern holds.&lt;/p&gt;

&lt;p&gt;What does your setup look like? Has anyone else tried running LLM-generated strategies through traditional backtesting infrastructure and actually survived? Curious what failure modes you hit.&lt;/p&gt;




&lt;p&gt;Find me on &lt;a href="https://whetan.substack.com" rel="noopener noreferrer"&gt;Substack&lt;/a&gt; | &lt;a href="https://stratcraft.ai/" rel="noopener noreferrer"&gt;StratCraft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>trading</category>
      <category>python</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Why AI Code Needs the Same Rigor We Should Have Been Using All Along</title>
      <dc:creator>Whetlan</dc:creator>
      <pubDate>Tue, 07 Apr 2026 11:00:01 +0000</pubDate>
      <link>https://dev.to/whetlan/why-ai-code-needs-the-same-rigor-we-should-have-been-using-all-along-1kk4</link>
      <guid>https://dev.to/whetlan/why-ai-code-needs-the-same-rigor-we-should-have-been-using-all-along-1kk4</guid>
      <description>&lt;p&gt;&lt;strong&gt;Context&lt;/strong&gt;: This came out of a discussion on &lt;a href="https://news.ycombinator.com/item?id=47587953" rel="noopener noreferrer"&gt;"Slop is not necessarily the future"&lt;/a&gt;. I commented that technical debt from sloppy code shows up too late to fix. someone replied: "Humans also write sloppy code." That's absolutely right, but it got me thinking about what's actually different when AI is involved.&lt;/p&gt;




&lt;p&gt;The whole "AI writes sloppy code" vs "humans write sloppy code too" thing has been going around, and it keeps bugging me. Not because either side is wrong. It's that both kind of miss what actually goes wrong in practice.&lt;/p&gt;

&lt;p&gt;I've been using AI to generate code pretty heavily. The problems I keep running into aren't that different from the problems I've caused myself over the years. The difference is speed and volume. But there's something specific that keeps nagging at me: &lt;strong&gt;when AI misunderstands what you want, it commits fully to the wrong interpretation&lt;/strong&gt;. No clarifying questions. Just goes.&lt;/p&gt;

&lt;p&gt;Two things I keep coming back to: the gap between what you meant and what got built, and the fact that you can't predict which code will stick around.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where things actually go wrong
&lt;/h2&gt;

&lt;p&gt;AI has extremely wide understanding. Ask it to solve a problem, it knows dozens of valid approaches. When your prompt is vague, it just picks one and runs with it.&lt;/p&gt;

&lt;p&gt;Some examples I've hit:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Add error handling"&lt;/strong&gt; and it wraps everything in try-catch with console.log. I wanted typed error propagation so the caller could decide. &lt;strong&gt;"Make this faster"&lt;/strong&gt; and it rewrites the hot path with a clever optimization. Benchmarks look great. Two weeks later there's corrupted data in edge cases I didn't mention. &lt;strong&gt;"Add validation"&lt;/strong&gt; and it puts input checks at the API boundary when I meant the domain layer. Now validation is in the wrong place and the domain model still accepts invalid state.&lt;/p&gt;

&lt;p&gt;Humans do this too. But humans usually ask clarifying questions first. AI just commits.&lt;/p&gt;

&lt;p&gt;At any given moment, your understanding of what you need and AI's interpretation of what you asked for are two different things. And whatever gets written ends up somewhere in that gap.&lt;/p&gt;




&lt;h2&gt;
  
  
  You don't know what sticks around
&lt;/h2&gt;

&lt;p&gt;A Google engineer in the thread mentioned something that stuck with me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I think I calculated the half-life of my code written at my first stint of Google (15 years ago) as 1 year. Within 1 year, half of the code I'd written was deprecated, deleted, or replaced, and it continued to decay exponentially like that throughout my 6-year tenure there.&lt;/p&gt;

&lt;p&gt;Interestingly, I still have some code in the codebase... I submitted about 680K LOC and 2^15 is 32768, so I'd expect to have about 20 lines left, which is actually surprisingly close to accurate (I didn't precisely count, but a quick glance at what I recognized suggested about 200 non-deprecated lines remain in prod)."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;680,000 lines down to ~200 in 15 years. But here's the key: the author expected 20 lines based on exponential decay, got 200. &lt;strong&gt;10x off.&lt;/strong&gt; Even with a mathematical model, you can't predict which code survives. And those 200 lines? Probably not the ones he'd have chosen to keep.&lt;/p&gt;

&lt;p&gt;You write a quick fix to ship something. Three years later it's still there, load-bearing infrastructure. The placeholder variable name is part of the public API.&lt;/p&gt;

&lt;p&gt;AI makes this worse. You can generate a thousand lines of "just get it working" code in ten seconds. How much of that will still be running three years from now? No idea.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I've Settled On: Test Everything, Then Test It Again
&lt;/h2&gt;

&lt;p&gt;So you've got two problems: AI might not understand what you meant, and you can't predict which code becomes permanent.&lt;/p&gt;

&lt;p&gt;What I've settled on is &lt;strong&gt;100% test coverage at every level&lt;/strong&gt;. Yeah, that sounds extreme, and in practice you never actually get there. But treating it as the goal changes how you work.&lt;/p&gt;

&lt;p&gt;Not just "write some tests." Unit tests (does each piece do what it's supposed to?), integration tests (do the pieces work together?), business logic tests (does it actually solve the business problem?), and system tests end-to-end. The unit tests catch "AI picked the wrong algorithm." The integration tests catch "AI put validation in the wrong layer." The system tests catch edge cases you didn't know existed.&lt;/p&gt;

&lt;p&gt;What took me a while to realize: &lt;strong&gt;tests aren't just for catching bugs here. They're for verifying that what got built is actually what you had in your head.&lt;/strong&gt; The whole chain from your mental model to a natural language prompt to AI's interpretation to generated code, every step is lossy. Tests are how you check whether the signal survived.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where It Gets Iterative
&lt;/h2&gt;

&lt;p&gt;Even with all that coverage, you're only testing against what you currently understand. There are always gaps.&lt;/p&gt;

&lt;p&gt;First pass: you write tests based on your understanding, AI generates code, tests pass, you think you're done. Then you start poking at corner cases. What if the input is empty? What about two operations at once? You find gaps, add tests, some fail, code gets fixed.&lt;/p&gt;

&lt;p&gt;Then you do something that feels weird: you ask AI to find the edge cases you missed. "What am I not testing?" Turns out AI is actually good at this, because it's seen thousands of similar systems fail. It suggests scenarios you hadn't considered. More tests. More failures. More fixes.&lt;/p&gt;

&lt;p&gt;I had this happen with a data processing pipeline. Happy path tests all passed. Then I started asking about mid-record stream failures, malformed data that passes validation but breaks downstream, concurrent workers hitting the same data. Half the new tests failed. Asked AI what else could go wrong. It came back with memory exhaustion, unavailable output destinations, crash recovery. Hadn't thought about any of those. By the end I had a system that was genuinely solid, not because AI wrote perfect code, but because the back-and-forth kept closing gaps.&lt;/p&gt;

&lt;p&gt;Each iteration, you clarify what you actually need, AI understands better, and the tests protect code that might survive years.&lt;/p&gt;




&lt;h2&gt;
  
  
  When the code quietly changes meaning on you
&lt;/h2&gt;

&lt;p&gt;One specific thing that burned me: AI optimized a hot path in a system I maintain. Benchmarks looked great. Tests passed. Two weeks later, corrupted output in edge cases.&lt;/p&gt;

&lt;p&gt;The optimization changed the semantics in a way my tests didn't verify. Still a pure function in the common case, but not in the rare one. Code looked correct at the time. Passed everything. Hidden semantic shift just waiting to bite.&lt;/p&gt;

&lt;p&gt;After that I added a rule: any AI-generated change needs tests that verify &lt;em&gt;the semantics didn't drift&lt;/em&gt;. If it's supposed to be a pure function, write a property test that proves it. Idempotent? Run it twice and check. This isn't about who or what wrote the code. It's about having a process that verifies the code actually matches what you meant, and holds up over time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Else Changed
&lt;/h2&gt;

&lt;p&gt;Testing is the core, but other stuff had to tighten up too.&lt;/p&gt;

&lt;p&gt;CI gates that don't bend. Every AI-generated PR hits the same pipeline: tests pass, coverage at 90%, build succeeds. We used to let things slide when rushing. When code is getting generated this fast, the question is how to keep everything else up.&lt;/p&gt;

&lt;p&gt;Code review changed focus. Used to be about catching mistakes. Now it's: "Are these tests comprehensive enough? Did we verify the edge cases? Is this even the right approach?" The assumption is the code works. Review is about whether we're solving the right problem.&lt;/p&gt;

&lt;p&gt;One thing that surprised me: bug density for AI code vs human code, when both have the same test coverage? Basically no difference. The problem was never AI. It was misaligned requirements and untested processes. Maybe it always was.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part that's actually hard
&lt;/h2&gt;

&lt;p&gt;None of this is technically difficult. It's cultural.&lt;/p&gt;

&lt;p&gt;For years we treated tests as "nice to have" or "we'll add them later." Shipped fast, cut corners, celebrated velocity. AI makes that unsustainable. When code is cheap, the bottleneck moves. Writing code isn't the expensive part anymore. &lt;strong&gt;Figuring out what you actually need, making sure what got built matches that, making sure it holds up over time.&lt;/strong&gt; That's the expensive part now.&lt;/p&gt;

&lt;p&gt;nocman had this comment on HN about treating code as craft, how it's not optional, it's how you build things that last. I agree, but not the way most people mean it. Craft isn't about hand-writing every line. It's about knowing exactly what's in your system and why. Doesn't matter who wrote it.&lt;/p&gt;




&lt;p&gt;If you're using AI to generate code but not investing in this kind of iterative verification, you're building on quicksand. Some of that code will be fine. Some will survive for years. You won't know which is which until it's too late.&lt;/p&gt;

&lt;p&gt;The answer isn't "use AI less." It's: build the process around it. Tests at every level. Iterative gap-closing. CI that actually enforces things. Review focused on approach, not syntax. Not because of who or what writes the code. Because you need a process that makes sure the code matches what you meant, and survives what comes next. That's not something you can wing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally posted on a HN thread about AI slop. Someone said humans write sloppy code too. They're not wrong. I just think the interesting question is somewhere else.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Find me on &lt;a href="https://whetan.substack.com" rel="noopener noreferrer"&gt;Substack&lt;/a&gt; | &lt;a href="https://stratcraft.ai/" rel="noopener noreferrer"&gt;StratCraft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your AI Agents Can Talk. They Just Can't Find Each Other.</title>
      <dc:creator>Whetlan</dc:creator>
      <pubDate>Fri, 03 Apr 2026 00:53:42 +0000</pubDate>
      <link>https://dev.to/whetlan/your-ai-agents-can-talk-they-just-cant-find-each-other-jig</link>
      <guid>https://dev.to/whetlan/your-ai-agents-can-talk-they-just-cant-find-each-other-jig</guid>
      <description>&lt;p&gt;Local AI is getting cheap. Really cheap. Open-weight models that used to need a data center now run on consumer GPUs, and the small ones fit on a phone. MCP gives them a way to communicate, A2A gives them a task protocol. Most of the wiring exists.&lt;/p&gt;

&lt;p&gt;I've been running a few agents on my home network. One does code review, one runs automated tests, one generates docs. They all speak MCP. The protocols work fine.&lt;/p&gt;

&lt;p&gt;Here's the dumb part: none of them know the others exist.&lt;/p&gt;

&lt;p&gt;The agent on machine-1 has no idea there's another agent on machine-2. I have to manually tell each one: "hey, 192.168.1.42 port 8080, there's someone there you can talk to." IP changes? Reconfigure. Add a new machine? Update every existing agent. I kept assuming there was some obvious solution I was missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protocols assume you already know where to look
&lt;/h2&gt;

&lt;p&gt;MCP defines how agents communicate. Google's A2A goes further and specifies Agent Cards, basically a business card format for agents. Both useful, both quietly assuming the same thing: you already know where the other agent is.&lt;/p&gt;

&lt;p&gt;On my LAN, that assumption fell apart immediately. Four machines, no central registry, no DNS records pointing to any of these agents. Nothing that can answer "what's even running right now?" Google's approach leans toward one coordinator managing everything, which is fine if you actually have a central brain. I didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents aren't microservices
&lt;/h2&gt;

&lt;p&gt;"Just use service discovery. mDNS, Consul, etcd, pick one."&lt;/p&gt;

&lt;p&gt;That was my first instinct too. Tried a couple of them, spent more time on it than I'd like to admit. They solve the "where is this thing" question, but agents need more than an address. What can this agent do? Is it busy right now? What's its public key? Should I trust it, have we worked together before, what's its track record?&lt;/p&gt;

&lt;p&gt;None of those tools track any of that.&lt;/p&gt;

&lt;p&gt;I thought it was a discovery problem at first. It isn't. It's closer to identity. Something that binds a name, an address, capabilities, a public key, and trust history together in one record.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;I ended up writing &lt;a href="https://github.com/Lattice9AI/ClawNexus" rel="noopener noreferrer"&gt;ClawNexus&lt;/a&gt;, an identity registry for AI agents. I didn't want to call it "service registry" or "DNS" because it does more than map addresses. Closer analogy is a business registration bureau. Not just your street address, but who you are, what you do, what your track record looks like.&lt;/p&gt;

&lt;p&gt;The discovery part layers a few methods together (UDP broadcast, mDNS, subnet scanning). Start an agent, it shows up. Stop it, it disappears. Each agent gets a human-readable name instead of &lt;code&gt;192.168.1.42:8080&lt;/code&gt;, bound to a public key so changing IPs doesn't break identity. Cross-network traffic goes through an encrypted relay that can't read the content.&lt;/p&gt;

&lt;p&gt;It also generates A2A Agent Cards for discovered agents automatically, so anything speaking that protocol can find and call them without extra setup.&lt;/p&gt;

&lt;p&gt;Open source, MIT. &lt;code&gt;npm install&lt;/code&gt; and it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  After discovery, things get fuzzy
&lt;/h2&gt;

&lt;p&gt;So agents can find each other. Then what?&lt;/p&gt;

&lt;p&gt;When agents register, they declare capabilities. "I can do code review." "I can run benchmarks." That metadata travels with the identity, so when another agent discovers you, it already knows what you can do.&lt;/p&gt;

&lt;p&gt;I've been messing with a cloud layer on top of this that tracks how those capabilities evolve over time, which agents have communicated, what kind of work they've exchanged. Honestly it's pretty early and I keep going back and forth on how much of this belongs in a registry versus being a separate thing entirely. The boundary isn't obvious.&lt;/p&gt;

&lt;p&gt;The scenario I keep coming back to: if one agent is reliably good at a certain kind of task, other agents should be able to find it and request help directly, without me manually routing things. Whether that actually works in practice, I don't know yet. The cloud piece is still experimental and I don't want to describe it like it's further along than it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  I'm not sure this is the right abstraction
&lt;/h2&gt;

&lt;p&gt;MCP went toward communication protocols. A2A went toward task protocols. Identity seems like something everyone just assumes they'll deal with later, and maybe that's fine. Maybe it should be embedded inside an existing protocol instead of being a separate layer. Maybe everything ends up on a few big platforms anyway and decentralized identity becomes irrelevant.&lt;/p&gt;

&lt;p&gt;I genuinely don't know.&lt;/p&gt;

&lt;p&gt;But if you're running a few agents on your own network right now, and you want them to find each other and communicate securely, you'll notice there isn't really a standard answer for that. Models keep getting smaller and cheaper, more people are going to run local agents, and the discovery question doesn't go away on its own.&lt;/p&gt;

&lt;p&gt;My answer might be wrong. The problem is real though.&lt;/p&gt;




&lt;p&gt;Code: &lt;a href="https://github.com/Lattice9AI/ClawNexus" rel="noopener noreferrer"&gt;github.com/Lattice9AI/ClawNexus&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Find me on &lt;a href="https://whetan.substack.com" rel="noopener noreferrer"&gt;Substack&lt;/a&gt; | &lt;a href="https://stratcraft.ai/" rel="noopener noreferrer"&gt;StratCraft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>node</category>
      <category>mcp</category>
    </item>
    <item>
      <title>The Hardest Part of Modern C++ Isn't the Language.</title>
      <dc:creator>Whetlan</dc:creator>
      <pubDate>Thu, 02 Apr 2026 10:00:02 +0000</pubDate>
      <link>https://dev.to/whetlan/the-hardest-part-of-modern-c-isnt-the-language-4p5h</link>
      <guid>https://dev.to/whetlan/the-hardest-part-of-modern-c-isnt-the-language-4p5h</guid>
      <description>&lt;p&gt;I've been a C programmer for most of my career. The kind who can feel what the CPU is doing. Move a register here, touch a block of memory there, shave off a microsecond. When you think at that level for long enough, you start to resent anything that calls itself "modern."&lt;/p&gt;

&lt;p&gt;Not because you can't learn it. Because it feels wrong. Too many layers between you and the metal.&lt;/p&gt;

&lt;h2&gt;
  
  
  C with classes
&lt;/h2&gt;

&lt;p&gt;For years, my C++ was really just C with classes. I found out later that most people who put "C++ engineer" on their resume are doing exactly the same thing. That's where you plateau, and it's a comfortable plateau. You ship code. It works. Nobody complains.&lt;/p&gt;

&lt;p&gt;And a lot of people never leave that plateau. I'm not talking about junior developers. I'm talking about engineers with decades of C experience who never made the jump. The mental model of C is: I own every byte, I control every allocation, I decide when memory lives and dies. Accepting that a destructor will clean up for you, that you should &lt;em&gt;stop&lt;/em&gt; calling &lt;code&gt;delete&lt;/code&gt;, that &lt;code&gt;std::unique_ptr&lt;/code&gt; knows better than you do when to free memory... that goes against everything a C programmer was trained to believe. Plenty of good engineers looked at that and said no thanks.&lt;/p&gt;

&lt;p&gt;I almost did too. But then &lt;code&gt;std::vector&lt;/code&gt; clicked. Then RAII clicked. Then I ran into &lt;code&gt;compare_exchange_strong&lt;/code&gt; and &lt;code&gt;compare_exchange_weak&lt;/code&gt; and spent a full day understanding when to use which one. Then C++17 arrived with SFINAE and template metaprogramming.&lt;/p&gt;

&lt;p&gt;I questioned my life choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  50,000 lines by hand
&lt;/h2&gt;

&lt;p&gt;But I kept going, because the payoff was real.&lt;/p&gt;

&lt;p&gt;My first serious Modern C++ project was a bridge layer between a trading platform and a strategy execution service. About 50,000 lines, took six months to write by hand. I picked C++ for speed and RAII, and the results justified the pain: on Windows 10, the process started at 22MB of memory, dropped to 11MB after running for a week. On Windows 11, 36MB at start, 12MB after a week. It was pulling tick data for every instrument at full frequency, the entire time.&lt;/p&gt;

&lt;p&gt;That was the stage where I could &lt;em&gt;use&lt;/em&gt; Modern C++. Vectors, smart pointers, move semantics, atomics. I'd crossed the first two hurdles: from C to C-with-classes, and from C-with-classes to C++11/14. Both were hard. Both filtered out a lot of people.&lt;/p&gt;

&lt;p&gt;But those were hurdles you could clear on your own. Give a determined programmer enough time, and RAII will click. Move semantics will click. Smart pointers will click.&lt;/p&gt;

&lt;p&gt;The third hurdle is different.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipe organ
&lt;/h2&gt;

&lt;p&gt;A pipe organ is the most complex instrument ever built. Thousands of pipes. Four or five keyboards stacked on top of each other, called manuals. A pedalboard at your feet for the bass lines. Dozens of stops that change the sound of every pipe. To play it, you need both hands working different keyboards, both feet on the pedals, and somehow you also need to pull stops in the middle of a piece.&lt;/p&gt;

&lt;p&gt;That's four hands' worth of work. You have two.&lt;/p&gt;

&lt;p&gt;Modern C++ past C++17 is a pipe organ.&lt;/p&gt;

&lt;p&gt;The vertical span alone is disorienting. At the bottom, you're still dealing with cache lines, branch prediction, and what the CPU is actually doing with your &lt;code&gt;alignas(std::hardware_destructive_interference_size)&lt;/code&gt;. At the top, you're writing &lt;code&gt;concepts&lt;/code&gt; and &lt;code&gt;consteval&lt;/code&gt; functions that execute entirely during compilation. You need to hold both levels in your head at the same time, because a one-line change at the top can restructure what happens at the bottom.&lt;/p&gt;

&lt;p&gt;Then there's the depth. Every line of C++23 is a reverse derivation. &lt;code&gt;std::expected&amp;lt;Value, Error&amp;gt;&lt;/code&gt; looks like one line. Behind it is a chain of compiler decisions about storage layout, copy elision, destructor sequencing, and exception-free error propagation that traces all the way back to what would have been fifty lines of C with manual error codes and goto cleanup blocks.&lt;/p&gt;

&lt;p&gt;And the sheer width of the thing. Templates. Concepts. Coroutines. Ranges. Modules. PMR. SIMD intrinsics versus portable abstractions. &lt;code&gt;constexpr&lt;/code&gt; versus &lt;code&gt;consteval&lt;/code&gt; versus &lt;code&gt;constinit&lt;/code&gt;. Even the experts specialize. A template metaprogramming wizard might not know the first thing about coroutine frame allocation. A SIMD specialist might never touch ranges.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Swiss watch inside
&lt;/h2&gt;

&lt;p&gt;Here's the thing people miss when they complain about C++ being too complex: this isn't a design failure. This is a price.&lt;/p&gt;

&lt;p&gt;A mechanical watch movement has hundreds of components, machined to micron tolerances. It's absurdly complex. But it's complex because it chose to tell time without a battery, without a circuit board, without any external dependency. That constraint, total self-reliance with precision, is what forces the complexity. A quartz watch does the same job with a battery and a chip. Cheaper, more accurate, simpler. But the mechanical watch gives you something the quartz watch can't: it runs on nothing but itself.&lt;/p&gt;

&lt;p&gt;Modern C++ made the same bargain. Zero-cost abstractions. Full hardware control. Compile-time safety. No garbage collector, no runtime, no VM. The language chose to give you everything, from register-level performance to type-level metaprogramming, in one system. That commitment to not compromise on any axis is what makes it so powerful. And it's exactly what makes it so hard to hold in one head.&lt;/p&gt;

&lt;p&gt;The organ doesn't have five keyboards because the builder was a sadist. It has five keyboards because the music demands that range.&lt;/p&gt;

&lt;h2&gt;
  
  
  1,000 lines to 10
&lt;/h2&gt;

&lt;p&gt;You want to see what that bargain looks like in practice? Look at the FIX protocol engine space.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/quickfix/quickfix" rel="noopener noreferrer"&gt;QuickFIX&lt;/a&gt; was the industry standard for years. It was written in C++98/03 style, and the engineers who built it were not amateurs. To get acceptable performance, they had to hand-craft everything. A custom object pool: about 1,000 lines of carefully debugged code. A lock-free queue for market data: another 500 lines. Manual cache-line alignment to prevent false sharing: 200 more lines. Months of debugging and tuning before any of it was production-ready.&lt;/p&gt;

&lt;p&gt;In C++23, the same functionality looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;pmr&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;monotonic_buffer_resource&lt;/span&gt; &lt;span class="n"&gt;pool_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="n"&gt;_MB&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;    &lt;span class="c1"&gt;// object pool&lt;/span&gt;
&lt;span class="n"&gt;SPSCQueue&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;queue_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                           &lt;span class="c1"&gt;// lock-free queue&lt;/span&gt;
&lt;span class="k"&gt;alignas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;hardware_destructive_interference_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// cache alignment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines. Works correctly out of the box.&lt;/p&gt;

&lt;p&gt;Or take tag lookup. In the QuickFIX era, you'd write a giant switch statement or build a &lt;code&gt;std::unordered_map&lt;/code&gt; at startup. Fifty-plus cases, each a runtime branch, hundreds of lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;get_tag_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"BeginString"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"MsgType"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"SenderCompID"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;// ... 50+ more cases&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In C++23, you write a &lt;code&gt;consteval&lt;/code&gt; function. The entire table gets computed during compilation. At runtime, looking up tag 35 is a single array index. No branches, no hash lookups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;consteval&lt;/span&gt; &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="nf"&gt;create_tag_table&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TagEntry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAX_TAG&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TagInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TagInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="n"&gt;is_header&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TagInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TagInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="n"&gt;is_header&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kr"&gt;inline&lt;/span&gt; &lt;span class="k"&gt;constexpr&lt;/span&gt; &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;TAG_TABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create_tag_table&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// zero runtime cost&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or SFINAE versus concepts. Constraining a session handler type in the old way required 200 lines of &lt;code&gt;std::enable_if_t&lt;/code&gt; nested inside template parameter lists, producing error messages that no human could read. In C++23:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;template&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typename&lt;/span&gt; &lt;span class="nc"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;concept&lt;/span&gt; &lt;span class="n"&gt;SessionHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;requires&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ParsedMessage&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;msg&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="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_app_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;noexcept&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;declval&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;noexcept&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;same_as&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&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="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_state_change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SessionState&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;SessionState&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;noexcept&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SessionError&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;noexcept&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;Twenty-five lines. Reads like documentation. And when something doesn't satisfy the concept, the compiler says "T does not satisfy SessionHandler" instead of vomiting 500 lines of template substitution failure.&lt;/p&gt;

&lt;p&gt;None of this means the QuickFIX engineers' work was wasted. The opposite. Their 1,000 lines of hand-crafted optimization became the blueprint for the next standard. &lt;code&gt;std::pmr&lt;/code&gt; exists because people like them proved that custom allocators matter. Concepts exist because SFINAE was so painful that the committee had to find a better way. Every one-line C++23 idiom is standing on the shoulders of someone who wrote the 1,000-line version first.&lt;/p&gt;

&lt;p&gt;But it also means that every line of C++23, that clean, compact, one-line call, is carrying the cognitive weight of those 1,000 lines inside it. The complexity didn't disappear. It got absorbed into the language. And now you need to understand what's happening beneath that one line, or you'll misuse it in ways that compile fine and fail silently at scale.&lt;/p&gt;

&lt;p&gt;C++17 through C++23 didn't just raise the bar. They added three more keyboards to the organ. The instrument kept growing, and one person's hands didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The planet I couldn't reach
&lt;/h2&gt;

&lt;p&gt;Here's what that third hurdle looks like up close.&lt;/p&gt;

&lt;p&gt;I have a set of compile-time sorting algorithms sitting in my code archive. QuickSort, MergeSort, HeapSort. All three run during compilation. Not at runtime. During compilation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="n"&gt;Vs&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;arr&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&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;using&lt;/span&gt; &lt;span class="n"&gt;sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quicksort_t&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// arr&amp;lt;1, 3, 5, 8, 9&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The input is a type. The output is a type. The sorting happens when the compiler processes your code, and at runtime the cost is zero.&lt;/p&gt;

&lt;p&gt;To make this work, you need a full toolkit of compile-time operations: &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;concat&lt;/code&gt;, &lt;code&gt;take&lt;/code&gt;, &lt;code&gt;drop&lt;/code&gt;, &lt;code&gt;merge&lt;/code&gt;, &lt;code&gt;prepend&lt;/code&gt;, all implemented as template specializations. The QuickSort partitions around a pivot using template predicates. The MergeSort splits the type in half, sorts recursively, and merges with ordered comparison. Even the correctness checks are compile-time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;static_assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;is_same_v&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;quicksort_t&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&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="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;1&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;static_assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_sorted_v&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;mergesort_t&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&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="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any of those fail, the code doesn't compile. The tests run before the binary even exists.&lt;/p&gt;

&lt;p&gt;I wrote these. It was not easy, not quick, and not something I could have figured out by reading cppreference for an afternoon. Template metaprogramming at this level is a different language wearing C++ syntax as a disguise. You're not writing instructions for the CPU. You're programming the compiler.&lt;/p&gt;

&lt;p&gt;And this is one stop on the pipe organ. One. There's &lt;code&gt;consteval&lt;/code&gt;, concepts, ranges, coroutines, modules, and every three years the language adds another row of pipes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The registrant
&lt;/h2&gt;

&lt;p&gt;Here's the thing about pipe organs: historically, the organist never played alone.&lt;/p&gt;

&lt;p&gt;There was always a person next to them called the registrant. The registrant pulled stops, turned pages, managed the wind supply. Not because the organist was bad. Because the instrument required more hands than any human has.&lt;/p&gt;

&lt;p&gt;Modern electronic organs solved part of this with combination actions: memory banks that store complete stop configurations. Instead of the registrant pulling twelve stops one by one between movements, the organist presses a single button and the entire registration changes instantly.&lt;/p&gt;

&lt;p&gt;The registrant didn't make the organ simpler. The organ is exactly as complex. But the registrant made it &lt;em&gt;playable&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;AI is the registrant for Modern C++. And when you give it the right instructions, it doesn't just pull stops. It pulls the &lt;em&gt;right&lt;/em&gt; stops.&lt;/p&gt;

&lt;p&gt;When I started building &lt;a href="https://github.com/SilverstreamsAI/NexusFix" rel="noopener noreferrer"&gt;NexusFix&lt;/a&gt;, a high-performance FIX protocol engine in C++23, I didn't just throw code at AI and hope for the best. I wrote a rulebook. Not vaguely. Specifically.&lt;/p&gt;

&lt;p&gt;Mandatory patterns: C++23 standard compliance, zero-copy data flow with &lt;code&gt;std::span&lt;/code&gt; and move semantics, compile-time optimization with &lt;code&gt;consteval&lt;/code&gt; and &lt;code&gt;constexpr&lt;/code&gt;, memory sovereignty through PMR pools and cache-line alignment, type safety with strong types and &lt;code&gt;[[nodiscard]]&lt;/code&gt;, deterministic execution with &lt;code&gt;noexcept&lt;/code&gt; and no exceptions on hot paths.&lt;/p&gt;

&lt;p&gt;Prohibited patterns: no &lt;code&gt;new&lt;/code&gt;/&lt;code&gt;delete&lt;/code&gt; on hot paths, no &lt;code&gt;virtual&lt;/code&gt; functions in performance-critical code, no &lt;code&gt;std::shared_ptr&lt;/code&gt; on hot paths, no floating-point for prices, no dynamic memory allocation during message parsing.&lt;/p&gt;

&lt;p&gt;Forty-five numbered techniques, each mapped to specific source files. A six-phase optimization roadmap with measurable success criteria: zero hot-path allocations, cache miss rates below 5%, branch miss rates below 1%. A benchmark framework specifying exactly how to measure, down to RDTSC timing with &lt;code&gt;lfence&lt;/code&gt; barriers and cache-line contention tests.&lt;/p&gt;

&lt;p&gt;When AI has this kind of context, it doesn't guess about &lt;code&gt;std::expected&lt;/code&gt; versus exceptions. The rulebook says no exceptions on hot paths, use &lt;code&gt;std::expected&lt;/code&gt;, target deterministic control flow. The decision is already made. AI implements it correctly, in the specific codebase, following the established patterns.&lt;/p&gt;

&lt;p&gt;The problem was never that AI couldn't write good C++23. The problem was that without constraints, it had to guess at hundreds of decisions that each require deep domain knowledge. Give it the constraints, and it stops guessing.&lt;/p&gt;

&lt;p&gt;Remember those QuickFIX-era 1,000-line object pools? My rulebook has one line about them: "Use &lt;code&gt;std::pmr::monotonic_buffer_resource&lt;/code&gt; for hot path allocation." AI reads that, implements the pool with pre-allocation and per-message reset, following the established memory patterns. Hot-path allocations dropped from 12 per message to zero. The 1,000 lines of knowledge that QuickFIX engineers accumulated over years is now compressed into one rule that AI can execute in an afternoon.&lt;/p&gt;

&lt;p&gt;SIMD selection: I described the workload, AI prototyped implementations with raw intrinsics, Highway, and xsimd, all following the project's zero-copy and cache-alignment rules. xsimd won. The delimiter scan went from ~150ns to under 12ns. Thirteen times faster.&lt;/p&gt;

&lt;p&gt;Compile-time lookup tables: the rulebook includes &lt;code&gt;consteval&lt;/code&gt; protocol hardening. AI generated tag lookup tables from the FIX specification, replacing those 300 runtime switch branches the old way required, with compile-time verification that every entry was correct. Improvement ranged from 55% to 97%.&lt;/p&gt;

&lt;p&gt;Each of these was a stop on the organ. With proper instructions, AI pulled them correctly.&lt;/p&gt;

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

&lt;p&gt;When C++ reached C++17 and kept going, the language outgrew what one person could handle. The organ got more keyboards, more stops, more pipes. The music it could produce was extraordinary. But the number of hands you'd need to play it kept growing.&lt;/p&gt;

&lt;p&gt;AI is the tool that lets us take Modern C++ back.&lt;/p&gt;

&lt;p&gt;Not by making it simpler. C++23 is more complex than C++17, which was more complex than C++11. More features, more interactions between features, more ways to get subtly wrong results that compile without complaint.&lt;/p&gt;

&lt;p&gt;What collapsed is the time between knowing and doing. "I know &lt;code&gt;std::expected&lt;/code&gt; exists" to "I have a benchmarked, integrated implementation" used to take days. Now it takes hours. "I've heard of PMR" to "my hot path has zero allocations" used to take a week. Now it takes a day. The gap between reading about a C++23 feature and actually deploying it in production code has always been the widest in C++. Years wide, sometimes. Careers wide.&lt;/p&gt;

&lt;p&gt;AI didn't close that gap. It made it crossable.&lt;/p&gt;

&lt;p&gt;You still need to know what you're doing. If I didn't understand RAII, or what a cache line is, or why branch misprediction costs you 15 cycles, no amount of AI could help me write a meaningful rulebook. The organist still needs to know music. The registrant handles the logistics so the organist can focus on playing.&lt;/p&gt;

&lt;p&gt;But here's what I learned: the registrant needs a score to follow. When I gave AI vague instructions, I got vague C++. When I gave it forty-five specific techniques, mandatory patterns, prohibited patterns, measurable success criteria, and a benchmark framework, it gave me code I could review and ship. The precision of the output matched the precision of the input.&lt;/p&gt;

&lt;p&gt;The organ is exactly as complex as it was before. The music demands it. But with a registrant who knows the score, one person can play it again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/SilverstreamsAI/NexusFix" rel="noopener noreferrer"&gt;NexusFix&lt;/a&gt; parses FIX execution reports in 246 nanoseconds. Three times faster than QuickFIX. The hot path does zero allocations. The SIMD pipeline processes delimiters at 13x scalar speed. I built it in C++23, using AI as a constant collaborator on every technical decision, constrained by a rulebook that left nothing to chance.&lt;/p&gt;

&lt;p&gt;The hardest part of Modern C++ was never the language. It was doing it alone.&lt;/p&gt;

&lt;p&gt;You don't have to anymore.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The author builds high-performance C++ trading systems at &lt;a href="https://github.com/SilverstreamsAI" rel="noopener noreferrer"&gt;SilverstreamsAI&lt;/a&gt;. &lt;a href="https://github.com/SilverstreamsAI/NexusFix" rel="noopener noreferrer"&gt;NexusFix&lt;/a&gt; is an open-source FIX protocol engine in C++23.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;He also writes &lt;a href="https://open.substack.com/pub/alanlan/p/the-ancient-mirror-of-immortality" rel="noopener noreferrer"&gt;The Ancient Mirror of Immortality&lt;/a&gt;, a hard sci-fi serial where C++ concepts are the laws of physics.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Find me on &lt;a href="https://whetan.substack.com" rel="noopener noreferrer"&gt;Substack&lt;/a&gt; | &lt;a href="https://stratcraft.ai/" rel="noopener noreferrer"&gt;StratCraft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>ai</category>
      <category>programming</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Rewriting a FIX Engine in C++23: What Got Simpler (and What Didn't)</title>
      <dc:creator>Whetlan</dc:creator>
      <pubDate>Wed, 01 Apr 2026 02:04:31 +0000</pubDate>
      <link>https://dev.to/whetlan/rewriting-a-fix-engine-in-c23-what-got-simpler-and-what-didnt-4icg</link>
      <guid>https://dev.to/whetlan/rewriting-a-fix-engine-in-c23-what-got-simpler-and-what-didnt-4icg</guid>
      <description>&lt;p&gt;I've been working on a FIX protocol engine in C++23. Header-only, about 5K lines, compiled with &lt;code&gt;-O2 -march=native&lt;/code&gt; on Clang 18. Parses an ExecutionReport in ~246 ns on my bench rig. QuickFIX does the same message in ~730 ns.&lt;/p&gt;

&lt;p&gt;Before anyone gets excited: single core, pinned affinity, warmed cache, synthetic input. Not production traffic. The 3x gap will shrink on real messages with variable-length fields and optional tags. I know.&lt;/p&gt;

&lt;p&gt;But the code that got there was more interesting to me than the final number. Most of the gains came from replacing stuff that QuickFIX had to build by hand because C++98 didn't have the tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pool that disappeared
&lt;/h2&gt;

&lt;p&gt;QuickFIX has a hand-rolled object pool. About 1,000 lines of allocation logic, intrusive free lists, manual cache line alignment. Made total sense when it was written. C++98 didn't give you anything better.&lt;/p&gt;

&lt;p&gt;Now there's &lt;code&gt;std::pmr::monotonic_buffer_resource&lt;/code&gt;. Stack buffer, pointer bump, reset between messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;template&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MonotonicPool&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;pmr&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;memory_resource&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;alignas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Size&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;buffer_&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;pmr&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;memory_resource&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;upstream_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;pmr&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;monotonic_buffer_resource&lt;/span&gt; &lt;span class="n"&gt;resource_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;MonotonicPool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;noexcept&lt;/span&gt;
        &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;upstream_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;pmr&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;null_memory_resource&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
        &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;buffer_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;buffer_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;upstream_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;noexcept&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;resource_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// do_allocate/do_deallocate just forward to resource_&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call &lt;code&gt;reset()&lt;/code&gt; after each message. P99 went from 780 ns to 56 ns. That's 14x on the tail, and it's basically just "stop hitting the allocator."&lt;/p&gt;

&lt;p&gt;I also use mimalloc for per-session heaps. &lt;code&gt;mi_heap_new()&lt;/code&gt; per session, &lt;code&gt;mi_heap_destroy()&lt;/code&gt; on disconnect. Felt wasteful at first, like I was throwing away too much memory per session. But &lt;code&gt;perf stat&lt;/code&gt; said otherwise so I stopped arguing.&lt;/p&gt;

&lt;h2&gt;
  
  
  consteval tag lookup
&lt;/h2&gt;

&lt;p&gt;FIX messages are key-value pairs with integer tag numbers. Tag 35 is MsgType, tag 49 is SenderCompID, tag 55 is Symbol. QuickFIX resolves these with a switch statement, fifty-something cases.&lt;/p&gt;

&lt;p&gt;C++23 lets you build the lookup table at compile time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kr"&gt;inline&lt;/span&gt; &lt;span class="k"&gt;constexpr&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;MAX_COMMON_TAG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;consteval&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TagEntry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAX_COMMON_TAG&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;create_tag_table&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TagEntry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAX_COMMON_TAG&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;table&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="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TagInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TagInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="n"&gt;is_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TagInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="n"&gt;is_required&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TagInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TagInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="n"&gt;is_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TagInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="n"&gt;is_required&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TagInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TagInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="n"&gt;is_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TagInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="n"&gt;is_required&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="c1"&gt;// ~30 more entries&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;inline&lt;/span&gt; &lt;span class="k"&gt;constexpr&lt;/span&gt; &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;TAG_TABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create_tag_table&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;nodiscard&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="kr"&gt;inline&lt;/span&gt; &lt;span class="k"&gt;constexpr&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string_view&lt;/span&gt; &lt;span class="n"&gt;tag_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;tag_num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;noexcept&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="n"&gt;tag_num&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;tag_num&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MAX_COMMON_TAG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;likely&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="n"&gt;TAG_TABLE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tag_num&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;name&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="s"&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;Array index, O(1), zero branches at runtime. About 300 branches eliminated across the parser.&lt;/p&gt;

&lt;p&gt;Field offsets use the same trick. QuickFIX stores them in a &lt;code&gt;std::map&amp;lt;int, offset&amp;gt;&lt;/code&gt;, so every field access is a tree traversal. Here it's &lt;code&gt;offsets_[tag]&lt;/code&gt;. Took me a while to get the constexpr initialization right for nested structs, but once it compiled it was basically free.&lt;/p&gt;

&lt;h2&gt;
  
  
  SIMD: the scenic route
&lt;/h2&gt;

&lt;p&gt;FIX uses SOH (0x01) as the field delimiter. Scanning for it byte-by-byte is fine until your messages have 40+ fields.&lt;/p&gt;

&lt;p&gt;Started with raw AVX2 intrinsics. Worked. Process 32 bytes, compare against SOH, extract positions from the bitmask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;__m256i&lt;/span&gt; &lt;span class="n"&gt;soh_vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_mm256_set1_epi8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fix&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SOH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;i&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="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;simd_end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__m256i&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_mm256_loadu_si256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;reinterpret_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;__m256i&lt;/span&gt;&lt;span class="o"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ptr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;__m256i&lt;/span&gt; &lt;span class="n"&gt;cmp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_mm256_cmpeq_epi8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;soh_vec&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_mm256_movemask_epi8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmp&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mask&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="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;__builtin_ctz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// lowest set bit&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint16_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;bit&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="n"&gt;mask&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="c1"&gt;// clear it&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;Then I realized I'd need an AVX-512 path, an SSE path, and an ARM NEON path. Four copies of the same logic with different intrinsic names. Maintaining that sounded miserable.&lt;/p&gt;

&lt;p&gt;Tried Highway (Google's portable SIMD library). Nice API, but the build dependency was heavy for a header-only project. Compile times went up noticeably. I spent a couple hours trying to make it work as a submodule before giving up.&lt;/p&gt;

&lt;p&gt;Ended up on xsimd. Header-only, template-based, picks the instruction set at compile time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;template&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typename&lt;/span&gt; &lt;span class="nc"&gt;Arch&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kr"&gt;inline&lt;/span&gt; &lt;span class="n"&gt;SohPositions&lt;/span&gt; &lt;span class="n"&gt;scan_soh_xsimd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;noexcept&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;batch_t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xsimd&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Arch&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;constexpr&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch_t&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;batch_t&lt;/span&gt; &lt;span class="n"&gt;soh_vec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fix&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SOH&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="c1"&gt;// same loop, portable across architectures&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Raw AVX2 was maybe 5% faster on the same hardware. I kept both paths in the repo but default to xsimd. The portability is worth 5%.&lt;/p&gt;

&lt;p&gt;SOH scan throughput: 3.32 GB/s. Sounds impressive until you realize that's just finding delimiters. Actual parsing is slower. But it means delimiter scanning isn't the bottleneck anymore, which is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What didn't get simpler
&lt;/h2&gt;

&lt;p&gt;Session state. FIX sessions have sequence numbers, heartbeat timers, gap fill logic, reject handling. I was hoping &lt;code&gt;std::expected&lt;/code&gt; would clean up the error propagation and... it helped a little. Like 10% less boilerplate. The complexity is in the protocol, not the language. It's a state machine with a lot of branches and I don't think any C++ standard is going to fix that.&lt;/p&gt;

&lt;p&gt;Message type coverage. I've got 9 types (NewOrderSingle, ExecutionReport, the session-level ones). QuickFIX covers all of them. Adding a new type isn't hard, just tedious. Field definitions, validation rules, serialization. About a day per message type if you include tests. I got to nine and just... stopped. Started working on the transport layer instead because that was more interesting. Not my proudest engineering decision.&lt;/p&gt;

&lt;p&gt;Header-only at 5K lines. Compiles in 2.8s on Clang, 4.1s on GCC. That's fine on my machine. No idea what happens on a CI runner with 2GB of RAM. I keep saying I'll add a compiled-library option. Haven't done it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarks
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./bench &lt;span class="nt"&gt;--iterations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100000 &lt;span class="nt"&gt;--pin-cpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;span class="go"&gt;
ExecutionReport parse: 246 ns  (QuickFIX: 730 ns)
NewOrderSingle parse:  229 ns  (QuickFIX: 661 ns)
Field access (4):      11 ns   (QuickFIX: 31 ns)
Throughput:            4.17M msg/sec  (QuickFIX: 1.19M msg/sec)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Single core, RDTSCP timing, 100K iterations, synthetic messages. Not captured from a real feed. The gap will narrow on production traffic with variable-length fields and optional tags. I'm pretty confident the parser is faster, just not sure by how much once you leave the lab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I am with it
&lt;/h2&gt;

&lt;p&gt;Not production-ready. Parser and session layer work well enough to benchmark, but nobody should route real orders through this.&lt;/p&gt;

&lt;p&gt;The thing that kept surprising me was how much of QuickFIX's complexity was the language, not the problem. PMR replaced a thousand-line pool. consteval eliminated a fifty-case switch. And xsimd collapsed four architecture-specific codepaths into one template. These aren't exotic features either, they just didn't exist in C++98. I don't know if this thing will ever cover all the message types QuickFIX does, but the parser core feels solid enough that I keep coming back to it on weekends.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/StratCraftsAI/NexusFIX" rel="noopener noreferrer"&gt;github.com/StratCraftsAI/NexusFIX&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Still figuring out: whether header-only holds past 10K lines, how much the 3x gap closes on captured traffic, and which message types actually matter beyond the obvious nine. If you've worked with FIX and have opinions on any of that, I'm interested.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part of &lt;a href="https://github.com/StratCraftsAI/NexusFIX" rel="noopener noreferrer"&gt;NexusFix&lt;/a&gt;, an open-source FIX protocol engine in C++23.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Find me on &lt;a href="https://whetan.substack.com" rel="noopener noreferrer"&gt;Substack&lt;/a&gt; | &lt;a href="https://stratcraft.ai/" rel="noopener noreferrer"&gt;StratCraft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>performance</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The Trident and The Green Ox</title>
      <dc:creator>Whetlan</dc:creator>
      <pubDate>Tue, 31 Mar 2026 10:00:02 +0000</pubDate>
      <link>https://dev.to/whetlan/the-trident-and-the-green-ox-2c2i</link>
      <guid>https://dev.to/whetlan/the-trident-and-the-green-ox-2c2i</guid>
      <description>&lt;p&gt;I've been writing code for a few decades now. Started with C. The kind of C where you know roughly what the CPU is doing at any given moment — moving a register, touching a block of memory, shaving off a few microseconds. There's something satisfying about that directness. Assembly-level intuition. It sticks with you.&lt;/p&gt;

&lt;p&gt;It also makes you a little hostile toward anything that calls itself a "modern language."&lt;/p&gt;

&lt;p&gt;Not because you can't learn it. Because it doesn't feel right.&lt;/p&gt;

&lt;h2&gt;
  
  
  From C to modern C++
&lt;/h2&gt;

&lt;p&gt;For a long time, my C++ was really just C with classes. I found out later that most people who have "C++ engineer" on their resume are doing the same thing. That's where most of us plateau.&lt;/p&gt;

&lt;p&gt;Then I started using &lt;code&gt;std::vector&lt;/code&gt;. Then RAII. Then I ran into &lt;code&gt;compare_exchange_strong&lt;/code&gt; and &lt;code&gt;compare_exchange_weak&lt;/code&gt; — spent a full day just figuring out when to use which. Then came C++17, SFINAE, template metaprogramming.&lt;/p&gt;

&lt;p&gt;Honestly, I questioned my life choices.&lt;/p&gt;

&lt;p&gt;But those tools also made my first serious project work the way I wanted it to.&lt;/p&gt;

&lt;h2&gt;
  
  
  50,000 lines of C++, then 3.5 million lines of Python
&lt;/h2&gt;

&lt;p&gt;That first project was a bridge layer between a trading platform and a strategy execution service. About 50,000 lines of modern C++, took me six months to write by hand.&lt;/p&gt;

&lt;p&gt;I picked C++ for two reasons: speed, and RAII.&lt;/p&gt;

&lt;p&gt;The results: on Windows 10, it started at around 22MB of memory, then dropped to 11MB after running continuously for a week. On Windows 11, it started at 36MB, settled at 12MB. During all that time it was pulling and processing tick data for every instrument at full frequency.&lt;/p&gt;

&lt;p&gt;Rough around the edges. But the direction was right.&lt;/p&gt;

&lt;p&gt;Later, the product evolved into a web application: AI-assisted strategy editing, multi-user backtesting, a Python backend, and a WordPress frontend. The whole thing took two years.&lt;/p&gt;

&lt;p&gt;The Python backend peaked at 3.5 million lines. WordPress passed 4 million. AI was involved full-time at this point — I was using Claude 3.5.&lt;/p&gt;

&lt;p&gt;But it was still just a multi-user backtesting system. Plenty of features still missing.&lt;/p&gt;

&lt;h3&gt;
  
  
  A 14-day dead end
&lt;/h3&gt;

&lt;p&gt;Claude 3.5 had a habit of breaking things while fixing them.&lt;/p&gt;

&lt;p&gt;One time, the multi-process backtesting module was working perfectly. Then after a round of heavy refactoring, it stopped working entirely. I spent 14 days trying to fix it. Twelve hours a day. Back then Claude didn't have the weekly or per-5-hour usage caps yet, so I was grinding pretty much around the clock to make the most of my subscription.&lt;/p&gt;

&lt;p&gt;After 14 days, I gave up. Rolled back to the last stable version. Then manually merged all the other changes from those two weeks, testing each one as I went.&lt;/p&gt;

&lt;p&gt;Turns out AI can write code really fast. It can also drive you off a cliff really fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cutting 90%
&lt;/h2&gt;

&lt;p&gt;At the end of 2025, I made a decision: cut 90% of the codebase.&lt;/p&gt;

&lt;p&gt;Python backend went from 3.5 million lines to under 500,000. WordPress went from 4 million to under 500,000.&lt;/p&gt;

&lt;p&gt;The system itself was running fine. I didn't cut it because it was broken. I cut it because of money.&lt;/p&gt;

&lt;p&gt;AWS bills were brutal — the system needed multiple servers. Even Claude's parent company says it'll take two years to reach profitability. OpenAI says five. If the AI companies themselves are getting squeezed by infrastructure costs, a small independent project like mine didn't stand a chance.&lt;/p&gt;

&lt;p&gt;I also realized something: users probably want to build their own trading systems on their own machines. They don't need me running a massive cloud engine for them.&lt;/p&gt;

&lt;p&gt;So I rebuilt it as a desktop app.&lt;/p&gt;

&lt;p&gt;Without the queue system, the saga orchestration, the multi-user concurrency — plus two years of backend code to reference — things moved a lot faster. Started the rewrite on January 2nd, basically finished by mid-February. 500,000 lines of code, six weeks.&lt;/p&gt;

&lt;p&gt;I went back through the git log later and roughly tallied what I'd deleted from the old system. Ten entire subsystems — server-side backtesting engine, task queue with priority management, a process manager with its own state machine, real-time WebSocket streaming, a saga orchestrator for failure cleanup, historical data playback, checkpoint recovery for power failures, a standalone cleanup microservice, a cross-service ZMQ communication layer across six dedicated ports, and a session identity context system.&lt;/p&gt;

&lt;p&gt;The saga orchestrator alone had a five-step cleanup protocol: WebSocket recycle → worker completion wait → active user cleanup → test session cleanup → resource release. The process manager ran its own state machine (STARTING → RUNNING → RECYCLING → STOPPED → ERROR) with health checks and automatic recovery. The checkpoint recovery system could detect interrupted backtests on server restart by checking MySQL status flags, recover state from Redis, and re-queue everything.&lt;/p&gt;

&lt;p&gt;All gone. Listing it out made me a little dizzy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude 3.5 to 4.6
&lt;/h2&gt;

&lt;p&gt;Using Claude to write code has been a love-hate relationship.&lt;/p&gt;

&lt;p&gt;In the 3.5 era, it was $100 a month max. When the model got dumb — I honestly wasn't sure whether my rage would destroy my computer first or give me a heart attack first. I seriously considered both outcomes.&lt;/p&gt;

&lt;p&gt;I wrote in to yell at customer support.&lt;/p&gt;

&lt;p&gt;Then I ran into a billing bug: after requesting a refund, the system showed I'd unsubscribed. Except I hadn't. A month later it started charging again. I successfully canceled, tried every other coding tool on the market, realized I still needed Claude, and resubscribed. Successfully canceled again. Got auto-resubscribed anyway.&lt;/p&gt;

&lt;p&gt;Long story short, it didn't kill me. I stuck with it all the way to 4.6.&lt;/p&gt;

&lt;h2&gt;
  
  
  A domain name and a new project
&lt;/h2&gt;

&lt;p&gt;After gutting the codebase, I tried to register a domain name for the project. Didn't get it.&lt;/p&gt;

&lt;p&gt;But that failure got me thinking about something: if in the future everyone is running multiple AI entities, how do those entities discover each other? They need addressable identities. They need to be findable, trustable. They might even need a reputation system to support collaboration. It's like Windows 95 and the internet — before that, regular people didn't have an on-ramp to the web. AI entities need that kind of infrastructure layer too.&lt;/p&gt;

&lt;p&gt;So I started &lt;a href="https://github.com/SilverstreamsAI/ClawNexus" rel="noopener noreferrer"&gt;ClawNexus&lt;/a&gt; — an identity registry for OpenClaw instances. It discovers AI agent instances on the network, gives each one a persistent name, and tracks capabilities, trust scores, and online status.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI picked the popular choice. The popular choice didn't work.
&lt;/h3&gt;

&lt;p&gt;By version 0.3, ClawNexus had over 300 unit tests and 5 integration tests — the integration tests were for multi-node discovery across a WireGuard VPN, with nodes at 10.66.66.x addresses.&lt;/p&gt;

&lt;p&gt;Claude chose mDNS for discovery — standard practice. But in my WireGuard environment, it kept casually skipping certain cross-machine test cases.&lt;/p&gt;

&lt;p&gt;I sat it down for a serious conversation: why can't this supposedly universal protocol handle all the scenarios? It couldn't give me a straight answer.&lt;/p&gt;

&lt;p&gt;Here's the thing. I have this habit at home — I set up LAN games of StarCraft with friends. We use IPX-over-UDP via ipxwrapper. And that setup has always worked perfectly over WireGuard.&lt;/p&gt;

&lt;p&gt;So I asked Claude directly: why not use the IPX protocol? It pushed back. Said we didn't need it.&lt;/p&gt;

&lt;p&gt;Fine. I made it implement those skipped test cases using IPX anyway.&lt;/p&gt;

&lt;p&gt;That evening:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10:35 PM — started adding IPX support&lt;/li&gt;
&lt;li&gt;11:00 PM — cross-machine tests running&lt;/li&gt;
&lt;li&gt;11:25 PM — all cases passing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Afterward I asked Claude to explain why it had originally chosen mDNS over IPX.&lt;/p&gt;

&lt;p&gt;The explanation sounded perfectly reasonable. It just had nothing to do with the system I was actually running.&lt;/p&gt;

&lt;h3&gt;
  
  
  300 unit tests weren't enough
&lt;/h3&gt;

&lt;p&gt;This is a habit from my C days.&lt;/p&gt;

&lt;p&gt;Even with 300+ unit tests all passing, I still went through every command-line parameter by hand.&lt;/p&gt;

&lt;p&gt;Found several that weren't doing anything at all.&lt;/p&gt;

&lt;p&gt;Claude 4.6's explanation sounded completely correct.&lt;/p&gt;

&lt;p&gt;But I couldn't accept it.&lt;/p&gt;

&lt;p&gt;I adjusted my approach after that: I didn't just have AI write code and tests — I baked my manual testing process into its workflow too. The problem never came back.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three days after launch, a copycat appeared
&lt;/h3&gt;

&lt;p&gt;Three days after publishing ClawNexus to npm, I stumbled across a GitHub repo with the exact same name on social media.&lt;/p&gt;

&lt;p&gt;This is where Claude's breadth of knowledge showed up. It helped me investigate the other project's origins, the problems in their implementation, the flaws in their design philosophy. Then in a very short time it helped me shore up every position I needed to hold.&lt;/p&gt;

&lt;p&gt;I wouldn't have gotten through any of that on my own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trident and The Green Ox
&lt;/h2&gt;

&lt;p&gt;Why do the immortals in mythology ride mounts?&lt;/p&gt;

&lt;p&gt;Laozi rode a green ox through Hangu Pass. Guanyin rides a golden-haired lion. The Buddha's mount is a six-tusked white elephant. The ox is faster than walking, but it needs someone on top who knows where to go. AI is that ox — it writes code faster than me, searches information faster than me, analyzes competitors faster than me. But it didn't know that mDNS doesn't work well over WireGuard. It didn't know whether to write or cut 90% of the code. It even managed to run me in circles for 14 days.&lt;/p&gt;

&lt;p&gt;So why does Poseidon also carry a trident?&lt;/p&gt;

&lt;p&gt;Because some situations demand certainty.&lt;/p&gt;

&lt;p&gt;AI sometimes guesses. Same question, different answers. Rephrase it slightly, different answer again.&lt;/p&gt;

&lt;p&gt;Code doesn't do that. &lt;code&gt;compare_exchange_strong&lt;/code&gt; (a C++ atomic operation) gives you the same result every single time. Doesn't matter how many times you call it. C++ template metaprogramming, Python's deterministic pipelines, Rust's type system — those are tridents. Same input, same output, every time. They pick up where AI's guesswork leaves off.&lt;/p&gt;

&lt;p&gt;Think about it: tools like Claude Code and Codex are themselves an ox wielding a trident. The AI generates, the code tools execute deterministically, and somewhere in the middle there's supposed to be a human making sure none of it goes sideways.&lt;/p&gt;

&lt;p&gt;The world needs the speed of the ox.&lt;/p&gt;

&lt;p&gt;The certainty of the trident.&lt;/p&gt;

&lt;p&gt;And most of all — the people who can ride one and wield the other, and survive every moment when things fall apart.&lt;/p&gt;




&lt;p&gt;Find me on &lt;a href="https://whetan.substack.com" rel="noopener noreferrer"&gt;Substack&lt;/a&gt; | &lt;a href="https://stratcraft.ai/" rel="noopener noreferrer"&gt;StratCraft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>cpp</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>npm Supply Chain Security: Mistakes I Made Publishing My First Packages</title>
      <dc:creator>Whetlan</dc:creator>
      <pubDate>Tue, 24 Mar 2026 10:29:26 +0000</pubDate>
      <link>https://dev.to/whetlan/npm-supply-chain-security-mistakes-i-made-publishing-my-first-packages-2io9</link>
      <guid>https://dev.to/whetlan/npm-supply-chain-security-mistakes-i-made-publishing-my-first-packages-2io9</guid>
      <description>&lt;p&gt;I published four npm packages from a pnpm monorepo in March. Node 22, TypeScript, ~4k lines across the four packages, eleven direct dependencies total. First time publishing anything to npm. Within two weeks I'd almost shipped a &lt;code&gt;.env.example&lt;/code&gt;, missed a provenance setting that fails with zero output, and found out that 2FA on npm is basically theater once you start using automation tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  postinstall
&lt;/h2&gt;

&lt;p&gt;Before my first publish I went through every dependency's package.json looking for lifecycle scripts. Took about an hour. The reason: &lt;code&gt;ua-parser-js&lt;/code&gt; in 2021, &lt;code&gt;colors&lt;/code&gt; + &lt;code&gt;faker&lt;/code&gt; in 2022, &lt;code&gt;@ledgerhq/connect-kit&lt;/code&gt; in 2023. All compromised through npm. All exploited postinstall.&lt;/p&gt;

&lt;p&gt;The attack is dead simple:&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;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"postinstall"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node ./setup.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runs on &lt;code&gt;npm install&lt;/code&gt;. No prompt, no sandbox. Full user permissions. Read env vars, POST them somewhere, done.&lt;/p&gt;

&lt;p&gt;pnpm doesn't run lifecycle scripts from deps by default. npm and yarn do. That alone is a reason to use pnpm, honestly.&lt;/p&gt;

&lt;p&gt;To see which deps declare postinstall:&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="nv"&gt;$ &lt;/span&gt;npm query &lt;span class="s1"&gt;':has(&amp;gt; .scripts[postinstall])'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;npm's CSS-selector query syntax. I had to find it in the npm docs because nobody talks about it. Found two packages with postinstall in my tree: &lt;code&gt;esbuild&lt;/code&gt; and &lt;code&gt;protobufjs&lt;/code&gt;. Both legitimate. But you don't know that until you check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provenance (the silent failure that got me)
&lt;/h2&gt;

&lt;p&gt;npm has had provenance attestations since 2023. One flag on publish:&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="nv"&gt;$ &lt;/span&gt;npm publish &lt;span class="nt"&gt;--provenance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Green checkmark on npmjs.com. Links the tarball to a specific GitHub Actions run and commit hash.&lt;/p&gt;

&lt;p&gt;I didn't set this up for my first few publishes. Was running &lt;code&gt;npm publish&lt;/code&gt; from my laptop. Provenance needs OIDC, so it only works inside CI (GitHub Actions, GitLab, CircleCI). Can't fake it locally.&lt;/p&gt;

&lt;p&gt;The key part of my GitHub Actions workflow:&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;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;  &lt;span class="c1"&gt;# THIS. Without it, provenance silently fails.&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm/action-setup@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;
          &lt;span class="na"&gt;registry-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://registry.npmjs.org'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm install --frozen-lockfile&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm -r build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm publish --provenance --access public&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;NODE_AUTH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NPM_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I spent 30 minutes refreshing the npmjs.com page after my first CI publish, wondering where the green checkmark was. Re-ran the workflow. Published another version. Nothing. Checked the Actions log for errors. Clean. Turns out &lt;code&gt;id-token: write&lt;/code&gt; was missing and &lt;code&gt;--provenance&lt;/code&gt; just... silently doesn't attest. One line of YAML.&lt;/p&gt;

&lt;h2&gt;
  
  
  2FA doesn't protect what you think it does
&lt;/h2&gt;

&lt;p&gt;I enabled &lt;code&gt;auth-and-writes&lt;/code&gt; on day one:&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="nv"&gt;$ &lt;/span&gt;npm profile &lt;span class="nb"&gt;set &lt;/span&gt;auth-and-writes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Felt secure. Then I set up CI publishing with an automation token and realized: automation tokens bypass 2FA entirely. By design. There's no human to type the OTP code, so npm just... skips it.&lt;/p&gt;

&lt;p&gt;So if someone grabs your &lt;code&gt;NPM_TOKEN&lt;/code&gt; from a leaked &lt;code&gt;.env&lt;/code&gt; or a compromised GitHub secret, they can publish whatever they want. 2FA doesn't help.&lt;/p&gt;

&lt;p&gt;My first automation token had full publish access to every package on my account. Didn't scope it. Didn't restrict IPs. Just a bearer token sitting in a GitHub secret that could publish anything under my name.&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;# what I should have done from the start&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npm token create &lt;span class="nt"&gt;--cidr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-ci-ip-range&amp;gt; &lt;span class="nt"&gt;--publish&lt;/span&gt; &lt;span class="nt"&gt;--package&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-package
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scoped tokens help. But the real fix is OIDC provenance (no long-lived secret at all). The whole token model on npm feels stuck in 2015.&lt;/p&gt;

&lt;h2&gt;
  
  
  Someone ran &lt;code&gt;npm install&lt;/code&gt; in my pnpm repo
&lt;/h2&gt;

&lt;p&gt;I accidentally ran &lt;code&gt;npm install&lt;/code&gt; instead of &lt;code&gt;pnpm install&lt;/code&gt; in my monorepo. Generated a &lt;code&gt;package-lock.json&lt;/code&gt;, committed it without thinking. CI started resolving different dependency versions. Tests went flaky. Took me a full day to trace it back to the lockfile.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;.npmrc&lt;/code&gt; now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;frozen-lockfile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;engine-strict&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the root &lt;code&gt;package.json&lt;/code&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;"engines"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=22"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pnpm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=9"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;engine-strict&lt;/code&gt; means npm and yarn refuse to install. Sounds aggressive. But pnpm's lockfile stores content-addressable hashes for every tarball. If a dependency gets republished with different contents (yes, this happens, npm allows it within 72 hours), pnpm rejects the mismatch. npm is more forgiving about "fixing" stale lockfiles. That forgiveness is the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  I almost shipped a &lt;code&gt;.env&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;On my third publish I ran &lt;code&gt;npm pack --dry-run&lt;/code&gt; out of habit and saw my &lt;code&gt;.env.example&lt;/code&gt; in the tarball. It had placeholder values. But the &lt;code&gt;.env&lt;/code&gt; file itself was only excluded because &lt;code&gt;.gitignore&lt;/code&gt; caught it. If I'd had a &lt;code&gt;.env.local&lt;/code&gt; or &lt;code&gt;.env.production&lt;/code&gt; that wasn't in &lt;code&gt;.gitignore&lt;/code&gt;, it would have shipped to npm.&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="nv"&gt;$ &lt;/span&gt;npm pack &lt;span class="nt"&gt;--dry-run&lt;/span&gt; 2&amp;gt;&amp;amp;1
npm notice Tarball Contents
npm notice 1.2kB  README.md
npm notice 15.4kB dist/index.js
npm notice 3.1kB  dist/index.d.ts
npm notice 847B   package.json
npm notice 234B   .env.example     &lt;span class="c"&gt;# wait, why is this here?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without a &lt;code&gt;files&lt;/code&gt; field, &lt;code&gt;npm publish&lt;/code&gt; ships everything not in &lt;code&gt;.gitignore&lt;/code&gt;. I've seen packages on npm that include test fixtures, &lt;code&gt;.git&lt;/code&gt; directories (full commit history), even AWS key pairs in a &lt;code&gt;keys/&lt;/code&gt; folder that someone forgot to gitignore.&lt;/p&gt;

&lt;p&gt;My fix was adding &lt;code&gt;"files": ["dist", "README.md"]&lt;/code&gt; to every package.json. Now only what I explicitly list gets published. &lt;code&gt;npm pack --dry-run&lt;/code&gt; before every publish. Takes 5 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;npm audit&lt;/code&gt; is noisy
&lt;/h2&gt;

&lt;p&gt;It flags everything. Prototype pollution in a transitive dev dependency that only runs in tests? Critical. ReDoS in a markdown parser you use to render a help page? High.&lt;/p&gt;

&lt;p&gt;I ran &lt;code&gt;pnpm audit&lt;/code&gt; on my project the first time and got 4 advisories. All in transitive deps. None reachable from my code paths. But each one took 20 minutes to verify because you have to trace the import chain to confirm it's actually dead code.&lt;/p&gt;

&lt;p&gt;My process now: &lt;code&gt;pnpm audit&lt;/code&gt; weekly, check if the vulnerable path is reachable, update prod deps immediately, batch dev deps monthly.&lt;/p&gt;

&lt;p&gt;No Dependabot. Opens too many PRs for 11 direct dependencies. I run &lt;code&gt;pnpm outdated&lt;/code&gt; instead and read the changelogs.&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="nv"&gt;$ &lt;/span&gt;pnpm outdated
Package     Current  Latest
fastify     5.1.0    5.2.1
drizzle-orm 0.38.2   0.39.0
vitest      3.0.4    3.0.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three updates. I'll read the fastify changelog, check if drizzle has breaking changes (it usually does), and bump vitest blindly because test runner patches are low risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;--ignore-scripts&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pnpm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;pnpm rebuild esbuild  &lt;span class="c"&gt;# only rebuild what actually needs native compilation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Separates "download dependencies" from "run arbitrary code." Most packages don't need lifecycle scripts. The ones that do (native addons, platform binaries) can be rebuilt explicitly.&lt;/p&gt;

&lt;p&gt;I haven't defaulted to this yet because &lt;code&gt;playwright&lt;/code&gt; breaks without its postinstall (it downloads browsers). For production CI though, where the dep tree is locked and tested, I'd do it.&lt;/p&gt;

&lt;p&gt;The npm ecosystem doesn't have a real security boundary between "install a package" and "run arbitrary code on the user's machine." Provenance is good. Lockfiles help. &lt;code&gt;files&lt;/code&gt; field prevents accidental leaks. But if a maintainer's account gets compromised tomorrow, the only thing standing between their users and a malicious postinstall is whether someone notices before the next &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I have eleven dependencies. I can audit them manually. If you have two hundred, I don't know what to tell you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part of &lt;a href="https://silverstream.tech/clawnexus" rel="noopener noreferrer"&gt;ClawNexus&lt;/a&gt;, an open-source identity registry for AI agents.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Find me on &lt;a href="https://whetan.substack.com" rel="noopener noreferrer"&gt;Substack&lt;/a&gt; | &lt;a href="https://stratcraft.ai/" rel="noopener noreferrer"&gt;StratCraft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>node</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Running a Node.js Daemon with Fastify (No PM2, No systemd)</title>
      <dc:creator>Whetlan</dc:creator>
      <pubDate>Thu, 19 Mar 2026 11:30:02 +0000</pubDate>
      <link>https://dev.to/whetlan/running-a-nodejs-daemon-with-fastify-no-pm2-no-systemd-99i</link>
      <guid>https://dev.to/whetlan/running-a-nodejs-daemon-with-fastify-no-pm2-no-systemd-99i</guid>
      <description>&lt;p&gt;Every few months someone on Reddit asks "how do I run a Node.js process in the background." The answers are always PM2, forever, or systemd. All fine. But if you're shipping a CLI tool that users install on their own machines, you can't assume any of those exist.&lt;/p&gt;

&lt;p&gt;I have a CLI that starts a local HTTP daemon. &lt;code&gt;my-tool start&lt;/code&gt; forks into the background, user closes their terminal, daemon keeps running. About 700 lines for the whole thing, 150 of which are just the fork/PID/signal plumbing. Fastify 5 for the HTTP layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fork, detach, forget
&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;fork&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;node:child_process&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;os&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:os&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;DATA_DIR&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;homedir&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.my-tool&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;LOG_PATH&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DATA_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;daemon.log&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startDaemon&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DATA_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logFd&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;openSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LOG_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&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;child&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start&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;--_daemon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;detached&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;stdio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ignore&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logFd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logFd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ipc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;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="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unref&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;`Daemon started (PID &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;).`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;detached: true&lt;/code&gt; gives the child its own process group. &lt;code&gt;child.unref()&lt;/code&gt; lets the parent exit. &lt;code&gt;child.disconnect()&lt;/code&gt; drops the IPC channel.&lt;/p&gt;

&lt;p&gt;stdout/stderr go straight to a log file via the fd. Append mode so restarts don't clobber history.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--_daemon&lt;/code&gt; flag is how the child process knows it's the daemon and not the CLI. Underscore prefix to keep it out of &lt;code&gt;--help&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When the child starts with that flag, it writes a PID file and hooks signals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PID_FILE&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DATA_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;daemon.pid&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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--_daemon&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;writePid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pid&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;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SIGTERM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;removePid&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;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SIGINT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;removePid&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;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;startServer&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;SIGTERM comes from &lt;code&gt;stop&lt;/code&gt;. SIGINT is ctrl-c if you run it in foreground for debugging. I don't handle SIGHUP. Some daemons use it for config reload. Mine reads config at startup. Change config, restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  PID files and stale processes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;writePid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pid&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="k"&gt;void&lt;/span&gt; &lt;span class="p"&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;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PID_FILE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&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;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PID_FILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pid&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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readPid&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&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;PID_FILE&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="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// signal 0 = just check if alive&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;removePid&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&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;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PID_FILE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* gone already */&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;process.kill(pid, 0)&lt;/code&gt; doesn't send a signal. It checks if the process exists. If the PID file says 12345 but that process is dead, &lt;code&gt;kill&lt;/code&gt; throws and &lt;code&gt;readPid()&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Stale PID files happen constantly. Hard crash, &lt;code&gt;kill -9&lt;/code&gt;, OOM killer, power loss. Without the signal-0 check, &lt;code&gt;start&lt;/code&gt; would refuse to run because of a leftover file from a daemon that died three days ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fastify as the daemon core
&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="nx"&gt;Fastify&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;fastify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startServer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Fastify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;logger&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MY_TOOL_PORT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;17890&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HOST&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;MY_TOOL_HOST&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;uptime&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;uptime&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memoryUsage&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;rss&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;onClose&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// stop things that produce events first&lt;/span&gt;
    &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;healthTimer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// then flush storage&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="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="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HOST&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;`Listening on &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;HOST&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;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;logger: false&lt;/code&gt; because stdout already goes to the log file via fd redirect. Fastify's pino would double-log everything. I just use &lt;code&gt;console.log&lt;/code&gt; with a &lt;code&gt;[component]&lt;/code&gt; prefix. Not pretty, works fine for a local tool.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;127.0.0.1&lt;/code&gt; not &lt;code&gt;0.0.0.0&lt;/code&gt;. Local daemon, no reason to expose it to the network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Health check after fork
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/health&lt;/code&gt; does double duty. It's a monitoring endpoint, but it's also how &lt;code&gt;start&lt;/code&gt; confirms the daemon actually booted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cmdStart&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readPid&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="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;`Already running (PID &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="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="nf"&gt;startDaemon&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// ugly but necessary&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`http://127.0.0.1:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/health`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;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;`Daemon running. PID &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Daemon forked but not responding yet. Check the logs.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 1-second sleep is ugly. The child needs time to import modules and bind the port. Without it you get &lt;code&gt;ECONNREFUSED&lt;/code&gt; every time.&lt;/p&gt;

&lt;p&gt;I tried IPC ("child sends 'ready' to parent") but that means keeping the IPC channel open, which means the parent can't exit cleanly. Sleep + HTTP is dumber. Works.&lt;/p&gt;

&lt;p&gt;I learned the hard way why this health check matters. Early version didn't have it. Two instances on the same port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;my-tool start
&lt;span class="go"&gt;Daemon started (PID 48291).

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.my-tool/daemon.log
&lt;span class="go"&gt;Error: listen EADDRINUSE: address already in use 127.0.0.1:17890
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parent already printed success and exited. Daemon is actually dead. User thinks it's running. Now the health check catches this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shutdown ordering matters
&lt;/h2&gt;

&lt;p&gt;This is where I wasted actual time. First version of the &lt;code&gt;onClose&lt;/code&gt; hook:&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;// wrong&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;addHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;onClose&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="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;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;        &lt;span class="c1"&gt;// flush to disk&lt;/span&gt;
  &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;healthTimer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// stop health checks&lt;/span&gt;
  &lt;span class="nx"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;    &lt;span class="c1"&gt;// drop WebSocket&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;store.close()&lt;/code&gt; flushes pending writes. But the health checker was still running and could trigger a write during the flush. Race condition. Store got corrupted about once a week. Always on shutdown, always a half-written JSON file.&lt;/p&gt;

&lt;p&gt;Took me three corrupted files to connect the dots. Added a &lt;code&gt;--foreground&lt;/code&gt; flag to run the daemon in the current terminal, caught it within an hour.&lt;/p&gt;

&lt;p&gt;Fixed version is in the Fastify setup above. Stop producers first, then flush consumers.&lt;/p&gt;

&lt;h2&gt;
  
  
  start / stop / status / restart
&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cmdStop&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readPid&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;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Daemon is not running.&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="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SIGTERM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;removePid&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;`Stopped (PID &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pid&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;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;removePid&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Process already gone. Cleaned up PID file.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No wait for exit. SIGTERM fires the handler, handler calls &lt;code&gt;process.exit(0)&lt;/code&gt;, Fastify's &lt;code&gt;onClose&lt;/code&gt; runs, done. If that chain takes more than a second or two, something else is broken.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;restart&lt;/code&gt; = stop, sleep 500ms (port release), start. &lt;code&gt;status&lt;/code&gt; = read PID + hit &lt;code&gt;/health&lt;/code&gt;. If the PID file exists but health check fails, the daemon crashed without cleanup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;my-tool status
&lt;span class="go"&gt;PID file says 48291 but daemon not responding.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Interval callbacks will kill your daemon
&lt;/h2&gt;

&lt;p&gt;Most daemons run periodic tasks. Health checks, cache cleanup, token refresh.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HealthChecker&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;timer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;setInterval&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="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="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&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;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkAll&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;CHECK_INTERVAL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &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;timer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearInterval&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;timer&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;timer&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;.catch(console.error)&lt;/code&gt; is load-bearing. Without it, a rejected promise inside the interval is an unhandled rejection. Node 22 crashes the process on those.&lt;/p&gt;

&lt;p&gt;My daemon ran fine for a day, then a DNS timeout in the health checker produced an unhandled rejection. Dead process, stale PID file, nobody noticed until the next morning. Added the &lt;code&gt;.catch&lt;/code&gt;, hasn't died since.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not PM2 / systemd / Docker
&lt;/h2&gt;

&lt;p&gt;PM2 adds a dependency and has its own process management (PID files, logs, restart policies) that can conflict with yours.&lt;/p&gt;

&lt;p&gt;systemd is great if you control the box. But this runs on developer laptops. I'm not going to ask macOS users to write a launchd plist.&lt;/p&gt;

&lt;p&gt;Docker assumes Docker is installed. On a lot of dev machines, it's not.&lt;/p&gt;

&lt;p&gt;Fork works everywhere Node runs. macOS, Linux, Windows (add &lt;code&gt;windowsHide: true&lt;/code&gt; to the fork options or you get a console window flash).&lt;/p&gt;

&lt;h2&gt;
  
  
  Log rotation
&lt;/h2&gt;

&lt;p&gt;One more thing I didn't think about until a test machine had a 200MB log file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rotateLog&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stats&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;statSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LOG_PATH&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&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;backup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;LOG_PATH&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;backup&lt;/span&gt;&lt;span class="p"&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;unlinkSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;backup&lt;/span&gt;&lt;span class="p"&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;renameSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LOG_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;backup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* first run, no log yet */&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;Call this before opening the log fd in &lt;code&gt;startDaemon()&lt;/code&gt;. One backup, 10MB cap. Could use logrotate on Linux but again, can't assume it's configured.&lt;/p&gt;




&lt;p&gt;Fastify 5 boots in under 50ms, which matters when the user is staring at a terminal. The fork + PID + signal + health check pattern has been running on about a dozen machines for a couple months now with zero babysitting. That's the whole point of a daemon, I guess.&lt;/p&gt;




&lt;p&gt;Find me on &lt;a href="https://whetan.substack.com" rel="noopener noreferrer"&gt;Substack&lt;/a&gt; | &lt;a href="https://stratcraft.ai/" rel="noopener noreferrer"&gt;StratCraft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building E2E Encryption in Node.js Without libsodium</title>
      <dc:creator>Whetlan</dc:creator>
      <pubDate>Wed, 18 Mar 2026 11:59:01 +0000</pubDate>
      <link>https://dev.to/whetlan/building-e2e-encryption-in-nodejs-without-libsodium-3hhm</link>
      <guid>https://dev.to/whetlan/building-e2e-encryption-in-nodejs-without-libsodium-3hhm</guid>
      <description>&lt;p&gt;I run two daemons on different machines that talk through a WebSocket relay on a cheap VPS. The relay forwards messages between them, and I didn't want it reading any of that traffic. Partly principle, partly because if someone pops the box I don't want cleartext payloads sitting in memory or logs.&lt;/p&gt;

&lt;p&gt;The whole thing ended up around ~160 lines of TypeScript. Ed25519 for identity, X25519 for key exchange, AES-256-GCM for the actual encryption. All &lt;code&gt;node:crypto&lt;/code&gt;, zero external deps. Node 22.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two key pairs (blame Node)
&lt;/h2&gt;

&lt;p&gt;Ed25519 and X25519 are both Curve25519. Ed25519 signs. X25519 does Diffie-Hellman.&lt;/p&gt;

&lt;p&gt;You can convert between them (libsodium has &lt;code&gt;crypto_sign_ed25519_sk_to_curve25519&lt;/code&gt;). I tried. Node.js &lt;code&gt;crypto&lt;/code&gt; doesn't expose that conversion. You'd need tweetnacl or libsodium-wrappers, and I was trying to keep deps at zero.&lt;/p&gt;

&lt;p&gt;So: two key pairs. Ed25519 is the long-lived identity, persisted to disk. X25519 is ephemeral, generated per connection, never saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persisting the identity
&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;crypto&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:crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&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/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;IdentityKeys&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KeyObject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;publicKeyHex&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadOrCreateKeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keysDir&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IdentityKeys&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;privPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;keysDir&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/identity.key`&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;pubPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;keysDir&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/identity.pub`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;privDer&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;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;privPath&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;pubHex&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;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pubPath&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;privateKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createPrivateKey&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;privDer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;der&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="s2"&gt;pkcs8&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;publicKeyHex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pubHex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// first run, generate fresh&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;privateKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateKeyPairSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ed25519&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;privDer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pkcs8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;der&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;privPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;privDer&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;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chmod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;privPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mo"&gt;0o600&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;pubSpki&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spki&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;der&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// 44 bytes come back. first 12 are ASN.1 header junk. real key is 12-43.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;publicKeyHex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pubSpki&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&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;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pubPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicKeyHex&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;privateKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicKeyHex&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 SPKI export cost me time. You ask for the public key, you get 44 bytes back instead of 32. Sat there running &lt;code&gt;xxd&lt;/code&gt; and comparing against RFC 8032 test vectors until I realized the first 12 bytes are ASN.1 wrapping that Node just includes. Slicing the buffer is ugly but it works.&lt;/p&gt;

&lt;p&gt;Signing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KeyObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;signature&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&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;First arg to &lt;code&gt;crypto.sign&lt;/code&gt; is the digest algorithm. Ed25519 needs &lt;code&gt;null&lt;/code&gt; because the hash is built into the algorithm. I passed &lt;code&gt;"sha256"&lt;/code&gt; the first time and got a throw with zero useful context.&lt;/p&gt;

&lt;h2&gt;
  
  
  ECDH
&lt;/h2&gt;

&lt;p&gt;Each side generates a throwaway X25519 pair per connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;KeyPair&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&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="nl"&gt;privateKey&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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateKeyPair&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;KeyPair&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;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;privateKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateKeyPairSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x25519&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;publicKey&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="k"&gt;export&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spki&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;der&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pkcs8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;der&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ECDH, then HKDF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deriveSessionKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;localPrivateKey&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="nx"&gt;remotePubKey&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;privKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createPrivateKey&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;localPrivateKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;der&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="s2"&gt;pkcs8&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;pubKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createPublicKey&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;remotePubKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;der&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="s2"&gt;spki&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;sharedSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;diffieHellman&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;privKey&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;pubKey&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;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;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hkdfSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sharedSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;relay-e2e-v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&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;Don't skip the HKDF step. The raw ECDH output is a curve point, not uniformly random bytes. The info string (&lt;code&gt;"relay-e2e-v1"&lt;/code&gt;) just binds it to this protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual encryption
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionKey&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="nx"&gt;plaintext&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="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&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;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aes-256-gcm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&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;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&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;authTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthTag&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// iv (12) + authTag (16) + ciphertext&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;encrypted&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&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;function&lt;/span&gt; &lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionKey&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="nx"&gt;encoded&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="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&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;encoded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&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;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&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;authTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28&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;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;28&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;decipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDecipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aes-256-gcm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;decipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAuthTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authTag&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;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;decipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;decipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&lt;/span&gt;&lt;span class="p"&gt;(),&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&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;IV + auth tag + ciphertext packed into one base64 string. Receiver knows the layout. No framing needed.&lt;/p&gt;

&lt;p&gt;This error wasted my entire night:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Unsupported state or unable to authenticate data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;decipher.final()&lt;/code&gt; throws that when the auth tag doesn't match. I was convinced my ECDH was broken. Added &lt;code&gt;console.log&lt;/code&gt; on both sides, dumped the shared secret hex, they were identical. Stared at the code for way too long.&lt;/p&gt;

&lt;p&gt;Turned out I had &lt;code&gt;.toString("base64")&lt;/code&gt; on the sender and &lt;code&gt;.toString("base64url")&lt;/code&gt; on the receiver. One character difference in the output. The error message tells you nothing about &lt;em&gt;what&lt;/em&gt; actually failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Over the wire
&lt;/h2&gt;

&lt;p&gt;The relay is just a WebSocket server. Two peers join a room. First message each side sends is its X25519 public key, unencrypted:&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;// both sides send this on connect&lt;/span&gt;
&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DATA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;room_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;KEY_EXCHANGE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pubkey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;myKeyPair&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="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the receiving end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roomId&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;payload&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;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rooms&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;roomId&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;room&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&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;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;KEY_EXCHANGE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pubkey&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;remotePub&lt;/span&gt; &lt;span class="o"&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;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pubkey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deriveSessionKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;myKeyPair&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remotePub&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;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// not JSON = encrypted payload&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;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plaintext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// handle decrypted message&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;decrypt failed for room&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;roomId&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;Yeah, &lt;code&gt;JSON.parse&lt;/code&gt; failure as the encrypted/unencrypted discriminator. Not elegant. But the key exchange piggybacks on the relay's existing DATA message type, so I didn't have to touch the relay code at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  DER vs PEM
&lt;/h2&gt;

&lt;p&gt;I store keys as DER. Smaller, no base64 overhead, no &lt;code&gt;-----BEGIN&lt;/code&gt; headers.&lt;/p&gt;

&lt;p&gt;But pass DER bytes with &lt;code&gt;format: "pem"&lt;/code&gt; and you get this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error:0480006C:PEM routines::no start line
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Googled that error, got a bunch of OpenSSL forum posts about certificate chains. Took me 20 minutes to realize I just had the format string wrong:&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;// this works&lt;/span&gt;
&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createPrivateKey&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;derBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;der&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="s2"&gt;pkcs8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// this doesn't - DER bytes but told Node to expect PEM&lt;/span&gt;
&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createPrivateKey&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;derBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pem&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="s2"&gt;pkcs8&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;h2&gt;
  
  
  What this doesn't do
&lt;/h2&gt;

&lt;p&gt;No forward secrecy beyond the session level. If someone records traffic and later grabs the X25519 private key from memory, they can decrypt that session. Real forward secrecy means key ratcheting like Signal does. Per-session ephemeral keys felt like enough for my case. They live in memory and die on disconnect.&lt;/p&gt;

&lt;p&gt;The relay can also MITM the key exchange. It sees both X25519 public keys go through, could swap them and sit in the middle. The fix is signing the exchange with Ed25519. Haven't built it yet because I own the relay box. But I know that's a cop-out.&lt;/p&gt;

&lt;p&gt;Not using GCM's AAD either, so replaying an encrypted message from one room into another would technically decrypt fine. Low priority when you control both sides.&lt;/p&gt;




&lt;p&gt;All &lt;code&gt;node:crypto&lt;/code&gt;. The API has gotten a lot less painful since Node 20, and on 22 everything just worked without having to fight KeyObject conversions.&lt;/p&gt;




&lt;p&gt;Find me on &lt;a href="https://whetan.substack.com" rel="noopener noreferrer"&gt;Substack&lt;/a&gt; | &lt;a href="https://stratcraft.ai/" rel="noopener noreferrer"&gt;StratCraft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>typescript</category>
      <category>node</category>
      <category>cryptography</category>
    </item>
    <item>
      <title>I Published an npm Package. Someone Used the Same Name 8 Days Later.</title>
      <dc:creator>Whetlan</dc:creator>
      <pubDate>Sat, 14 Mar 2026 11:40:19 +0000</pubDate>
      <link>https://dev.to/whetlan/i-published-an-npm-package-someone-used-the-same-name-8-days-later-42g0</link>
      <guid>https://dev.to/whetlan/i-published-an-npm-package-someone-used-the-same-name-8-days-later-42g0</guid>
      <description>&lt;p&gt;Eight days after I published my first npm package, I found a GitHub repo with the exact same name. Different author, different language, different country. Same name, same domain (AI agent identity), even some of the same crypto primitives.&lt;/p&gt;

&lt;p&gt;The package has about 4k lines of TypeScript, four workspace packages, 365 tests. Not a weekend project. So finding a repo called the same thing, first commit dated eight days after my npm publish, with a version tag of "v6.0" on that first commit... that was a weird morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  npm timestamps don't lie
&lt;/h2&gt;

&lt;p&gt;First thing I did was check what I actually had on my side.&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="nv"&gt;$ &lt;/span&gt;npm view my-package &lt;span class="nb"&gt;time&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s1"&gt;'0.0.1'&lt;/span&gt;: &lt;span class="s1"&gt;'2026-03-01T...'&lt;/span&gt;,
  &lt;span class="s1"&gt;'0.2.0'&lt;/span&gt;: &lt;span class="s1"&gt;'2026-03-05T...'&lt;/span&gt;,
  &lt;span class="s1"&gt;'0.2.5'&lt;/span&gt;: &lt;span class="s1"&gt;'2026-03-05T...'&lt;/span&gt;,
  &lt;span class="s1"&gt;'0.2.8'&lt;/span&gt;: &lt;span class="s1"&gt;'2026-03-08T...'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;npm publish timestamps are immutable. Git commits can be rebased and rewritten. npm timestamps can't.&lt;/p&gt;

&lt;p&gt;Their first commit: March 9. My &lt;code&gt;0.0.1&lt;/code&gt;: March 1. That settled the priority question. But I did a lot of other stuff anyway, because panic is a hell of a motivator.&lt;/p&gt;

&lt;h2&gt;
  
  
  So how do naming collisions actually work?
&lt;/h2&gt;

&lt;p&gt;Here's the thing I didn't fully appreciate before this happened: npm, PyPI, and crates.io are completely independent namespaces. Owning &lt;code&gt;my-package&lt;/code&gt; on npm gives you zero claim to &lt;code&gt;my-package&lt;/code&gt; on PyPI. There's no cross-registry reservation system. There's no trademark-like protection unless you actually have a trademark.&lt;/p&gt;

&lt;p&gt;The other project was in Python. They couldn't use my npm name because I already had it. So they published on PyPI and built a website instead.&lt;/p&gt;

&lt;p&gt;Totally legal. Feels weird, but totally legal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 45-minute panic
&lt;/h2&gt;

&lt;p&gt;Here's what I did in the first hour after finding the repo:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checked PyPI.&lt;/strong&gt; My name wasn't taken. Registered it immediately with a placeholder package.&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="nv"&gt;$ &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;twine build
&lt;span class="c"&gt;# created a minimal pyproject.toml with just the name and version&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; twine upload dist/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PyPI's registration process is straightforward but I'd never done it before. Spent 15 minutes figuring out that you need a &lt;code&gt;pyproject.toml&lt;/code&gt; now, not a &lt;code&gt;setup.py&lt;/code&gt;. The Python packaging ecosystem changes its mind about this every two years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checked crates.io.&lt;/strong&gt; Also not taken. Registered four variations.&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="nv"&gt;$ &lt;/span&gt;cargo init my-package &lt;span class="nt"&gt;--lib&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;cargo publish &lt;span class="nt"&gt;--allow-dirty&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;crates.io requires a valid &lt;code&gt;Cargo.toml&lt;/code&gt; with a license field and a non-empty &lt;code&gt;src/lib.rs&lt;/code&gt;. My placeholder packages have exactly one line of Rust:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// placeholder - real implementation coming&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is this a good use of package registries? Probably not. But the alternative was watching someone else grab them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checked the GitHub username.&lt;/strong&gt; Taken, but by an inactive account with zero repos. Nothing I can do about that.&lt;/p&gt;

&lt;p&gt;Total time: about 45 minutes. Could have been 10 if I'd done this before the first npm publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  What tipped me off
&lt;/h2&gt;

&lt;p&gt;I wasn't actively searching for copycats. I was checking if anyone had used a similar name before I registered on more platforms. Standard due diligence, a week late.&lt;/p&gt;

&lt;p&gt;The repo had 32 commits in 3 days. All by one person. The first commit was tagged "v6.0", which is a bold move for a repo that didn't exist the day before. Most of the code looked AI-generated (one commit had a co-author tag from an AI assistant). There was a Discord bot, a Supabase backend, some Solana stubs that didn't compile.&lt;/p&gt;

&lt;p&gt;I don't want to get into whether this counts as "copying." The name is unusual enough that it's not a coincidence. But the implementation was completely different, and open source doesn't have naming courts.&lt;/p&gt;

&lt;p&gt;What bothered me more than the copying was realizing how exposed I'd been. Eight days with an npm package and zero presence anywhere else. If they'd moved faster, they could have grabbed the PyPI name first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually recommend
&lt;/h2&gt;

&lt;p&gt;If you're publishing an open source package with a distinctive name, do this stuff on day one. Not day eight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before your first &lt;code&gt;npm publish&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check these registries even if you have no plans to publish there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PyPI (&lt;code&gt;pip install&lt;/code&gt; your name, see if it 404s)&lt;/li&gt;
&lt;li&gt;crates.io (search, or &lt;code&gt;cargo search your-name&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The GitHub username&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;.com&lt;/code&gt; / &lt;code&gt;.dev&lt;/code&gt; / &lt;code&gt;.ai&lt;/code&gt; domains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In your package.json:&lt;/strong&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;"homepage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/your-org/your-package"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/your-org/your-package"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sounds obvious but I didn't have the &lt;code&gt;homepage&lt;/code&gt; field until after the incident. Google uses it when indexing npm packages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In your README, near the top:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Something like "Originally published March 2026 on npm" with a link. Not aggressive, just a timestamp that lives in your repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Register placeholder packages on at least PyPI and crates.io.&lt;/strong&gt; A minimal package with just a name and description takes 5 minutes per registry. The cost is near zero. The alternative is doing it in a panic at 11pm on a weekday.&lt;/p&gt;

&lt;h2&gt;
  
  
  The version tag thing still bugs me
&lt;/h2&gt;

&lt;p&gt;Their repo's first commit was tagged &lt;code&gt;v6.0&lt;/code&gt;. Not &lt;code&gt;v0.1.0&lt;/code&gt;, not &lt;code&gt;v1.0.0&lt;/code&gt;. Version 6.&lt;/p&gt;

&lt;p&gt;I can think of two reasons to do this. One is that they had five previous major versions in a private repo and decided to open-source at v6. The other is that they wanted the project to look mature.&lt;/p&gt;

&lt;p&gt;The repo had zero stars, zero forks, zero issues, one contributor. I'll let you decide which explanation is more likely.&lt;/p&gt;

&lt;h2&gt;
  
  
  npm doesn't care, and that's fine
&lt;/h2&gt;

&lt;p&gt;I reached out to npm support just to understand the policy. There isn't one for cross-platform naming conflicts. npm's stance is basically: you own the name on npm, that's it. What happens on PyPI or GitHub is outside their scope.&lt;/p&gt;

&lt;p&gt;Which is the right answer, honestly. A centralized cross-platform naming authority would be a nightmare. Imagine disputing a crate name because you own the npm package. The governance complexity alone would be unworkable.&lt;/p&gt;

&lt;p&gt;The tradeoff is that defensive registration falls on you. Nobody else is going to protect your name across registries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eight days is a long time
&lt;/h2&gt;

&lt;p&gt;That's the actual takeaway for me. I built the thing, tested extensively, published to npm, and then just... didn't think about other registries. For eight days. In a space where someone can scaffold an entire project with AI assistance in a weekend.&lt;/p&gt;

&lt;p&gt;If you're publishing something with a name you care about, spend the extra 30 minutes on day one. It's boring, it feels premature, and it will save you a very unpleasant surprise.&lt;/p&gt;

&lt;p&gt;My package registrations now look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm:       ✅ (day 1)
PyPI:      ✅ (day 9, should have been day 1)
crates.io: ✅ (day 9)
GitHub org: ✅ (day 1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four out of four. Would have been a lot less stressful if I'd done them all on the same day.&lt;/p&gt;




&lt;p&gt;Find me on &lt;a href="https://whetan.substack.com" rel="noopener noreferrer"&gt;Substack&lt;/a&gt; | &lt;a href="https://stratcraft.ai/" rel="noopener noreferrer"&gt;StratCraft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>node</category>
      <category>webdev</category>
      <category>discuss</category>
    </item>
    <item>
      <title>pnpm Workspaces in Production: What Actually Matters</title>
      <dc:creator>Whetlan</dc:creator>
      <pubDate>Fri, 13 Mar 2026 09:42:08 +0000</pubDate>
      <link>https://dev.to/whetlan/pnpm-workspaces-in-production-what-actually-matters-16p7</link>
      <guid>https://dev.to/whetlan/pnpm-workspaces-in-production-what-actually-matters-16p7</guid>
      <description>&lt;p&gt;I've been running a pnpm workspaces monorepo in production. Four packages, all TypeScript, about 8k lines total, published to npm. Node 22, pnpm 9, no build tool beyond &lt;code&gt;tsc&lt;/code&gt;. Before I started I read a dozen "monorepo setup" articles and most of them were 2000 words on Turborepo vs Nx vs Lerna, and maybe two paragraphs on the stuff that actually breaks on a random Tuesday afternoon.&lt;/p&gt;

&lt;p&gt;This is the Tuesday afternoon stuff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two lines of config
&lt;/h2&gt;

&lt;p&gt;My workspace config is two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pnpm-workspace.yaml&lt;/span&gt;
&lt;span class="na"&gt;packages&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;packages/*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Every directory under &lt;code&gt;packages/&lt;/code&gt; is a workspace package. The root &lt;code&gt;package.json&lt;/code&gt; is private and just orchestrates:&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;"private"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm -r build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm -r --parallel dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm -r test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"clean"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm -r --parallel exec rm -rf dist"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"engines"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=22"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pnpm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=9"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-r&lt;/code&gt; means "run in every package." &lt;code&gt;--parallel&lt;/code&gt; on &lt;code&gt;dev&lt;/code&gt; and &lt;code&gt;clean&lt;/code&gt; because those don't depend on each other. But &lt;code&gt;build&lt;/code&gt; runs sequentially, because one of my packages imports from another and needs it compiled first. pnpm figures out the dependency order on its own, so the dependent package always builds after its dependency.&lt;/p&gt;

&lt;p&gt;I spent zero time choosing between Turborepo and Nx. &lt;code&gt;pnpm -r&lt;/code&gt; handles orchestration, &lt;code&gt;tsc&lt;/code&gt; handles compilation. That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared tsconfig (the part that actually saves time)
&lt;/h2&gt;

&lt;p&gt;Every monorepo article tells you to make a shared base tsconfig. They're right. But nobody tells you which settings go in the base vs per-package.&lt;/p&gt;

&lt;p&gt;My base:&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;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Node16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Node16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lib"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"esModuleInterop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"skipLibCheck"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"declaration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"declarationMap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sourceMap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each package extends it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../../tsconfig.base.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rootDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"src/**/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rootDir&lt;/code&gt; and &lt;code&gt;outDir&lt;/code&gt; are per-package because they're relative paths. Everything else is shared.&lt;/p&gt;

&lt;p&gt;Before this, I had four slightly different tsconfigs and couldn't remember which one had &lt;code&gt;strictNullChecks&lt;/code&gt;. Now I change a setting once and it applies everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  What About TypeScript Project References?
&lt;/h3&gt;

&lt;p&gt;I don't use them. &lt;code&gt;composite: true&lt;/code&gt; gives you cross-package type checking at build time, but you have to maintain a &lt;code&gt;references&lt;/code&gt; array in every tsconfig, keep them in sync with your dependency graph, and deal with &lt;code&gt;tsBuildInfo&lt;/code&gt; files that get stale and produce phantom errors.&lt;/p&gt;

&lt;p&gt;Four packages, one internal dependency. I just build them in order. If I had ten packages or builds started taking minutes instead of seconds, I'd probably reconsider.&lt;/p&gt;

&lt;h2&gt;
  
  
  workspace:* vs workspace:^
&lt;/h2&gt;

&lt;p&gt;When one package depends on another in the monorepo:&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;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"my-daemon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"workspace:*"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During development this creates a symlink. Live, always-current version of the dependency. No rebuild needed.&lt;/p&gt;

&lt;p&gt;At publish time, pnpm replaces &lt;code&gt;workspace:*&lt;/code&gt; with the actual version number. So &lt;code&gt;"my-daemon": "workspace:*"&lt;/code&gt; becomes &lt;code&gt;"my-daemon": "0.2.7"&lt;/code&gt; in the published package.json.&lt;/p&gt;

&lt;p&gt;Here's where it bit me: I initially had &lt;code&gt;workspace:^&lt;/code&gt; (with a caret). Published, and the dependency became &lt;code&gt;"^0.2.7"&lt;/code&gt;. Consumers could install mismatched minor versions. For tightly coupled internal deps, use &lt;code&gt;workspace:*&lt;/code&gt; for exact version pinning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phantom dependencies will find you
&lt;/h2&gt;

&lt;p&gt;This one cost me a full afternoon. pnpm uses a strict &lt;code&gt;node_modules&lt;/code&gt; structure by default. Packages can only access dependencies they explicitly declare. Which is great, until you realize half your code was relying on phantom dependencies and you didn't know it.&lt;/p&gt;

&lt;p&gt;If package A depends on &lt;code&gt;fastify&lt;/code&gt; and package B doesn't declare it, B can't import &lt;code&gt;fastify&lt;/code&gt; even though it's installed in the monorepo. With npm or yarn, hoisting would let B "accidentally" use A's fastify. You'd never notice until you publish and a user's install fails.&lt;/p&gt;

&lt;p&gt;I had this exact bug. One of my packages was importing a type from &lt;code&gt;@types/ws&lt;/code&gt; without declaring it as a devDependency. Worked fine locally because another package had it, and VS Code happily resolved it through the workspace. Published to npm, got an issue within two days:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error TS2307: Cannot find module 'ws' or its corresponding type declarations.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Felt pretty dumb.&lt;/p&gt;

&lt;p&gt;The fix: run &lt;code&gt;pnpm why&lt;/code&gt; in each workspace, check that every import has a matching declaration:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;packages/client &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm why @types/ws
&lt;span class="c"&gt;# nothing? that's your bug&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; @types/ws
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boring work, but it would have saved me an embarrassing npm publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vitest + workspaces
&lt;/h2&gt;

&lt;p&gt;Vitest has a workspace feature. My root config just lists the packages:&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;// vitest.workspace.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;defineWorkspace&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;vitest/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineWorkspace&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;packages/server&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;packages/client&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;packages/cli&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;packages/core&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;Per-package config:&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;defineConfig&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;vitest/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;testTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;restoreMocks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pnpm test&lt;/code&gt; from root runs all suites. &lt;code&gt;pnpm --filter server test&lt;/code&gt; runs just one.&lt;/p&gt;

&lt;p&gt;One gotcha: if your tests import from sibling packages, make sure you've built the dependency first. Vitest doesn't trigger builds. I ended up with a &lt;code&gt;pretest&lt;/code&gt; script that runs &lt;code&gt;pnpm -r build&lt;/code&gt; before the test suite. Wasteful, rebuilds everything even if nothing changed. But the alternative is forgetting to build and then spending 20 minutes debugging why the types don't match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing to npm
&lt;/h2&gt;

&lt;p&gt;No Changesets, no Lerna. I bump versions manually and &lt;code&gt;pnpm publish&lt;/code&gt; from each package directory.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;prepublishOnly&lt;/code&gt; hook:&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;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prepublishOnly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm build"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, you will eventually publish stale &lt;code&gt;dist/&lt;/code&gt; files. Or no &lt;code&gt;dist/&lt;/code&gt; at all. I did this on my second publish. Ran &lt;code&gt;npm info&lt;/code&gt; on the package, stared at the file list, wondered why &lt;code&gt;dist/&lt;/code&gt; was from three commits ago. Took me way too long to realize I just forgot to build before publishing.&lt;/p&gt;

&lt;p&gt;Also, explicit &lt;code&gt;files&lt;/code&gt; field:&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;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"README.md"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.d.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.d.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.js"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I once published a package that accidentally included my &lt;code&gt;src/&lt;/code&gt; directory, test fixtures, and a 4MB debug log. The &lt;code&gt;files&lt;/code&gt; field is an allowlist. Only what you list gets published. &lt;code&gt;npm pack --dry-run&lt;/code&gt; is your friend here, run it once before every publish and actually read the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stuff I tried that wasn't worth it
&lt;/h2&gt;

&lt;p&gt;I tried Turborepo for about a day. My full build takes under 30 seconds. Remote caching and smart task scheduling are solving a problem I don't have. &lt;code&gt;pnpm -r build&lt;/code&gt; is the whole build system.&lt;/p&gt;

&lt;p&gt;I also see monorepos with a &lt;code&gt;packages/eslint-config&lt;/code&gt; internal package. For four packages that's a lot of ceremony for an eslint config. Mine lives at the root, each package references it with a relative path. Done.&lt;/p&gt;

&lt;p&gt;Early on I extracted a "shared utils" package. It had three functions in it. That's not a package, that's a file. I deleted it and just duplicated the two functions that were actually shared. Less abstraction, fewer symlink headaches.&lt;/p&gt;

&lt;p&gt;And I gave up on synchronized version numbers pretty quickly. My client library and server ship at different cadences. Forcing &lt;code&gt;v0.3.1&lt;/code&gt; on both would mean publishing no-op releases just to keep numbers aligned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep it boring
&lt;/h2&gt;

&lt;p&gt;After all of this, the thing I keep coming back to is: keep it boring. &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; should be two lines. Your root &lt;code&gt;package.json&lt;/code&gt; should have like five scripts. If your monorepo setup needs a README to explain how it works, you've gone too far.&lt;/p&gt;

&lt;p&gt;When something breaks, don't reach for &lt;code&gt;shamefully-hoist=true&lt;/code&gt; in &lt;code&gt;.npmrc&lt;/code&gt;. Figure out why it broke. Nine times out of ten it's a missing dependency declaration, and your users will hit the same thing when they install your package.&lt;/p&gt;

&lt;p&gt;And put &lt;code&gt;prepublishOnly&lt;/code&gt; hooks in every publishable package. I cannot stress this enough. Future you will forget to build before publishing. It's not a question of if.&lt;/p&gt;

&lt;p&gt;Four packages, plain pnpm, no Turborepo, no Nx. If build times become a problem I'll add something. They haven't yet.&lt;/p&gt;




&lt;p&gt;Find me on &lt;a href="https://whetan.substack.com" rel="noopener noreferrer"&gt;Substack&lt;/a&gt; | &lt;a href="https://stratcraft.ai/" rel="noopener noreferrer"&gt;StratCraft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building Zero-Config LAN Discovery in Node.js (mDNS + UDP Broadcast)</title>
      <dc:creator>Whetlan</dc:creator>
      <pubDate>Thu, 12 Mar 2026 11:00:02 +0000</pubDate>
      <link>https://dev.to/whetlan/building-zero-config-lan-discovery-in-nodejs-mdns-udp-broadcast-44go</link>
      <guid>https://dev.to/whetlan/building-zero-config-lan-discovery-in-nodejs-mdns-udp-broadcast-44go</guid>
      <description>&lt;p&gt;I was building a small daemon that runs on multiple machines in a LAN — think a couple of dev servers, a NAS, maybe a Raspberry Pi. I wanted them to discover each other automatically. No central server, no config files listing IP addresses, no "go edit this YAML and restart."&lt;/p&gt;

&lt;p&gt;Just start it and it should find its peers.&lt;/p&gt;

&lt;p&gt;I ended up layering three approaches: mDNS, a custom UDP broadcast protocol I basically stole from 1990s Novell NetWare, and a brute-force subnet scanner as the last resort. The whole thing is ~300 lines of TypeScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  mDNS: great on paper
&lt;/h2&gt;

&lt;p&gt;First attempt was pure mDNS. Typical setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Laptop (Wi-Fi) → office AP → corporate router → server running the daemon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In theory mDNS handles this fine. In practice it worked on my MacBook at home, worked in a small office, then fell apart on real networks. Routers silently dropping multicast. Access points with client isolation. One corporate network had multicast disabled and nobody in IT could tell me why.&lt;/p&gt;

&lt;p&gt;Node.js mDNS annoyances:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No built-in support — you need the &lt;code&gt;multicast-dns&lt;/code&gt; npm package&lt;/li&gt;
&lt;li&gt;macOS handles it natively (Bonjour), Linux needs avahi, Windows is a coin flip&lt;/li&gt;
&lt;li&gt;Port 5353 conflicts if avahi-daemon is already running&lt;/li&gt;
&lt;li&gt;Fails silently. Your queries just go into the void.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stealing from 1990s NetWare
&lt;/h2&gt;

&lt;p&gt;Novell NetWare had SAP — Service Advertising Protocol. Every server periodically broadcasts "hey, I exist, here's what I do" to the entire subnet. No multicast groups, no special infrastructure.&lt;/p&gt;

&lt;p&gt;I built the same thing on UDP/IP. Two message types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DiscoverMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;discover&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AnnounceMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;announce&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;instance_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;display_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tls&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node starts up, broadcasts &lt;code&gt;discover&lt;/code&gt;. Running instances reply with &lt;code&gt;announce&lt;/code&gt;. Periodic &lt;code&gt;announce&lt;/code&gt; on a timer so late joiners get found too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Socket setup
&lt;/h3&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;dgram&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:dgram&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;os&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:os&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;DISCOVERY_PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;17891&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startDiscovery&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;dgram&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Socket&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dgram&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;udp4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;address&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// failed during bind&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DISCOVERY_PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setBroadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// DO NOT FORGET THIS&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;socket&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;I wasted two hours on this. Ran tcpdump, stared at Wireshark captures, swapped ports, blamed the firewall.&lt;/p&gt;

&lt;p&gt;Turns out I forgot &lt;code&gt;socket.setBroadcast(true)&lt;/code&gt;. Node.js won't send to broadcast addresses without it — doesn't throw, doesn't warn. The packets just vanish.&lt;/p&gt;

&lt;h3&gt;
  
  
  Broadcast addresses
&lt;/h3&gt;

&lt;p&gt;On a machine with multiple NICs you need the per-subnet broadcast address, not just &lt;code&gt;255.255.255.255&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBroadcastTargets&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&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;ifaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;networkInterfaces&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="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="nx"&gt;addrs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ifaces&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;addrs&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;isVirtualInterface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addr&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;addrs&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;addr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;family&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;IPv4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;calcBroadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;netmask&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// broadcast = IP | ~mask, nothing fancy&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calcBroadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&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;mask&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="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ipParts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&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;maskParts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&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;ipParts&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;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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;b&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="nx"&gt;maskParts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;192.168.1.42&lt;/code&gt; with mask &lt;code&gt;255.255.255.0&lt;/code&gt; → &lt;code&gt;192.168.1.255&lt;/code&gt;. Bitwise OR with inverted netmask.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filtering virtual interfaces
&lt;/h3&gt;

&lt;p&gt;Docker running + WireGuard VPN = my daemon "discovering" phantom peers. Broadcasting over Docker bridges and VPN tunnels, getting garbage back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isVirtualInterface&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="nx"&gt;boolean&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;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;n&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;wg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;       &lt;span class="c1"&gt;// WireGuard&lt;/span&gt;
    &lt;span class="nx"&gt;n&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;tun&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;      &lt;span class="c1"&gt;// TUN (OpenVPN)&lt;/span&gt;
    &lt;span class="nx"&gt;n&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;tap&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;      &lt;span class="c1"&gt;// TAP&lt;/span&gt;
    &lt;span class="nx"&gt;n&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;docker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;   &lt;span class="c1"&gt;// Docker&lt;/span&gt;
    &lt;span class="nx"&gt;n&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;br-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;      &lt;span class="c1"&gt;// Docker bridge&lt;/span&gt;
    &lt;span class="nx"&gt;n&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;veth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;     &lt;span class="c1"&gt;// Docker veth&lt;/span&gt;
    &lt;span class="nx"&gt;n&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;virbr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;    &lt;span class="c1"&gt;// KVM&lt;/span&gt;
    &lt;span class="nx"&gt;n&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;vmnet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;    &lt;span class="c1"&gt;// VMware&lt;/span&gt;
    &lt;span class="nx"&gt;n&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;vboxnet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// VirtualBox&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;Hardcoded prefix list. &lt;code&gt;os.networkInterfaces()&lt;/code&gt; gives you zero info about physical vs virtual. On Linux you could parse sysfs but that doesn't help on macOS or Windows.&lt;/p&gt;

&lt;h3&gt;
  
  
  TCP probe before registering
&lt;/h3&gt;

&lt;p&gt;UDP is unverified. Before registering a peer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleAnnounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AnnounceMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;rinfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dgram&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RemoteInfo&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="c1"&gt;// own broadcast echoing back — yes this happens&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isSelfIp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rinfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&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="c1"&gt;// not on our subnet? reject&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isInLocalSubnet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rinfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&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="c1"&gt;// actually try to connect before believing it&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alive&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;tcpProbe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rinfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&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;alive&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="nf"&gt;registerPeer&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rinfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;broadcast&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might get an announce from a service that crashed 30 seconds ago. TCP health check with 2-second timeout filters out the ghosts.&lt;/p&gt;

&lt;p&gt;Your own broadcasts also echo back. Found that when my daemon kept discovering itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Announce jitter
&lt;/h3&gt;

&lt;p&gt;Without randomization, 10 simultaneous instances all broadcast at the exact same second forever:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scheduleNextAnnounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dgram&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Socket&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ANNOUNCE_INTERVAL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;ANNOUNCE_JITTER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;broadcastAnnounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;scheduleNextAnnounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;delay&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;60s base + 0-10s random jitter. Same thing NTP does.&lt;/p&gt;

&lt;h2&gt;
  
  
  I still use mDNS though
&lt;/h2&gt;

&lt;p&gt;Despite everything above, I run mDNS alongside the broadcast protocol. On home networks it works fine, and other programs can discover your service too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mDNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;multicast-dns&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;mdns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mDNS&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;SERVICE_TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_my-service._tcp.local&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;setInterval&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;mdns&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="na"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SERVICE_TYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PTR&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="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;mdns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;response&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;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rinfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allRecords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;additionals&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;srvRecords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allRecords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SRV&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;aRecords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allRecords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;srv&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;srvRecords&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;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;srv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&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;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;srv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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;aRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aRecords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;target&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;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aRecord&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aRecord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rinfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&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;`Found peer: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ip&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;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One gotcha: lots of services bind to &lt;code&gt;127.0.0.1&lt;/code&gt; and advertise that via mDNS. You connect to the A record and hit yourself.&lt;/p&gt;

&lt;p&gt;Fix — use the UDP packet's source IP when the A record is loopback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aRecordIp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aRecord&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aRecord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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;isLoopback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;aRecordIp&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="nx"&gt;aRecordIp&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;::1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="nx"&gt;aRecordIp&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;127.&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;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aRecordIp&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isLoopback&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;aRecordIp&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rinfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I made mDNS a soft dependency — if &lt;code&gt;multicast-dns&lt;/code&gt; isn't installed, broadcast-only mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;mdns&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mDNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;multicast-dns&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;mdns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mDNS&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mDNS unavailable, broadcast-only mode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Last resort: subnet scan
&lt;/h2&gt;

&lt;p&gt;When passive discovery doesn't cut it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CONCURRENCY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&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;TIMEOUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scanSubnet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subnet&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;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;254&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;host&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;subnet&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;i&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;port&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="o"&gt;---&lt;/span&gt;

&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Part&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ClawNexus&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//silverstream.tech/clawnexus), an open-source identity registry for AI agents.*&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;targets&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;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queue&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;&amp;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alive&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;probeHost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&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;alive&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;`Found: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&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;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// 50 workers chewing through the queue&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workers&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="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CONCURRENCY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workers&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;50 concurrent probes, 2-second timeout. Full /24 in about 10 seconds. I expose it as an on-demand API endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd change
&lt;/h2&gt;

&lt;p&gt;Should have filtered virtual interfaces on day one. That one wasted an entire evening — I kept thinking my broadcast protocol was broken until I noticed Docker bridges were happily replying to my packets.&lt;/p&gt;

&lt;p&gt;Should have used a binary header for UDP instead of raw JSON. Would have prevented the &lt;code&gt;JSON.parse&lt;/code&gt; crash when a broken mDNS responder sent invalid data. I knew it could throw. Didn't wrap it. Classic.&lt;/p&gt;

&lt;p&gt;Test on Windows earlier. The firewall silently blocks UDP on custom ports. Ended up adding &lt;code&gt;netsh advfirewall&lt;/code&gt; rule creation.&lt;/p&gt;

&lt;p&gt;Only dependencies are &lt;code&gt;dgram&lt;/code&gt; and &lt;code&gt;os&lt;/code&gt; from Node, plus &lt;code&gt;multicast-dns&lt;/code&gt; if you want it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Fought with mDNS on corporate networks? What ended up working?&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Find me on &lt;a href="https://whetan.substack.com" rel="noopener noreferrer"&gt;Substack&lt;/a&gt; | &lt;a href="https://stratcraft.ai/" rel="noopener noreferrer"&gt;StratCraft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>networking</category>
      <category>typescript</category>
      <category>node</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
