<?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: Ted</title>
    <description>The latest articles on DEV Community by Ted (@henry_dan_81513dd35a2f540).</description>
    <link>https://dev.to/henry_dan_81513dd35a2f540</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2256153%2Fd3b27e5a-9e82-4c4d-b481-9b835d4deea3.png</url>
      <title>DEV Community: Ted</title>
      <link>https://dev.to/henry_dan_81513dd35a2f540</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/henry_dan_81513dd35a2f540"/>
    <language>en</language>
    <item>
      <title>Every Page Was Exactly the Same Size. That Was the Bug Report.</title>
      <dc:creator>Ted</dc:creator>
      <pubDate>Sun, 09 Aug 2026 08:00:17 +0000</pubDate>
      <link>https://dev.to/henry_dan_81513dd35a2f540/every-page-was-exactly-the-same-size-that-was-the-bug-report-41a</link>
      <guid>https://dev.to/henry_dan_81513dd35a2f540/every-page-was-exactly-the-same-size-that-was-the-bug-report-41a</guid>
      <description>&lt;p&gt;Search Console told me ten pages on a site I run were duplicates of each other. That report is usually noise — it fires on trailing slashes, on query strings, on pagination. I opened it expecting to close it.&lt;/p&gt;

&lt;p&gt;The site is a React single-page app that prerenders static HTML at build time, so crawlers get real content instead of an empty &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;. I fetched the ten flagged URLs to see what the crawler was seeing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/section/a/first-page      200   6025B
/section/a/second-page     200   6025B
/section/b/third-page      200   6025B
/section/c/fourth-page     200   6025B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten different URLs. Ten identical byte counts.&lt;/p&gt;

&lt;p&gt;Not similar. &lt;strong&gt;Identical&lt;/strong&gt; — down to the byte, because they were the same file. Every one of them was the homepage, served under a different URL, carrying a &lt;code&gt;&amp;lt;link rel="canonical"&amp;gt;&lt;/code&gt; pointing at the homepage. Each of those pages was telling Google, in the crawler's first pass, &lt;em&gt;I am not a page. I am the homepage.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing had failed
&lt;/h2&gt;

&lt;p&gt;Here is what makes this hard to catch, and it is the whole point of this post.&lt;/p&gt;

&lt;p&gt;The routes returned &lt;strong&gt;200&lt;/strong&gt;. The app has a catch-all rewrite, so any path serves the shell and the router sorts it out in the browser. A human visiting one of those URLs saw the correct page, fully rendered, with the correct title in the tab — because after hydration the client-side code sets all of that properly.&lt;/p&gt;

&lt;p&gt;The build &lt;strong&gt;exited 0&lt;/strong&gt;. It printed a success line and a route count, and the route count was a number nobody had a reason to question.&lt;/p&gt;

&lt;p&gt;The prerender step had a &lt;code&gt;try/catch&lt;/code&gt; around the part that loads the page data. When that load failed, the catch logged a warning and returned an empty array. Every downstream route was generated by looping over that array, so an empty array produced zero pages — quietly, with a success exit code.&lt;/p&gt;

&lt;p&gt;And the pages that &lt;em&gt;were&lt;/em&gt; missing didn't 404. They fell through to the catch-all and served the shell. The failure covered its own tracks.&lt;/p&gt;

&lt;p&gt;So: no exception, no red build, no broken link, nothing wrong on screen. Four independent layers, each doing something individually reasonable, and the composite result was a few hundred pages that existed for humans and did not exist for crawlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fallback converts a failure into a plausible lie
&lt;/h2&gt;

&lt;p&gt;This is the reframe I keep coming back to.&lt;/p&gt;

&lt;p&gt;A fallback exists to stop a failure from being loud. That is its entire job, and most of the time it is the right job. But the output of a fallback is not an error message — it is a value. A plausible one. Something shaped exactly like a correct answer.&lt;/p&gt;

&lt;p&gt;Which means &lt;strong&gt;you cannot find this class of bug by looking for failures, because no failure occurred.&lt;/strong&gt; Your logs are clean by construction. Your monitors are green by construction. The system did not break; it substituted.&lt;/p&gt;

&lt;p&gt;So the question stops being &lt;em&gt;what went wrong&lt;/em&gt; and becomes something much more mechanical:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What should be different here, and is it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Uniformity is the signal. Ten pages that should have ten different sizes had one size. That is not a subtle statistical hint — it is a screaming anomaly, and it is invisible unless you go looking for sameness specifically.&lt;/p&gt;

&lt;p&gt;Once I started asking that question instead of hunting for errors, the same afternoon turned up three more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four bugs, one technique
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Identical outputs where inputs differed.&lt;/strong&gt; The byte counts above. Ten inputs, one output. That found the missing pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One half worked and the other didn't, in the same file.&lt;/strong&gt; Two sections of the same build enriched their pages from different sources — one fetched over HTTP, one needed a bundler to read a local data file. The HTTP one produced 156 links on the live page. The bundler one produced zero. Same file, same build, same run, one worked and one didn't. That is a controlled experiment I got for free, and it isolated the cause to the bundler dependency in about a minute. Without the working half I would have been guessing at the whole pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A set difference nobody had ever computed.&lt;/strong&gt; One page linked out to every item in a database table. The router resolved those links against a &lt;em&gt;separate&lt;/em&gt; static file. Nobody had ever asked whether those two lists agreed. I subtracted one from the other: ten links pointed at pages that could not exist. Not because of a typo — because the two sources disagreed about spelling in six cases and about categorisation in four. Every one of those links had been manufacturing a fresh duplicate page, from an indexed page, for months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Arithmetic against a stale premise.&lt;/strong&gt; A migration script sitting in the repo predicted the table would end up with a particular distribution of values: eleven of one kind, a hundred and seven of another. The live table had twenty-one and ninety-seven. That ten-row gap was the entire finding — a later audit had deliberately moved ten rows, and running the older script would have reverted it. I didn't need to understand either script's reasoning to know something was wrong. The numbers didn't match, and mismatched numbers are a question.&lt;/p&gt;

&lt;p&gt;None of those four is clever. All of them are &lt;em&gt;comparisons&lt;/em&gt;. That is the only skill involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one that actually mattered
&lt;/h2&gt;

&lt;p&gt;The worst thing I found that day had nothing to do with prerendering, and it was hiding behind the same mechanism.&lt;/p&gt;

&lt;p&gt;A classification field in the database had five possible values. It had gained its fifth some months earlier. One function mapped that field to a human-readable label with a &lt;code&gt;switch&lt;/code&gt;, and that switch had cases for four of the five. The fifth hit the &lt;code&gt;default&lt;/code&gt; branch and came out as the most severe of the other four.&lt;/p&gt;

&lt;p&gt;The pages affected were three of the highest-traffic pages on the site. Each of them was displaying a confident, specific, &lt;strong&gt;wrong&lt;/strong&gt; classification. Not a blank. Not a placeholder. The opposite of the truth, stated plainly, in the page title.&lt;/p&gt;

&lt;p&gt;Two things kept it invisible. First, the fallback again — the output was a real label, so nothing looked empty or broken. Second, and worse: a &lt;em&gt;different&lt;/em&gt; function, the one that picks the colour for the status badge, &lt;strong&gt;did&lt;/strong&gt; have a case for the fifth value. So the page rendered the correct colour next to the incorrect word. Anyone glancing at it saw a styled, complete, confident page.&lt;/p&gt;

&lt;p&gt;The tell was in the type signature. The function declared a return type listing all five labels. It could only ever return four. A function whose return type names a value it cannot produce is telling you, in writing, that a case is missing — and TypeScript will not flag it, because returning a subset of a union is perfectly legal.&lt;/p&gt;

&lt;p&gt;That is a cheap thing to grep for and I had never thought to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually do differently
&lt;/h2&gt;

&lt;p&gt;Two changes, and one of them is not a technique.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delete the fallback where the fallback is a lie.&lt;/strong&gt; The prerender's &lt;code&gt;try/catch&lt;/code&gt; now throws, and the build refuses to run if it loads fewer pages than expected. A build that silently drops a few hundred pages and exits 0 is worse than a build that fails, because the failing one costs you ten minutes and the silent one costs you months. That is the change I'd make first in any system: find the places where degraded output is indistinguishable from correct output, and make them loud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compare things that should match.&lt;/strong&gt; Not as a debugging step — as a routine check. Do the links on this page point at pages that exist? Does the sitemap match what the build emitted? Does the static file agree with the database table? Every one of those is a set difference you can compute in a few lines, and every one of them found something real for me. They're boring, they're fast, and they don't require knowing what you're looking for in advance, which is the entire point when the bug produces plausible output.&lt;/p&gt;

&lt;p&gt;The non-technique: &lt;strong&gt;stop trusting the fact that nothing looks wrong.&lt;/strong&gt; For this whole class of problem, "nothing looks wrong" is not evidence of health. It's the expected symptom.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;You cannot detect a failure that didn't happen. Every fallback in your system is a place where a failure was quietly converted into a plausible value, and plausible values do not show up in logs, monitors, or a glance at the page.&lt;/p&gt;

&lt;p&gt;What they do show up in is comparison. Two things that should differ and don't. Two lists that should match and don't. A number that should be one number and is another.&lt;/p&gt;

&lt;p&gt;Ten files with the same byte count is not a subtle clue. It's a bug report — as long as somebody thinks to measure the thing that was never supposed to be uniform.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>webdev</category>
      <category>seo</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Insecure Default Was the Configuration</title>
      <dc:creator>Ted</dc:creator>
      <pubDate>Fri, 07 Aug 2026 08:00:52 +0000</pubDate>
      <link>https://dev.to/henry_dan_81513dd35a2f540/the-insecure-default-was-the-configuration-14pp</link>
      <guid>https://dev.to/henry_dan_81513dd35a2f540/the-insecure-default-was-the-configuration-14pp</guid>
      <description>&lt;p&gt;I was moving a pile of automation scripts into version control — years of accumulated cron jobs that had only ever existed on one machine — and ran a secret scan before the first commit. It found this, in two files:&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="n"&gt;SERP_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SERP_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a1b2c3d4…&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A live API key, sitting in source, as the default argument. The fix is so obvious it barely counts as a decision. Delete the literal, put the value in an env file, read it from there. Ten minutes.&lt;/p&gt;

&lt;p&gt;I did the ten minutes. Then, before committing, I checked one more thing: what actually sets &lt;code&gt;SERP_API_KEY&lt;/code&gt; when these run?&lt;/p&gt;

&lt;p&gt;Nothing does. Not the crontab, which has no env line for it. Not the wrapper the jobs run through, which passes the environment along untouched. Not a shell profile — cron does not read one.&lt;/p&gt;

&lt;p&gt;So that hardcoded literal was not a fallback. It was the configuration. Every scheduled run of those jobs for months had used the second argument, because the first was never populated. What I had just deleted as a bad habit was the only reason the jobs worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two halves that look alike
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;os.environ.get(KEY, default)&lt;/code&gt; reads as a preference and a safety net. Use the environment; if it is not there, fall back to this.&lt;/p&gt;

&lt;p&gt;The shape is honest about the mechanism and silent about the reality. It does not tell you which half runs. Both branches are valid code, both produce a working value, and the expression looks identical whether the variable is set on every machine or on none of them. &lt;code&gt;process.env.KEY || 'literal'&lt;/code&gt; has the same problem. So does every config layer with a default: the fallback is invisible precisely when it is doing all the work.&lt;/p&gt;

&lt;p&gt;The asymmetry that makes this dangerous is that the two failure modes arrive at different times. If the environment variable &lt;em&gt;is&lt;/em&gt; set and I delete the default, nothing happens — the code was already taking the first branch. If it is &lt;em&gt;not&lt;/em&gt; set and I delete the default, the job dies. Both edits look the same in a diff. Both feel like the same small act of hygiene.&lt;/p&gt;

&lt;p&gt;And the death is quiet. These were scheduled jobs. They fail at 09:10 on a Monday into a log file, and the next thing that reads that log is me, whenever I next think to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question that separates them
&lt;/h2&gt;

&lt;p&gt;Not "is there a hardcoded secret here." I could see that. The scanner saw it.&lt;/p&gt;

&lt;p&gt;The question is: &lt;strong&gt;which branch is running in production right now?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For an environment variable that is one command, and it has to be run in the same context the job runs in — not in my shell, where my profile has been sourced and everything looks populated. My interactive shell is the least representative environment on the machine. Cron gets a nearly empty one.&lt;/p&gt;

&lt;p&gt;Once I asked that, the fix changed shape. It was no longer "delete the literal." It was "make the environment actually carry the value, then delete the literal, then prove the job still resolves it." The last step is the one I would have skipped. It is also the only one that distinguishes a security fix from an outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scanner was too clever
&lt;/h2&gt;

&lt;p&gt;There is a second half to this, and it is worse.&lt;/p&gt;

&lt;p&gt;My scan looked for high-entropy strings — long runs of hex and base64, the shape of an API key. It caught both hardcoded keys immediately. It also caught a third one in a different file, and I felt good about the coverage.&lt;/p&gt;

&lt;p&gt;Then, reading around an unrelated line in a config block, I found this a few lines below:&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="n"&gt;EMAIL&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;me@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;…&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A real account login, in plaintext, used to authenticate against a hosted database. Fifteen characters. My pattern required thirty-two.&lt;/p&gt;

&lt;p&gt;That is not a tuning problem I can fix by lowering the threshold — at fifteen characters and lower, everything matches. It is a category error. &lt;strong&gt;Entropy-based secret detection is built for machine-generated credentials, and a password is human-generated by definition.&lt;/strong&gt; It is short because a person types it. It contains a word because a person remembers it. Every property that makes it a bad password makes it invisible to a scanner looking for randomness.&lt;/p&gt;

&lt;p&gt;The credential with the widest blast radius on the machine — an account login, plausibly reused elsewhere — was the one my tooling was structurally incapable of finding. I found it by reading.&lt;/p&gt;

&lt;p&gt;So the scan needs two passes with different logic: one for shape, matching entropy and known key prefixes, and one for &lt;em&gt;name&lt;/em&gt;, matching &lt;code&gt;PASSWORD&lt;/code&gt;, &lt;code&gt;SECRET&lt;/code&gt;, &lt;code&gt;TOKEN&lt;/code&gt;, &lt;code&gt;PASSWD&lt;/code&gt; as assignment targets regardless of what is on the right-hand side. The second pass is dumber and catches what the first cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not everything that looks like a secret is one
&lt;/h2&gt;

&lt;p&gt;The mirror of that failure showed up in the same commit, and it is worth naming because over-correcting here has its own cost.&lt;/p&gt;

&lt;p&gt;Two more long random-looking strings survived my scan, and I left both in source deliberately.&lt;/p&gt;

&lt;p&gt;One was a verification key for a search-engine ping protocol. That protocol works by having you host the key as a text file at your own domain root, so a crawler can confirm you control the site. The key is &lt;em&gt;published by design&lt;/em&gt;. I confirmed it in one request: fetching the key file at the domain returned 200. Stripping it would have been theatre.&lt;/p&gt;

&lt;p&gt;The other was a JSON Web Token for a database client. JWTs are three base64 segments joined by dots, and the middle one is the payload — you can decode it without any credential at all. Its role field said &lt;code&gt;anon&lt;/code&gt;: the public client key, meant to ship to browsers, protected by row-level policies rather than by being secret. Also fine to commit.&lt;/p&gt;

&lt;p&gt;Both of those look exactly like the thing I was hunting. The difference is not visible in the string. You get it by asking what the value is &lt;em&gt;for&lt;/em&gt; — and for a JWT you can simply read the answer out of the token.&lt;/p&gt;

&lt;p&gt;Blindly stripping every high-entropy string would have broken two working systems in the name of security. That is the same failure as leaving the real password in, just pointed the other way: acting on the shape of a value instead of its function.&lt;/p&gt;

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

&lt;p&gt;The refactor that shipped is unremarkable, which is the point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each script reads its credential from a single env file, using the same loader the rest of the codebase already used. Consistency here matters more than elegance; a second loader is a second thing to get wrong.&lt;/li&gt;
&lt;li&gt;Every one raises a clear error naming the missing variable, instead of falling back. If it cannot find the value, I want the job dead and loud, not running on a stale literal.&lt;/li&gt;
&lt;li&gt;I verified each one by executing it and watching it resolve a real value. Not a syntax check. A syntax check would have passed on all three, including the one where I had used &lt;code&gt;os.environ&lt;/code&gt; without importing &lt;code&gt;os&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the small joke at the end. Having reasoned carefully about which branch runs in production, I introduced a &lt;code&gt;NameError&lt;/code&gt; and nearly shipped it, because &lt;code&gt;ast.parse&lt;/code&gt; validates grammar and knows nothing about names. The check that caught it was running the thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;A default value is a claim about what happens when the real source is missing. It is not a claim that the real source exists.&lt;/p&gt;

&lt;p&gt;Before you delete one as a bad habit, find out which half has been load-bearing. If the answer is "the literal," you are not tightening security — you are removing the configuration, and you will find out at 09:10 on a Monday, in a log file, if you find out at all.&lt;/p&gt;

&lt;p&gt;And run two scans. One for what secrets look like, one for what they are called. The scanner that finds your API keys will walk straight past your password.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your Analytics Can Only See As Far As Your Login Does</title>
      <dc:creator>Ted</dc:creator>
      <pubDate>Sat, 01 Aug 2026 04:34:28 +0000</pubDate>
      <link>https://dev.to/henry_dan_81513dd35a2f540/your-analytics-can-only-see-as-far-as-your-login-does-332i</link>
      <guid>https://dev.to/henry_dan_81513dd35a2f540/your-analytics-can-only-see-as-far-as-your-login-does-332i</guid>
      <description>&lt;p&gt;I run a directory site. People arrive, look at listings, and some of them click through to an external booking page. Those outbound clicks are the only thing on the site that correlates with money, so they are logged: what was clicked, from which page, on what device, and — if the person happens to be signed in — which account.&lt;/p&gt;

&lt;p&gt;Out of 1,372 recorded clicks, six carried an account.&lt;/p&gt;

&lt;p&gt;Six. Not six percent. Six clicks, across eight months, belonging to four distinct people.&lt;/p&gt;

&lt;p&gt;That number reads as a broken integration. Something in the chain that attaches identity to an event is dropping it, and 99.56% of the time nobody notices because the click still gets recorded and the dashboard still fills in. So I went looking for the break.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chain was fine
&lt;/h2&gt;

&lt;p&gt;The click handler passes through a hook, the hook calls an edge function, the edge function reads the &lt;code&gt;Authorization&lt;/code&gt; header and resolves it to a user. The client library attaches the current session's token automatically. There was no header override anywhere, session persistence was on, and the storage key was pinned so it could not rotate out from under itself.&lt;/p&gt;

&lt;p&gt;The requests also pass through a same-origin proxy on the way to the backend, which is the kind of thing that quietly eats headers. I tested it directly: the auth endpoint received the token, parsed it, and rejected it for the correct reason. Headers survive the hop.&lt;/p&gt;

&lt;p&gt;Every layer did what it was written to do. Which meant the six were not survivors of a lossy process. They were the entire population of something.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six were not random
&lt;/h2&gt;

&lt;p&gt;Here is the part that ended the investigation.&lt;/p&gt;

&lt;p&gt;I pulled the six clicks and joined them to the accounts that made them, and looked at the gap between when the account was created and when the click happened.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.7 minutes
1.8 minutes
2.8 minutes
7.4 minutes
11.6 minutes
14.9 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six clicks. All of them inside a quarter of an hour of the account existing at all.&lt;/p&gt;

&lt;p&gt;That is not the signature of a lossy pipeline. A pipeline that drops identity 99.56% of the time drops it uniformly — you would see attributed clicks scattered across the whole timeline, at all sorts of distances from signup. Instead every attributed click sits in one narrow window, and the window has an obvious shape: it is the period during which somebody was still signed in from having just signed up.&lt;/p&gt;

&lt;p&gt;Nothing was failing. The tracking captured identity every single time identity was present. It was present for about ten minutes per person, ever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nobody chose this
&lt;/h2&gt;

&lt;p&gt;The site is deliberately open. Nothing is gated. You never have to make an account to see anything, and the only reasons to make one are to save a listing or leave a review — both optional, both rare. That was a good decision, made on its own merits, about content access.&lt;/p&gt;

&lt;p&gt;But an auth model is not only a content-access decision. It also silently sets the maximum duration over which your analytics can associate two events with the same person.&lt;/p&gt;

&lt;p&gt;Nobody wrote that down. There is no config value for it. It emerged from "the site should be open to everyone" and it landed at roughly ten minutes, and it will stay at roughly ten minutes no matter how good the analytics get, because the analytics were never the constraint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You cannot measure a relationship that outlives your identifier.&lt;/strong&gt; Whatever your system uses to say "this is the same person as before" — a session, a token, a cookie, a device id — has a lifespan, and every question your analytics can answer must fit inside it. Ask a longer question and you do not get a wrong answer. You get an empty one, which is worse, because empty looks like evidence of absence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it presents as a bug
&lt;/h2&gt;

&lt;p&gt;The reason this costs a day rather than a minute is that the symptom shows up in the wrong place.&lt;/p&gt;

&lt;p&gt;The number that looks wrong is in the analytics table. So you debug the analytics: the handler, the function, the header, the proxy. All of that code is fine, which means every check comes back clean, which feels like you are narrowing in on something subtle when in fact you are auditing a layer that has no defect in it at all.&lt;/p&gt;

&lt;p&gt;The defect is not in the layer that produced the symptom. It is in a design decision one layer down and several months back, which is working exactly as intended and has no idea it is also an analytics constraint. Instrumentation problems present as instrumentation. Architecture problems also present as instrumentation.&lt;/p&gt;

&lt;p&gt;The tell was the distribution, not the magnitude. &lt;code&gt;0.44%&lt;/code&gt; tells you almost nothing — it is consistent with a lossy pipeline, a rare event, or a design ceiling. The &lt;em&gt;shape&lt;/em&gt; of when those six occurred distinguishes between them in one query. When a number looks broken, the useful question is rarely "how big is it" and almost always "how is it distributed".&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is a design change
&lt;/h2&gt;

&lt;p&gt;More tracking would not have helped, because nothing was going untracked. The change is to stop borrowing identity from the session.&lt;/p&gt;

&lt;p&gt;So every tracked click now also carries a first-party identifier generated once in the browser's local storage, and signup copies that identifier onto the new account. Joining the two recovers the whole visit: what somebody clicked before they had an account, and — the part that actually matters — what they clicked days afterwards.&lt;/p&gt;

&lt;p&gt;That is a different kind of thing from a session. It is not authentication and it grants nothing. It is one value whose only job is to outlive the login, because the login was never going to live long enough to answer the question.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it still cannot do
&lt;/h2&gt;

&lt;p&gt;Worth being precise about, because an identifier like this invites more confidence than it earns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It identifies a browser, not a person.&lt;/strong&gt; Cleared storage, a second device, a private window — each one starts fresh. Somebody who browses on a phone and books on a laptop is two rows and always will be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It undercounts, never over.&lt;/strong&gt; It can fail to connect two events that belonged to the same person. It cannot merge two people into one. Given a choice about which direction to be wrong in, that is the right one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It only works going forward.&lt;/strong&gt; Every existing account predates it, so their history stays blank. There is no backfill, because the data was never captured — and inventing a join key retroactively would produce exactly the kind of confident wrong answer this was meant to eliminate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that makes it a weak instrument. It makes it an instrument with a stated range, which is the only kind worth having. The version I replaced also had a range. It just did not mention it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general shape
&lt;/h2&gt;

&lt;p&gt;Every system that counts things has a horizon, and the horizon is usually set somewhere else entirely — in an auth model, a retention policy, a cookie lifetime, a log rotation window. None of those are described as analytics decisions. All of them are.&lt;/p&gt;

&lt;p&gt;When a metric comes back far lower than it should be, there are three possibilities, and they look identical at a glance: the pipeline is lossy, the event is genuinely rare, or you asked a question longer than your identifier lives. The first is a bug and you should fix it. The second is a finding and you should accept it. The third is neither — it is a boundary, and no amount of work inside the analytics layer will move it.&lt;/p&gt;

&lt;p&gt;Check the distribution before you check the code. If every observation you &lt;em&gt;do&lt;/em&gt; have clusters inside one window, you have not found a leak. You have found the edge of what your system was ever able to see.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your System Cannot Say I Don't Know</title>
      <dc:creator>Ted</dc:creator>
      <pubDate>Fri, 31 Jul 2026 08:00:44 +0000</pubDate>
      <link>https://dev.to/henry_dan_81513dd35a2f540/your-system-cannot-say-i-dont-know-35e</link>
      <guid>https://dev.to/henry_dan_81513dd35a2f540/your-system-cannot-say-i-dont-know-35e</guid>
      <description>&lt;p&gt;Four things I have been told by my own systems in the last three months:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0&lt;/strong&gt; clicks on outbound links, on a page I could watch people click through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean&lt;/strong&gt; from a typechecker, on a codebase with two type errors in it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Position 13&lt;/strong&gt; for a page that has never once ranked 13th.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;200 OK&lt;/strong&gt; from an endpoint that did nothing at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these was a bug in the ordinary sense. Nothing crashed, nothing threw, nothing appeared in a log with the word ERROR next to it. Each was a real value, produced by a real system, working exactly as written.&lt;/p&gt;

&lt;p&gt;And every one of them was a statement about the world that the system had no business making, because in each case the thing that was supposed to do the measuring had not run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The overloaded value
&lt;/h2&gt;

&lt;p&gt;Pick any layer of your stack and there is a single value doing two incompatible jobs.&lt;/p&gt;

&lt;p&gt;A database query returns no rows. Does that mean the table has no matching rows, or that the query never reached the table? Both are &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A process exits &lt;code&gt;0&lt;/code&gt;. Does that mean it checked everything and found nothing wrong, or that it checked nothing? Both are zero.&lt;/p&gt;

&lt;p&gt;A counter reads &lt;code&gt;0&lt;/code&gt;. Is that "we measured, and it was none", or "nothing was ever counted"? Both are &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A command produces no output. Silence is what success looks like. It is also what a program that did nothing looks like.&lt;/p&gt;

&lt;p&gt;This is not a language design mistake anyone can point at and fix. It is a consequence of representing absence at all: the moment you have a value meaning "nothing here", it will be produced both by &lt;em&gt;nothing being here&lt;/em&gt; and by &lt;em&gt;the lookup not happening&lt;/em&gt;. Every layer inherits it. Every layer passes it upward, where it stops looking like an absence and starts looking like data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The asymmetry that keeps it hidden
&lt;/h2&gt;

&lt;p&gt;Here is what makes this specifically dangerous rather than merely annoying.&lt;/p&gt;

&lt;p&gt;In all four of my examples, the incorrect reading is the &lt;em&gt;reassuring&lt;/em&gt; one.&lt;/p&gt;

&lt;p&gt;Zero errors. All checks passed. No such user. Nothing to report.&lt;/p&gt;

&lt;p&gt;A false alarm gets investigated within the hour, because it is uncomfortable and somebody wants it to go away. A false all-clear gets investigated never, because it is exactly what you hoped to see and there is nothing to chase.&lt;/p&gt;

&lt;p&gt;So the failure mode is not just that the two states are indistinguishable. It is that the wrong one is &lt;em&gt;load-bearing for your peace of mind&lt;/em&gt;, and the entire purpose of the signal is to let you stop looking.&lt;/p&gt;

&lt;p&gt;That is why these survive for months rather than hours. A green check is not a piece of information you evaluate. It is permission to move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four layers, one shape
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The instrument.&lt;/strong&gt; A dashboard showed zero outbound clicks. The tracking worked correctly — for one specific kind of link. Every other link on the site was a plain anchor tag the counter had never been taught about. So "0" was accurate about what it measured and silent about the fact that it was measuring almost nothing. The number was not wrong. Its scope was, and a number does not carry its scope. &lt;em&gt;(&lt;a href="https://dev.to/posts/zero-is-not-a-measurement"&gt;Zero Is Not a Measurement&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The compiler.&lt;/strong&gt; I ran a typechecker four times before shipping, and it exited clean every time. The project used a config that points at other configs and contains no source files of its own — a normal, widely-generated layout. Given that config, the tool typechecks an empty set, finds nothing wrong with it, and exits zero. It was not being lenient. It was being thorough about nothing. &lt;em&gt;(&lt;a href="https://dev.to/posts/it-passed-because-it-never-looked"&gt;It Passed Because It Never Looked&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The statistic.&lt;/strong&gt; A page reported an average search position of 13. It had never ranked 13th. It ranked around 6 for one group of queries and around 20 for another, and the mean of a bimodal distribution is a number describing a state that does not exist. Unlike the others this one is not even a failure — the average is computed correctly, from complete data, by a system with no faults. It still describes nothing real. &lt;em&gt;(&lt;a href="https://dev.to/posts/average-position-hid-cannibalization"&gt;One Page Ranked #6 and #20 at the Same Time&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The database.&lt;/strong&gt; A password reset endpoint looked users up through an API layer that could not reach the table it needed. The call failed every time it ran. But only the result was captured and the error was discarded, so the failure arrived as &lt;code&gt;null&lt;/code&gt;, and &lt;code&gt;null&lt;/code&gt; was read as "no such user" — which routed into a branch specified to return success while doing nothing, because revealing whether an account exists is a security leak. &lt;em&gt;(&lt;a href="https://dev.to/posts/a-discarded-error-becomes-an-answer"&gt;A Discarded Error Becomes an Answer&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Four different layers. One shape: an operation that did not happen, represented identically to an operation that happened and found nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question that separates them
&lt;/h2&gt;

&lt;p&gt;There is one question that distinguishes a real signal from a decorative one, and it takes about a minute to ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If this were broken, what would I see?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the honest answer is "exactly what I am seeing now", you do not have a signal. You have a value that happens to be reassuring.&lt;/p&gt;

&lt;p&gt;Run it against the four:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the click tracker were counting nothing, the dashboard would read 0. It read 0.&lt;/li&gt;
&lt;li&gt;If the typechecker were examining no files, it would exit clean. It exited clean.&lt;/li&gt;
&lt;li&gt;If the average were hiding a split, it would return a plausible middle number. It returned 13.&lt;/li&gt;
&lt;li&gt;If the user lookup were failing completely, the endpoint would return 200 and send nothing. It returned 200 and sent nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four for four. In each case, "healthy" and "not running" produce byte-identical output, and I had been reading that output as evidence for months.&lt;/p&gt;

&lt;p&gt;The follow-up question is the actionable one: &lt;strong&gt;what would have to change for these to be distinguishable?&lt;/strong&gt; That is usually a small piece of work. Print how many files were checked. Report the count of rows the query considered, not just the result. Show a distribution alongside the mean. Return a different status code when the lookup itself failed.&lt;/p&gt;

&lt;p&gt;None of that is clever. It is just refusing to let one value carry two meanings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unknown is a state you have to store
&lt;/h2&gt;

&lt;p&gt;The fix that generalises is to stop modelling two states where there are three.&lt;/p&gt;

&lt;p&gt;Not &lt;em&gt;found&lt;/em&gt; and &lt;em&gt;not found&lt;/em&gt;. &lt;strong&gt;Found&lt;/strong&gt;, &lt;strong&gt;not found&lt;/strong&gt;, and &lt;strong&gt;could not determine&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That third state has to survive as far as the decision. It is not enough to log it — logging is what you do with something you have decided not to act on. It has to reach the branch, and the branch has to treat it differently, because "I could not check whether this person has an account" and "this person has no account" call for opposite behaviour. One is a 500 that pages you. The other is a normal response.&lt;/p&gt;

&lt;p&gt;In practice that means a handful of unglamorous habits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bind your errors even when you do not think you need them.&lt;/strong&gt; Discarding an error does not make it not happen. It converts it into whatever your success path does with a missing value, which is usually to proceed confidently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make failure reachable, then check that it is.&lt;/strong&gt; Break the check on purpose and watch it go red. If it stays green, you have not learned that your code is clean; you have learned that your check does not run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Require the failure to be specific.&lt;/strong&gt; A nonzero exit says something failed. It does not say &lt;em&gt;this check found this fault&lt;/em&gt;. Assert on the content, not just the status, or an empty run and a passing run remain interchangeable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distinguish "no data" from "data says zero" in anything you display.&lt;/strong&gt; They are different claims and a dashboard that renders them identically is lying quietly, every day, to whoever is looking at it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this is getting harder
&lt;/h2&gt;

&lt;p&gt;The volume of code arriving in my repositories that I did not type by hand has gone up sharply, and it is very good code by every measure my tooling can apply. It compiles. It is typed correctly. It follows the conventions of the file it sits in.&lt;/p&gt;

&lt;p&gt;What it is not reliably doing is &lt;em&gt;knowing things&lt;/em&gt;. It produces the plausible value confidently, and the plausible value is frequently right, which is exactly what makes the exceptions invisible. Generated code and swallowed errors have the same signature: something that looks like knowledge, occupying the place where knowledge should be, with no marker distinguishing it from the real thing.&lt;/p&gt;

&lt;p&gt;Which means the burden shifts. It is no longer enough to check that the code is well-formed — every tool I have already does that, extremely well. The open question is whether the things it asserts are true, and almost nothing in a standard pipeline is built to ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Software very rarely fabricates information out of nothing. What it does constantly, at every layer, is take &lt;em&gt;I could not find out&lt;/em&gt; and quietly round it to the nearest confident-looking answer — usually zero, usually null, usually success.&lt;/p&gt;

&lt;p&gt;Then it hands you that answer with exactly the same face it uses for a real one.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>programming</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>A Second Check Is a Second Source of Truth</title>
      <dc:creator>Ted</dc:creator>
      <pubDate>Thu, 30 Jul 2026 08:00:45 +0000</pubDate>
      <link>https://dev.to/henry_dan_81513dd35a2f540/a-second-check-is-a-second-source-of-truth-39lf</link>
      <guid>https://dev.to/henry_dan_81513dd35a2f540/a-second-check-is-a-second-source-of-truth-39lf</guid>
      <description>&lt;p&gt;Signup on a site I run asked people to prove they owned their email address, and then asked them again.&lt;/p&gt;

&lt;p&gt;First a six-digit code, emailed and typed back in. Only after that does the account get created. Then, on creation, the auth service sent its own confirmation email with a link in it.&lt;/p&gt;

&lt;p&gt;Two emails. One address. Same question asked twice.&lt;/p&gt;

&lt;p&gt;It read as thorough. It was actually a fork in the data, and it had already put a real user somewhere the system has no sensible answer for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state nobody designed
&lt;/h2&gt;

&lt;p&gt;Someone signed up. They received the code, entered it correctly, and the account was created. The app signed them in immediately, as it is written to do.&lt;/p&gt;

&lt;p&gt;They never clicked the second link. Why would they? They had just typed a code out of their inbox and were now looking at a logged-in page. From where they sat, the job was done.&lt;/p&gt;

&lt;p&gt;So the record says: account exists, password set, session valid, signed in — and &lt;code&gt;email_confirmed_at&lt;/code&gt; is null.&lt;/p&gt;

&lt;p&gt;That row is not describing a half-finished signup. It is describing a completed one, plus a leftover flag from a check that nobody needed and nothing enforced. But a flag called &lt;em&gt;email confirmed&lt;/em&gt; saying &lt;em&gt;no&lt;/em&gt; is going to be read by something, eventually. Any support tooling, any audit, any future feature that gates on confirmation, will look at that user and conclude they never proved their address.&lt;/p&gt;

&lt;p&gt;They did. Six weeks earlier. There is a used code in another table with their name on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two records, one fact
&lt;/h2&gt;

&lt;p&gt;This is the reframe that took me embarrassingly long.&lt;/p&gt;

&lt;p&gt;I had been thinking of the second check as redundancy — belt and braces, no harm in it. It is not redundancy. &lt;strong&gt;It is a second place where the same fact is recorded, and two records of one fact will eventually disagree.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fact is "this person controls this address." My OTP table knows it, in the form of a code marked used. The auth service knows it, in the form of a timestamp. Nothing keeps the two in sync, because they were never designed as one thing — one is application logic, the other is a platform default that was left on.&lt;/p&gt;

&lt;p&gt;The moment a user satisfies one and not the other, the fact has two values. And when a fact has two values, every consumer downstream has to pick which one to believe, usually without knowing there is a choice being made.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one that wasn't enforcing anything
&lt;/h2&gt;

&lt;p&gt;There is a version of this where the second check is the real one and the first is a convenience. That would at least be coherent.&lt;/p&gt;

&lt;p&gt;It was not that. The link enforced nothing.&lt;/p&gt;

&lt;p&gt;The user was signed in before the confirmation email could possibly have been opened, because the app calls sign-in directly after creating the account. Sign-in did not consult the flag. Nothing in the product consulted the flag. The account worked identically whether the link was clicked or not.&lt;/p&gt;

&lt;p&gt;So the second check could not block anything, could not gate anything, and could not fail in any way the user would notice. Its entire observable effect was to set a column that then contradicted the other record of the same fact.&lt;/p&gt;

&lt;p&gt;That is the test worth applying: &lt;strong&gt;if a check cannot refuse anything, it is not a check.&lt;/strong&gt; It is a log entry with an opinion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it was on at all
&lt;/h2&gt;

&lt;p&gt;Nobody chose this. The confirmation email is the platform default, and it is a sensible default — for an app whose signup is "create account, we'll email you a link." That is the flow it was designed for.&lt;/p&gt;

&lt;p&gt;We had replaced that flow with our own and never turned the old one off. The original code even said so, in a comment:&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;// Create the user account with auto-confirm (since we verified email via OTP)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intent was recorded. The setting it referred to was never changed. So the comment described a system that did not exist, sitting directly above the line that created the problem, for months.&lt;/p&gt;

&lt;p&gt;I have a lot of sympathy for that comment. It is what happens when you build the replacement and reasonably assume the thing it replaces has stepped aside.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing it, and the row already broken
&lt;/h2&gt;

&lt;p&gt;Turning off the platform confirmation was one setting. New signups now get exactly one email, containing exactly one code, and land signed in with a single coherent record of the fact.&lt;/p&gt;

&lt;p&gt;The stranded user needed a separate decision, and it is a more interesting one than it looks. The options were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Leave them flagged unverified, which is false.&lt;/li&gt;
&lt;li&gt;Set the flag to now, which is also false — it says they proved ownership today, which they did not.&lt;/li&gt;
&lt;li&gt;Set it to the moment they actually entered the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I took the third. Their confirmation timestamp is now thirty-three seconds &lt;em&gt;before&lt;/em&gt; their account was created, which looks like a data error and is in fact the most accurate statement available: they proved they owned that address, then the account was made.&lt;/p&gt;

&lt;p&gt;I would rather have a timestamp that looks odd and is true than one that looks tidy and is not. Anyone who investigates that row will find the corresponding used code sitting right there in the other table, and the sequence will make sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would ask earlier next time
&lt;/h2&gt;

&lt;p&gt;When you replace a platform flow with your own, the work is not finished when yours works. It is finished when the platform's version is off.&lt;/p&gt;

&lt;p&gt;Until then you do not have a verified email address. You have two claims about a verified email address, maintained by different systems, agreeing by coincidence — and coincidences hold right up until a user does something entirely reasonable, like reading their inbox once instead of twice.&lt;/p&gt;

</description>
      <category>auth</category>
      <category>webdev</category>
      <category>programming</category>
      <category>database</category>
    </item>
    <item>
      <title>Your Toolchain Checks Grammar, Not Facts</title>
      <dc:creator>Ted</dc:creator>
      <pubDate>Wed, 29 Jul 2026 08:00:21 +0000</pubDate>
      <link>https://dev.to/henry_dan_81513dd35a2f540/your-toolchain-checks-grammar-not-facts-37ba</link>
      <guid>https://dev.to/henry_dan_81513dd35a2f540/your-toolchain-checks-grammar-not-facts-37ba</guid>
      <description>&lt;p&gt;I went looking for one wrong email address and found six wrong domains.&lt;/p&gt;

&lt;p&gt;They had been in production for months, across six different server functions. Nothing had ever complained, because there was nothing in the stack capable of complaining. Every one of them was valid code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually there
&lt;/h2&gt;

&lt;p&gt;Three distinct hosts, none of which I own.&lt;/p&gt;

&lt;p&gt;The first was the &lt;code&gt;.com&lt;/code&gt; version of my own brand, which I have never owned — the site runs on a different TLD. Somewhere along the way, generated code decided the &lt;code&gt;.com&lt;/code&gt; was the obvious address and used it anyway: in a contact-form reply telling users to email support there, and in admin links inside security alerts.&lt;/p&gt;

&lt;p&gt;The second was the &lt;code&gt;.io&lt;/code&gt; version. Same idea, different guess. This one was worse, because it was not only a link:&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;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alerts &amp;lt;alerts@brand.io&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the &lt;em&gt;sender&lt;/em&gt; on a batch of subscriber notifications. Transactional email providers verify sending domains. You cannot send from a domain you have not proven you control. So every one of those emails failed at the provider, silently, from the day the code shipped.&lt;/p&gt;

&lt;p&gt;The third was the provider's own shared test domain — the address used in every tutorial and quickstart:&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;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;App &amp;lt;noreply@resend.dev&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one was on the password reset email. Shared test domains are heavily restricted, typically to your own account address. So the one email in the system that a locked-out user depends on was going out from the least deliverable sender available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why none of it was caught
&lt;/h2&gt;

&lt;p&gt;Consider what each stage of a normal pipeline actually knows.&lt;/p&gt;

&lt;p&gt;The typechecker knows &lt;code&gt;"alerts@brand.io"&lt;/code&gt; is a string, and a string is what the field wants. Correct, and finished.&lt;/p&gt;

&lt;p&gt;The bundler knows the file parses. Correct, and finished.&lt;/p&gt;

&lt;p&gt;The tests know the function returns the shape they asserted. If they mock the mail client — and they do, because nobody sends real email in CI — they cannot see the sender at all.&lt;/p&gt;

&lt;p&gt;The deploy knows the function compiled and uploaded.&lt;/p&gt;

&lt;p&gt;Not one of these stages holds a fact about which domains I own. That is not an oversight in any of them. Type systems check the shape of values, not their correspondence to the world. There is no type that means "a domain this organisation controls."&lt;/p&gt;

&lt;p&gt;So the whole apparatus is checking grammar. The sentence is well-formed. Nobody is checking whether it is true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generated code fails where the plausible answer is wrong
&lt;/h2&gt;

&lt;p&gt;This is the part that has changed for me.&lt;/p&gt;

&lt;p&gt;When I write a domain by hand, I am reading it off something — a dashboard, a DNS record, an invoice. When a model writes one, it is producing the most plausible token sequence given the context. For a brand named X, &lt;code&gt;x.com&lt;/code&gt; is overwhelmingly the most plausible domain. For an email provider's sender field, the address from the documentation is the most plausible value.&lt;/p&gt;

&lt;p&gt;Both guesses are &lt;em&gt;excellent&lt;/em&gt; guesses. They are what a reasonable person would assume. They are also wrong, and wrong in a way that is invisible in review, because &lt;code&gt;support@brand.com&lt;/code&gt; looks correct on the page unless you already know the answer.&lt;/p&gt;

&lt;p&gt;That predicts where to look. These errors cluster wherever the correct value is &lt;strong&gt;guessable&lt;/strong&gt;: brand domains, support addresses, affiliate and partner IDs, API senders, dashboard URLs. Anywhere there is an obvious-looking answer, the obvious-looking answer is what you get, and it is right often enough that the exceptions never get checked.&lt;/p&gt;

&lt;p&gt;It is not a hallucination in the dramatic sense. Nothing is invented. Something real is substituted for something true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Declare your external surface
&lt;/h2&gt;

&lt;p&gt;The fix took about twenty minutes. Extract every URL host and email domain from the source, compare against a list of hosts you have declared, fail on anything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✗ 12 undeclared domain reference(s) in git ref abc1234:

  resend.dev  (5 references)
      functions/send-password-reset/index.ts:184
      functions/verify-code/index.ts:155
      … and 3 more

  brand.io   (3 references)
  brand.com  (3 references)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run against the commit from before I started fixing things by hand, it reported all eleven references I had found — plus one more I had missed entirely. A page was setting its canonical URL to a domain I do not own, which meant it had been telling search engines that the authoritative copy of that page lives on someone else's property.&lt;/p&gt;

&lt;p&gt;I found six by reading. The script found seven, in about a second, and will keep finding them.&lt;/p&gt;

&lt;p&gt;Two details matter more than the regex:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The allowlist belongs in the repository, not in configuration.&lt;/strong&gt; The failure mode is generated code introducing a plausible host. If the allowlist lives somewhere an assistant can quietly extend, it stops being a check and becomes a formality. In the repo, widening it is a diff, and a diff gets looked at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it runs is not obvious.&lt;/strong&gt; A pre-push hook is the tight loop, and it only sees commits that pass through your machine. If you use a hosted agent that commits and pushes on its own — mine does — a local hook is blind precisely where most of the generated code enters. So the same script also runs on a schedule against the remote branch, and alerts only when the set of offending hosts &lt;em&gt;changes&lt;/em&gt;, so a known-unfixed finding does not decay into noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;p&gt;It catches wrong constants. It does not catch wrong behaviour.&lt;/p&gt;

&lt;p&gt;The same week, I fixed a lookup that had been failing on every call for months because it queried a schema the API layer does not expose. Valid code, real function, correct arguments, and the error was being discarded so the failure read as an ordinary result. No domain check would ever see that. It is a different class and it needs a different tool.&lt;/p&gt;

&lt;p&gt;I would rather say that plainly than let a script imply more coverage than it has. A guard that quietly under-delivers is the thing we are trying to get away from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general version
&lt;/h2&gt;

&lt;p&gt;There is a category of error that generated code produces far more readily than handwritten code, and that conventional tooling is structurally unable to detect: &lt;strong&gt;values that are well-formed and untrue.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You cannot review your way out of it, because the values look right. You cannot typecheck your way out of it, because they are the right type. What you can do is write down the small set of facts your project depends on — which domains are yours, which accounts, which endpoints — and make a machine compare the code against that list every time.&lt;/p&gt;

&lt;p&gt;The list is the interesting artifact. Not because it is clever, but because until you sit down to write it, nobody has ever actually enumerated what your codebase is allowed to talk to.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>webdev</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>A Discarded Error Becomes an Answer</title>
      <dc:creator>Ted</dc:creator>
      <pubDate>Tue, 28 Jul 2026 20:03:11 +0000</pubDate>
      <link>https://dev.to/henry_dan_81513dd35a2f540/a-discarded-error-becomes-an-answer-hck</link>
      <guid>https://dev.to/henry_dan_81513dd35a2f540/a-discarded-error-becomes-an-answer-hck</guid>
      <description>&lt;p&gt;A user told me password reset was broken. They clicked the link, entered their address, got the confirmation message, and no code arrived. They tried again. They checked spam. Nothing.&lt;/p&gt;

&lt;p&gt;The endpoint was returning HTTP 200 with a friendly message every single time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The send was never reached
&lt;/h2&gt;

&lt;p&gt;The function writes the code to the database before it emails it. So the first useful question was not "did the email fail" but "is there a row". There wasn't — not for either attempt.&lt;/p&gt;

&lt;p&gt;That moves the fault upstream of anything to do with email, which is where I had been looking. Something returned early.&lt;/p&gt;

&lt;p&gt;Here is the line that did 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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;existingUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;normalizedEmail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maybeSingle&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;userExists&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;existingUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That call can never succeed. It reaches for the &lt;code&gt;auth&lt;/code&gt; schema through the REST layer, and the REST layer does not expose &lt;code&gt;auth&lt;/code&gt; — only &lt;code&gt;public&lt;/code&gt; and one other. The request fails every time it is made.&lt;/p&gt;

&lt;p&gt;You cannot tell from the code, because the error was never bound. Only &lt;code&gt;data&lt;/code&gt; was destructured. The failure had nowhere to go, so it went nowhere, and &lt;code&gt;existingUser&lt;/code&gt; came out &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three states, one variable
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;null&lt;/code&gt; here is doing an enormous amount of work it was never asked to do.&lt;/p&gt;

&lt;p&gt;There are three possible worlds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The lookup ran and found a user.&lt;/li&gt;
&lt;li&gt;The lookup ran and found nobody.&lt;/li&gt;
&lt;li&gt;The lookup could not run.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code models two of them. World three collapses into world two, because both produce &lt;code&gt;null&lt;/code&gt;, and &lt;code&gt;!!null&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt; either way. An infrastructure failure and a factual answer about a person become the same value.&lt;/p&gt;

&lt;p&gt;And once they are the same value, the next line acts on it with total confidence:&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;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;userExists&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Don't reveal whether the account exists.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&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;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;If an account exists, a reset code will be sent&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;
  
  
  The privacy feature was the camouflage
&lt;/h2&gt;

&lt;p&gt;This is the part I keep turning over.&lt;/p&gt;

&lt;p&gt;That branch is correct. It is there on purpose. If a password reset endpoint answers differently for a registered address than an unregistered one, it becomes an oracle: anyone can feed it addresses and learn who has an account. Refusing to distinguish the two cases is standard practice and it is the right call.&lt;/p&gt;

&lt;p&gt;But look at what it does in combination with the swallowed error.&lt;/p&gt;

&lt;p&gt;The endpoint's job, in that branch, is to be &lt;em&gt;indistinguishable from success while doing nothing&lt;/em&gt;. That is the specification. So when a total backend failure routed into it, the failure inherited a response designed from the ground up to look exactly like the working path — to the user, to the client code, to logs, to monitoring.&lt;/p&gt;

&lt;p&gt;A deliberate decision to reveal less became the reason nobody could see the outage. The feature was not bypassed or defeated. It worked perfectly, on the wrong input.&lt;/p&gt;

&lt;p&gt;I do not think there is a clever fix for that tension. Anti-enumeration branches have to be quiet. What has to change is what is allowed to reach them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same shape, three more times that day
&lt;/h2&gt;

&lt;p&gt;Once I knew what I was looking for, it was not one bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same lookup appeared in the function that completes the reset.&lt;/strong&gt; It needed the user's id to set the new password, got &lt;code&gt;null&lt;/code&gt;, and returned "user not found". So even if I had fixed the sending half, a correct code would still have failed to change anything. Two functions, one root cause, and fixing either alone would have looked like the fix didn't work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A third copy guarded invitations&lt;/strong&gt; — "does this person already have an account?" It always answered no, so the guard never fired.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rate limiter counted rows in a table that does not exist.&lt;/strong&gt; Same pattern: query fails, error discarded, &lt;code&gt;data&lt;/code&gt; is null, &lt;code&gt;null?.length || 0&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt;, zero is below the threshold, no limit applied. A rate limiter that had never once limited anything, reporting healthy by returning a number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Silence is not evidence
&lt;/h2&gt;

&lt;p&gt;The common thread is not carelessness about errors. It is that in each case, an absence was allowed to &lt;em&gt;mean&lt;/em&gt; something.&lt;/p&gt;

&lt;p&gt;No row means no user. No count means no traffic. No output means no problems. Each of those readings is reasonable, and each is only valid if you already know the check ran. Nothing in the code established that, so the inference ran on an assumption that was never checked.&lt;/p&gt;

&lt;p&gt;That is worth separating from the ordinary advice to handle your errors. The problem is not an unhandled exception crashing something. Nothing crashed. The problem is that a failure was converted into a confident, plausible, &lt;em&gt;wrong&lt;/em&gt; claim about the world, and then acted on.&lt;/p&gt;

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

&lt;p&gt;The lookup now goes through a database function that can read the table it needs, and — more to the point — the error is bound and checked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lookupError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth_user_id_by_email&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;p_email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;normalizedEmail&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;lookupError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lookup failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lookupError&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&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;Could not process reset request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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;userId&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&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;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;If an account exists, a reset code will be sent&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;Same two branches as before, plus a third that was always necessary and always missing. The rule it encodes is small enough to state in one line:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A lookup that could not run is not a verdict about the user.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is a 500. It is our problem, not a fact about them. The quiet branch is still quiet, but only genuine answers are allowed to reach it.&lt;/p&gt;

&lt;p&gt;The rate limiter got the same treatment — a failed count now returns an error rather than a permissive zero. If you cannot tell whether someone is over the limit, "0" is a guess wearing a number's clothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  It used to work
&lt;/h2&gt;

&lt;p&gt;The part I find hardest to sit with is that this was not a thing nobody ever got right. It worked for months. Here is what the broken lookup replaced:&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;// Check if user exists&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listUsers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userExists&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That version goes through the admin API, which can read the table. It loads every user and scans them in memory, which is genuinely wasteful and gets worse as you grow. Replacing it was a reasonable thing to want.&lt;/p&gt;

&lt;p&gt;The commit that replaced it left a comment saying exactly what it was for:&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;// Targeted user lookup (O(1)) instead of listing all users&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That comment is honest and the intent behind it is good. The new query really is O(1). It is also a query against a schema the REST layer does not expose, so it returns nothing, forever — and because the error was dropped, "nothing" got read as "no such user" and routed into the branch designed to look like success.&lt;/p&gt;

&lt;p&gt;An optimisation replaced a slow correct answer with a fast wrong one, and every downstream signal said fine.&lt;/p&gt;

&lt;p&gt;That reframes what the failure actually is. Not neglect, not inexperience — a deliberate improvement to a working system, applied without a way to notice it had stopped working. The old code was scanning every user on every request and that was &lt;em&gt;visible&lt;/em&gt;: it would have shown up in timings, in cost, in anything watching. The replacement was instant, because doing nothing is very fast.&lt;/p&gt;

&lt;p&gt;Nobody reported it in the ten weeks it was broken. They tried, got a reassuring message, and left. I had no signal at all, because the endpoint was reporting success the entire time.&lt;/p&gt;

&lt;p&gt;It was found because one person mentioned it in passing, and because a row that should have existed didn't.&lt;/p&gt;

&lt;p&gt;If you have an endpoint that deliberately returns the same response whether or not it did anything, go and check that it does the thing. It is the one place in your system where working and broken were &lt;em&gt;designed&lt;/em&gt; to look identical, and that design is doing its job for both.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>supabase</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>It Passed Because It Never Looked</title>
      <dc:creator>Ted</dc:creator>
      <pubDate>Mon, 27 Jul 2026 06:17:34 +0000</pubDate>
      <link>https://dev.to/henry_dan_81513dd35a2f540/it-passed-because-it-never-looked-552l</link>
      <guid>https://dev.to/henry_dan_81513dd35a2f540/it-passed-because-it-never-looked-552l</guid>
      <description>&lt;p&gt;I ran the typechecker. It passed. I ran the build. It passed. I pushed, and a page that carries a large share of the site's traffic started rendering an error screen instead of content.&lt;/p&gt;

&lt;p&gt;The error was not subtle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error TS2350: Only a void function can be called with the 'new' keyword.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is about as blunt as TypeScript gets. It is not an edge case, not a version-specific quirk, not something that needs strict mode to surface. It is the compiler saying you tried to &lt;code&gt;new&lt;/code&gt; something that cannot be constructed.&lt;/p&gt;

&lt;p&gt;So why did four consecutive clean runs of &lt;code&gt;npx tsc --noEmit&lt;/code&gt; tell me everything was fine?&lt;/p&gt;

&lt;h2&gt;
  
  
  The command examined nothing
&lt;/h2&gt;

&lt;p&gt;The project's &lt;code&gt;tsconfig.json&lt;/code&gt; looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"..."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"references"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&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.app.json"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&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.node.json"&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;This is a &lt;em&gt;solution-style&lt;/em&gt; config. It contains no source files of its own — &lt;code&gt;"files": []&lt;/code&gt; says so explicitly. It exists to point at two other configs, one for application code and one for build tooling. The pattern is normal and sensible; it lets you apply different rules to your app than to your config files.&lt;/p&gt;

&lt;p&gt;The catch is what plain &lt;code&gt;tsc&lt;/code&gt; does with it. Given a config with project references, &lt;code&gt;tsc&lt;/code&gt; &lt;strong&gt;does not descend into the referenced projects&lt;/strong&gt;. It typechecks what the config directly includes, which here is nothing at all, finds no errors in that empty set, and exits zero.&lt;/p&gt;

&lt;p&gt;The command that actually checks the application code is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsc &lt;span class="nt"&gt;-p&lt;/span&gt; tsconfig.app.json &lt;span class="nt"&gt;--noEmit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that against the same commit and both errors appear immediately, with file and line numbers.&lt;/p&gt;

&lt;p&gt;This is not an exotic misconfiguration I inflicted on myself. It is the shape that scaffolding tools generate by default for React and TypeScript projects. A very large number of repositories have this exact layout, and in every one of them the reflexive &lt;code&gt;npx tsc --noEmit&lt;/code&gt; is a no-op that looks like a pass.&lt;/p&gt;

&lt;p&gt;The build does not save you either. Modern bundlers transpile TypeScript by stripping the types — they never check them. A clean build tells you the syntax parsed. It says nothing about whether the types are coherent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug itself was a dead import
&lt;/h2&gt;

&lt;p&gt;The underlying mistake is almost funny in isolation. The file imported a set of icons from an icon library, and the list included one named &lt;code&gt;Map&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;MapPin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ExternalLink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CheckCircle2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ListChecks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ArrowRight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ShieldCheck&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lucide-react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That import shadows the global &lt;code&gt;Map&lt;/code&gt; constructor for the entire module. So when I later wrote what I thought was an ordinary lookup table:&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;websiteBySlug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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="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;website&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;r&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;website&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 was not constructing a &lt;code&gt;Map&lt;/code&gt;. I was trying to call &lt;code&gt;new&lt;/code&gt; on a React component that renders an SVG. Hence: &lt;em&gt;only a void function can be called with the &lt;code&gt;new&lt;/code&gt; keyword&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The detail that stuck with me: &lt;strong&gt;that icon was never rendered anywhere in the file.&lt;/strong&gt; It was a leftover from an earlier edit, an unused import with no visible effect on the page. Its only remaining function in the codebase was to shadow a global constructor and lie in wait. Deleting the line was the entire fix.&lt;/p&gt;

&lt;p&gt;If you use an icon library, it is worth knowing which of its exports collide with JavaScript globals. &lt;code&gt;Map&lt;/code&gt;, &lt;code&gt;Set&lt;/code&gt;, &lt;code&gt;Image&lt;/code&gt;, &lt;code&gt;Text&lt;/code&gt;, &lt;code&gt;Menu&lt;/code&gt;, &lt;code&gt;Link&lt;/code&gt;, &lt;code&gt;History&lt;/code&gt;, &lt;code&gt;Option&lt;/code&gt;, &lt;code&gt;Search&lt;/code&gt;, &lt;code&gt;Frame&lt;/code&gt; — all common icon names, all real globals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Green and zero are not the same kind of wrong
&lt;/h2&gt;

&lt;p&gt;I have written before about a dashboard reading zero when nothing was counting — a null result produced by the instrument rather than the world.&lt;/p&gt;

&lt;p&gt;A false green is worse, and the reason is behavioural rather than technical.&lt;/p&gt;

&lt;p&gt;A zero is uncomfortable. It contradicts what you hoped for, so it invites a second look. Even when you accept it, you accept it as a finding, and findings get revisited.&lt;/p&gt;

&lt;p&gt;A green result closes the question. It is the outcome you wanted, arriving in the form you expected, and it terminates the investigation by design. That is its whole purpose. Nobody audits a passing check, because a passing check is the thing you run &lt;em&gt;instead of&lt;/em&gt; auditing.&lt;/p&gt;

&lt;p&gt;So the two possible meanings of my clean exit code —&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;everything was examined and nothing was wrong&lt;/li&gt;
&lt;li&gt;nothing was examined&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;— are indistinguishable from outside, and only one of them prompts any further curiosity. I got the second one four times and read it as the first, because that is what green is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tell was there
&lt;/h2&gt;

&lt;p&gt;There was evidence, and I walked past it repeatedly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx tsc --noEmit&lt;/code&gt; produced &lt;strong&gt;no output whatsoever&lt;/strong&gt; on a codebase of several hundred routes. Not a warning, not a note, not a summary line. Just an instant return to the prompt.&lt;/p&gt;

&lt;p&gt;That should have registered. A typechecker that finishes instantly on a large project and says nothing has either done something impressive or done nothing at all, and the second is far more likely. I read the silence as cleanliness. Silence and success produce identical terminal output, which is precisely the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the verifier
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Make your check fail on purpose.&lt;/strong&gt; Introduce a deliberate error — a genuinely broken line — and confirm the tool actually complains. If it stays green, you have not discovered a robust codebase. You have discovered that your check does not run.&lt;/p&gt;

&lt;p&gt;I first wrote that as a setup step: do it once when you create the project, and again if you inherit a repository or change build tooling. &lt;a href="https://dev.to/henry_dan_81513dd35a2f540/it-passed-because-it-never-looked-552l/comments/3c0l9"&gt;FromZeroToShip&lt;/a&gt; pushed back on the word &lt;em&gt;once&lt;/em&gt;, and their counterexample is sharper than my own.&lt;/p&gt;

&lt;p&gt;Their scanner had an exclusion rule that quietly stopped excluding what it was meant to. Six files were scored wrong for weeks and nobody opened the report, because it was green. They had already run the deliberate-break test months earlier and it had passed — the check was genuinely falsifiable on the day they tested it. Then the pattern it matched widened, the failure condition stopped being reachable, and "no errors" stayed true for the wrong reason.&lt;/p&gt;

&lt;p&gt;That is my ending reached by a different road. Mine was never reachable. Theirs stopped being reachable. Neither transition emits anything at the moment it happens, so a one-time proof has an expiry date you cannot read from the outside.&lt;/p&gt;

&lt;p&gt;Which means the deliberate break should not be a habit. It should be a job. Break each guard on every run, and require it to go red before its verdict counts.&lt;/p&gt;

&lt;p&gt;And require it to go red &lt;em&gt;for its own reason&lt;/em&gt;. A nonzero exit is not enough — that only says something failed, not that this check found this fault. My own failure signature was not a wrong answer, it was no answer: exit code 0 with empty output, byte-identical to exit code 0 on a genuinely clean codebase. An assertion on the content of the complaint — &lt;em&gt;given this broken line, I expect TS2350 in this file&lt;/em&gt; — separates them immediately, because an empty check has nothing to say. Silence only becomes evidence once something is required to speak.&lt;/p&gt;

&lt;p&gt;The generalisation goes past TypeScript. Any verification step can degrade into theatre: a test suite where a path filter stopped matching, a linter whose config no longer resolves, an integration check pointed at a stale environment. In each case the pipeline goes green, the ritual is observed, and nothing is being verified.&lt;/p&gt;

&lt;p&gt;An unrun check is not neutral. It is worse than having no check at all, because no check leaves you appropriately nervous, and a broken one sells you confidence you have not earned.&lt;/p&gt;

&lt;p&gt;I shipped a crash to a production page with four green checkmarks behind me. The compiler had the answer the entire time. I just never asked it anything.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>webdev</category>
      <category>programming</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Zero Is Not a Measurement</title>
      <dc:creator>Ted</dc:creator>
      <pubDate>Tue, 21 Jul 2026 13:19:11 +0000</pubDate>
      <link>https://dev.to/henry_dan_81513dd35a2f540/zero-is-not-a-measurement-3b8d</link>
      <guid>https://dev.to/henry_dan_81513dd35a2f540/zero-is-not-a-measurement-3b8d</guid>
      <description>&lt;p&gt;The affiliate dashboard said zero.&lt;/p&gt;

&lt;p&gt;Not a small number — zero. One booking platform account had been quietly accumulating commissions for months. The other, for a different product category on the same site, had never recorded anything at all.&lt;/p&gt;

&lt;p&gt;The conclusion writes itself. That category gets a fraction of the traffic. The audience is narrower. The intent is weaker. Some channels just don't convert, and the honest thing to do is stop pretending otherwise and put the effort somewhere with a pulse.&lt;/p&gt;

&lt;p&gt;I'd been reasoning that way for a while. It's a reasonable read of a zero.&lt;/p&gt;

&lt;p&gt;Then I actually looked at the links.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing was counting
&lt;/h2&gt;

&lt;p&gt;There were ten of them, all pointing at the booking platform's public product pages. Clean URLs. They worked — click one and you land exactly where you should, on the right product, ready to book.&lt;/p&gt;

&lt;p&gt;Not one of them carried a partner ID.&lt;/p&gt;

&lt;p&gt;The platform had no way to know those visitors came from my site. No parameter, no identifier, nothing to attribute a booking to. Every click for months had arrived as anonymous traffic. The commission wasn't small. It was structurally impossible.&lt;/p&gt;

&lt;p&gt;The detail that makes this worth writing down is what came next. I checked whether the platform's integration was set up at all — and it was. Their analytics script was sitting in the page head, correctly installed, carrying the right partner ID. Someone had done that step properly and reasonably concluded the integration was finished.&lt;/p&gt;

&lt;p&gt;It wasn't finished. Those are two different systems doing two different jobs. The script powers the vendor's widget analytics. Attribution comes from the parameter on the outbound link. Installing one tells you nothing about the other, and the dashboard that would have revealed the gap was the same dashboard reading zero.&lt;/p&gt;

&lt;p&gt;So the setup looked complete, the links looked fine, the clicks were real, and the earnings were structurally guaranteed to be nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The money wasn't the loss
&lt;/h2&gt;

&lt;p&gt;At the volume involved, the forgone commission was trivial. Pennies, honestly.&lt;/p&gt;

&lt;p&gt;What I actually lost was the ability to learn anything. For as long as that channel was uninstrumented, a click that converted and a click that went nowhere produced &lt;em&gt;identical&lt;/em&gt; output: silence. There was no experiment I could have run, no amount of patience, no traffic threshold that would have separated "this doesn't work" from "nothing is watching."&lt;/p&gt;

&lt;p&gt;And I had been drawing conclusions from it the whole time. Every judgment I'd made about that category — that the demand was weak, that it wasn't worth building out — came from a system that was incapable of reporting success. The zero wasn't evidence. It was a measurement that had never been shown to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then it happened again, four hours later
&lt;/h2&gt;

&lt;p&gt;Same day, unrelated system.&lt;/p&gt;

&lt;p&gt;I was auditing internal links in a build output, trying to work out why a particular page had almost no search visibility. I searched the generated HTML for links pointing to it and got zero results. Nothing on the entire site linked to this page.&lt;/p&gt;

&lt;p&gt;That's a real diagnosis with a real fix, and I wrote it up as one. Orphaned page. No inbound links. That's why it's invisible.&lt;/p&gt;

&lt;p&gt;The search pattern was wrong. The build writes links as absolute URLs — full domain and all — and I'd searched for the relative form. When I corrected it, the page had nineteen inbound links. More than any comparable page on the site. It was one of the &lt;em&gt;best&lt;/em&gt;-linked pages I had.&lt;/p&gt;

&lt;p&gt;I'd already built a recommendation on top of that zero before I checked it.&lt;/p&gt;

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

&lt;p&gt;Here's the thing I keep relearning.&lt;/p&gt;

&lt;p&gt;A zero is not an observation about the world. It's a &lt;strong&gt;claim your instrument makes about the world&lt;/strong&gt; — and a broken instrument makes that claim with exactly as much confidence as a working one.&lt;/p&gt;

&lt;p&gt;"Nothing happened" and "nothing was recorded" are indistinguishable from inside the data. There is no field that separates them. Both render as the same empty result set, the same flat line, the same blank dashboard panel.&lt;/p&gt;

&lt;p&gt;This is what makes null results uniquely treacherous compared to wrong ones. A wrong number tends to announce itself eventually — it contradicts something, it fails a sanity check, someone says &lt;em&gt;that can't be right&lt;/em&gt;. A zero contradicts nothing. It's perfectly consistent with a world in which the thing you're measuring simply doesn't happen. It slots neatly into whatever story you already believed, which in both my cases was a story I found quite easy to accept.&lt;/p&gt;

&lt;p&gt;Worse, a zero is &lt;em&gt;actionable&lt;/em&gt; in a way that invites you to stop looking. It says: this channel is dead, this page is orphaned, move on. It closes the question. Both of my zeros came with a ready-made next step attached, and in both cases the next step was work I'd have done for nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The habit that catches it
&lt;/h2&gt;

&lt;p&gt;The fix is embarrassingly cheap, which is why it's worth making automatic: &lt;strong&gt;every zero needs a positive control.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before you believe a null result, point the same instrument at something you already know should be non-zero. Not a different instrument — the same one, same query, same pattern, same code path. Then check that it returns something.&lt;/p&gt;

&lt;p&gt;If I'd counted inbound links to a page I &lt;em&gt;knew&lt;/em&gt; was heavily linked and gotten zero for that too, the broken pattern would have surfaced in about ten seconds. The failure was entirely visible; I just never gave it a chance to show itself, because I only ever asked the question where I didn't already know the answer.&lt;/p&gt;

&lt;p&gt;The affiliate case is the same shape. One deliberate test click through the full path — link, redirect, dashboard — would have made the silence audible immediately. That test is five minutes and you do it once, at setup, when you still remember what the system is supposed to do.&lt;/p&gt;

&lt;p&gt;The general rule I'd write on the wall: &lt;strong&gt;any measurement pipeline that has never once produced a non-zero reading has not been tested.&lt;/strong&gt; It's not reporting an absence. It's reporting nothing at all, and you have no basis to tell those apart.&lt;/p&gt;

&lt;p&gt;Sometimes you can't manufacture a positive. The event is genuinely rare, or producing one costs real money, or the only way to get one is to write fake data into production and hope you remember to take it out. That situation is common enough that pretending otherwise would make the rest of this advice easy to dismiss.&lt;/p&gt;

&lt;p&gt;When it happens, the answer isn't to shrug and trust the zero anyway. It's to downgrade what the zero entitles you to claim. An uncontrolled null is not a negative result — it's an &lt;strong&gt;unknown&lt;/strong&gt;, and it should sit in your head as &lt;em&gt;I have no reading&lt;/em&gt; rather than &lt;em&gt;the reading is nothing&lt;/em&gt;. Those two states feel identical and justify completely different decisions. One of them tells you to shut the channel down; the other tells you that you still have work to do before you're allowed an opinion.&lt;/p&gt;

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

&lt;p&gt;The links carry their partner ID now, and I confirmed the format against the platform's own link generator rather than guessing the parameter name — because a wrong parameter would have failed in precisely the same silent way, and I'd have congratulated myself on a fix that changed nothing.&lt;/p&gt;

&lt;p&gt;The bigger change is procedural. Whenever a check comes back empty now, I ask one question before I do anything with it: &lt;em&gt;what would this look like if my measurement were broken?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the answer is "exactly like this" — and with a zero, the answer is almost always "exactly like this" — then I haven't found anything yet. I've just found a place where I need to look harder.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>webdev</category>
      <category>programming</category>
      <category>testing</category>
    </item>
    <item>
      <title>You Deleted It. The Crawler Still Sees It.</title>
      <dc:creator>Ted</dc:creator>
      <pubDate>Sun, 19 Jul 2026 07:56:19 +0000</pubDate>
      <link>https://dev.to/henry_dan_81513dd35a2f540/you-deleted-it-the-crawler-still-sees-it-183c</link>
      <guid>https://dev.to/henry_dan_81513dd35a2f540/you-deleted-it-the-crawler-still-sees-it-183c</guid>
      <description>&lt;p&gt;I removed two listings from a directory site I run. Not for a small reason — they failed the one promise the directory exists to make. The kind of removal you do immediately and double-check.&lt;/p&gt;

&lt;p&gt;I did double-check. Flipped the flag in the database, loaded the site, watched the queries filter them out. Gone from the city pages, gone from the main directory. Removal confirmed.&lt;/p&gt;

&lt;p&gt;A day later, both were still on the site.&lt;/p&gt;

&lt;p&gt;Not on some cached page or stale CDN edge. In the current, freshly deployed HTML — sitting in the exact lists I had watched them disappear from. I had verified the deletion in one document. The site is two.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rendered site is two documents
&lt;/h2&gt;

&lt;p&gt;Here's the architecture, because the bug lives in it. The site is a single-page app: the browser downloads a JavaScript bundle, the bundle fetches data, the page assembles itself client-side. Crawlers are historically bad at waiting around for that, so — like a lot of SPA operators — I run a prerender step at build time. A script fetches the same data, bakes real content into each route's static HTML, and &lt;em&gt;that&lt;/em&gt; is what robots, link previews, and anyone with broken JavaScript actually receives. The bundle takes over afterward and re-renders everything live.&lt;/p&gt;

&lt;p&gt;It works. It's also a decision with a consequence I under-appreciated for months: &lt;strong&gt;every fact on the site now exists twice.&lt;/strong&gt; Once in the live render, computed in the browser from a fresh query. And once in the baked copy, computed at build time by a different script, from a different query, through a different code path.&lt;/p&gt;

&lt;p&gt;Two documents. Two authors. One URL.&lt;/p&gt;

&lt;p&gt;And the two authors don't consult each other. The browser filters listings with a &lt;code&gt;WHERE allowed = true&lt;/code&gt;. The build script — written earlier, for a different purpose — never selected that column. So when I "deleted" the two properties, the browser's query dutifully dropped them, and the build script kept baking both into the static HTML on every deploy, complete with links and ratings. From its perspective, the deletion had never happened.&lt;/p&gt;

&lt;p&gt;The deletion was real in one document and fiction in the other. Nothing errored, because nothing was wrong: both documents were internally consistent, both rendered cleanly, both passed every check that looks at documents one at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three divergences, one day
&lt;/h2&gt;

&lt;p&gt;Once I knew to look for the split, I found it three times in a single day — same site, escalating stakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The title fix that wasn't.&lt;/strong&gt; A page was earning search impressions for a phrase its title didn't contain, so I added the phrase. Checked the page in the browser: there it was, in the tab, in the markup. But client-side titles are set by the bundle &lt;em&gt;after&lt;/em&gt; load — the crawler reads the baked &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, and the baked title still didn't have the phrase. The fix was fully live for every human and invisible to the one reader that determines whether the page ranks. It had been "fixed" in the wrong document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The labels that didn't propagate.&lt;/strong&gt; I'd spent that week making the site more honest — replacing a blanket "verified" badge with per-listing labels that say what's actually confirmed versus merely claimed. The app rendered the new labels beautifully. The baked copy went right on telling crawlers every listing was verified, in a header the build script had been stamping onto nine different pages. The honesty overhaul shipped to the browser and not to the record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The deletion.&lt;/strong&gt; The first two failures overstate; this one &lt;em&gt;resurrects&lt;/em&gt;. A removed listing isn't a stale adjective. It's an entity I had decided, for cause, should not be presented — still being presented, by a document I'd forgotten was the one that counts.&lt;/p&gt;

&lt;p&gt;And the failure runs in both directions. While fixing the build script I added a column to its fetch that didn't exist in that table. The data API rejected the query — and the script, hitting an error, quietly returned an empty list, at which point every detail page fell back to a generic template. No build failure. No warning. Just a deploy where the baked document silently regressed while the live one stayed perfect. Two documents means twice the ways to be wrong, and each one's failures are invisible from inside the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nothing catches this
&lt;/h2&gt;

&lt;p&gt;Every tool I had was pointed at one document or the other. Never at the seam.&lt;/p&gt;

&lt;p&gt;The type checker checks the app. The tests exercise the app. The browser — where all manual verification happens, because that's where &lt;em&gt;you&lt;/em&gt; look at your site — runs the app. Meanwhile the build logs confirm the prerender ran, which is not the same as confirming what it wrote. The bug wasn't in either document. It was in the fact that there were two.&lt;/p&gt;

&lt;p&gt;This is the oldest failure shape in data systems — the same fact stored in two places will drift, given time — wearing a disguise that makes it easy to miss: the two copies here aren't two database rows or two config files. They're two &lt;em&gt;renderings of the same URL&lt;/em&gt;, which is precisely why it doesn't feel like duplication. It feels like one page. You'd never think to diff a page against itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Done" names a document
&lt;/h2&gt;

&lt;p&gt;The fix for the specific bugs was mundane: make the build fetch select the flag, filter on it, propagate the labels, correct the title — in the baked layer, where each change should have landed first. The durable fix was changing what I accept as evidence that a change shipped.&lt;/p&gt;

&lt;p&gt;Looking at the site in a browser verifies the live document. It verifies nothing about the baked one. So the definition of done, for anything that matters to a crawler — a title, a claim, a listing's existence — is now mechanical: fetch the served HTML the way a robot would, with no JavaScript executed, and confirm the change is in the bytes. &lt;code&gt;curl&lt;/code&gt;, grep, done. Ten seconds. It would have caught all four incidents before deploy, and it's the only check that examines the document I kept forgetting existed.&lt;/p&gt;

&lt;p&gt;If your site renders twice, you don't have a page. You have a page and its understudy, and the understudy is the one performing for the audience that decides whether anyone finds you. Verify the performer that's actually on stage.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Further reading&lt;/strong&gt; — adjacent failure modes from the same system: &lt;a href="https://tedagentic.com/posts/prerender-silent-failure" rel="noopener noreferrer"&gt;the prerender pipeline that ran on every deploy while writing empty pages&lt;/a&gt;, and &lt;a href="https://tedagentic.com/posts/prose-is-a-copy-too" rel="noopener noreferrer"&gt;prose as one more unsynchronized copy of your data&lt;/a&gt;. Same lesson at different layers: anything that exists twice, drifts.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://tedagentic.com/posts/you-deleted-it-the-crawler-still-sees-it" rel="noopener noreferrer"&gt;tedagentic.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>javascript</category>
      <category>debugging</category>
    </item>
    <item>
      <title>The Listing Didn't Exist. The Booking Link Worked.</title>
      <dc:creator>Ted</dc:creator>
      <pubDate>Sat, 18 Jul 2026 18:55:00 +0000</pubDate>
      <link>https://dev.to/henry_dan_81513dd35a2f540/the-listing-didnt-exist-the-booking-link-worked-2i4d</link>
      <guid>https://dev.to/henry_dan_81513dd35a2f540/the-listing-didnt-exist-the-booking-link-worked-2i4d</guid>
      <description>&lt;p&gt;The property was called something plausible. It had a star rating, a price, a tidy paragraph of description, valid &lt;code&gt;LodgingBusiness&lt;/code&gt; structured data, and a green "Verified" badge. It also had a &lt;strong&gt;Book Now&lt;/strong&gt; button wired to a working affiliate link — the kind that pays a commission if someone clicks through and reserves a room.&lt;/p&gt;

&lt;p&gt;The property did not exist. Not "closed," not "renamed." It was never real. Some fast, AI-assisted build had generated it whole — name, blurb, rating, amenities — and pointed it at a live payment rail.&lt;/p&gt;

&lt;p&gt;It wasn't alone. There was a whole cohort of them, and the giveaway was almost funny: a dozen distinct "hotels" in different cities all sharing the &lt;em&gt;same&lt;/em&gt; affiliate link. Different names, different descriptions, one destination URL. A forger who signed every painting with the same signature.&lt;/p&gt;

&lt;p&gt;None of them were broken. That's the part worth sitting with. They passed every automated check I could throw at them. Valid types. Valid schema. No 404s, no build errors, no lint warnings. A validator confirmed they were well-formed, and being well-formed was the entire disguise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong implies there's a real thing being described
&lt;/h2&gt;

&lt;p&gt;I'd spent earlier weeks fixing data that was &lt;em&gt;wrong&lt;/em&gt; — a fact gone stale, an entity filed in the wrong city, a corrected value that never reached the page. Wrong is a solved kind of problem. Wrong means there's a true answer and the stored one disagrees. You audit, you compare against a source, you reconcile.&lt;/p&gt;

&lt;p&gt;This was different, and it took me a beat to see why. You cannot reconcile a fabrication against a source, because there is no referent. The row doesn't describe a real thing incorrectly. It describes nothing, confidently. There's no fact to check because there's no subject.&lt;/p&gt;

&lt;p&gt;So the first reframe: &lt;strong&gt;AI-generated seed data isn't a rough draft you refine. It's a forgery you have to detect&lt;/strong&gt; — and its defining property is that it validates. Every instinct we've built says malformed data is the dangerous data, because malformed data is what breaks things. But malformed data announces itself. The genuinely dangerous record is the one that is perfectly formed and completely false, because nothing in your pipeline is designed to doubt a row that parses.&lt;/p&gt;

&lt;p&gt;I deleted them. You don't refactor a forgery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quieter version of the same problem
&lt;/h2&gt;

&lt;p&gt;The fake listings were the loud case. Underneath them was a quieter one that had spread much further, and it's the part that actually changed how I think.&lt;/p&gt;

&lt;p&gt;Real listings had a policy field — a small piece of data recording whether a given property had actually been confirmed to permit a certain thing on-site. Most of them had never been confirmed. The field was empty.&lt;/p&gt;

&lt;p&gt;Here is what the page rendered for an empty field: a green &lt;strong&gt;Verified&lt;/strong&gt; badge. A "Yes" in the amenities grid. A line of structured data asserting the thing was permitted. A confident sentence in the server-rendered copy that a crawler reads before any of the interactive parts load.&lt;/p&gt;

&lt;p&gt;Nobody decided to claim these things. There was no bad actor and no bad data. There was one line of code, repeated in spirit across the codebase, that read roughly: &lt;em&gt;if this value is missing, treat it as true.&lt;/em&gt; A default. &lt;code&gt;?? true&lt;/code&gt;, or its cousin in four other files.&lt;/p&gt;

&lt;p&gt;That is the second reframe, and it's the one I'd tattoo on a new engineer: &lt;strong&gt;a default is a claim.&lt;/strong&gt; When you fill an unknown with the favorable value, you are not being helpful and you are not leaving it blank. You are asserting a fact you do not have. The empty field didn't stay empty on the way to the user. It was quietly promoted to a promise.&lt;/p&gt;

&lt;p&gt;Absence rendered as a yes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The missing value was "I don't know"
&lt;/h2&gt;

&lt;p&gt;Once I saw it in one place I saw it everywhere, and they all traced to the same root cause. The data model had two states where it needed three.&lt;/p&gt;

&lt;p&gt;There was &lt;code&gt;true&lt;/code&gt;, there was &lt;code&gt;false&lt;/code&gt;, and there was &lt;code&gt;null&lt;/code&gt; — but &lt;code&gt;null&lt;/code&gt; was never a state of its own. It was an &lt;em&gt;input to be resolved&lt;/em&gt;, and every layer resolved it the same optimistic way. The card coerced it to yes. The badge coerced it to yes. The structured data coerced it to yes. The prerendered copy a crawler reads coerced it to yes. One missing fact became four confident assertions, none of which had any way to represent the honest answer.&lt;/p&gt;

&lt;p&gt;Because there was no honest answer available. "Unverified" wasn't a value the system could hold. And a system that cannot represent "I don't know" does not fall silent about what it doesn't know. It reaches for the most flattering value in range and commits.&lt;/p&gt;

&lt;p&gt;The fix was not clever. It was to add the value that was missing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A real, explicit state for &lt;em&gt;unconfirmed&lt;/em&gt; — stored, not inferred.&lt;/li&gt;
&lt;li&gt;A three-way rendering instead of two: &lt;strong&gt;Yes&lt;/strong&gt;, &lt;strong&gt;No&lt;/strong&gt;, and &lt;strong&gt;Unconfirmed&lt;/strong&gt;. The unconfirmed state gets its own muted styling — never the green of a confirmed yes, and just as importantly never the red of a confirmed no. "We haven't checked" is a different claim from "this is not allowed," and collapsing them is its own kind of lie, pointed the other direction.&lt;/li&gt;
&lt;li&gt;Every layer taught to defer to that state instead of defaulting past it. The card, the badge, the structured data, the static copy the crawler reads — all four now say "unconfirmed" together, or say nothing, rather than four independent optimistic guesses.&lt;/li&gt;
&lt;li&gt;Positive claims gated behind an actual confirmed value, so the system asserts a thing is true only when something actually backs it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Give the model a way to shrug
&lt;/h2&gt;

&lt;p&gt;The lesson generalizes past any one field or any one site. It applies anywhere a system holds facts it can't fully back — which is everywhere, and increasingly so as more of the initial data gets generated rather than entered by someone who checked.&lt;/p&gt;

&lt;p&gt;We tend to think of honesty in a system as the &lt;em&gt;absence&lt;/em&gt; of false statements — no bugs, no stale rows, a clean audit. But you can pass every audit and still have a system that lies, because the lies aren't in the data. They're in the defaults. They're in what happens to a value you never had.&lt;/p&gt;

&lt;p&gt;Real honesty is the &lt;em&gt;presence&lt;/em&gt; of "unknown" as a first-class thing: a value you can store, a state you can render, a claim your structured data is allowed to decline to make. If the only options your model offers are yes and no, then every gap in your knowledge silently becomes whichever one flatters you — and the extreme end of that gradient is a hotel that doesn't exist with a working button to book it.&lt;/p&gt;

&lt;p&gt;Give the model an explicit way to say "I don't know." Otherwise it will nod at everything, and you won't find out where until someone audits the room and discovers half of it was painted by the same forger.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Further reading&lt;/strong&gt; — the adjacent failure modes: &lt;a href="https://tedagentic.com/posts/ai-confidence-gap-production-sites" rel="noopener noreferrer"&gt;why AI tools produce confident silent errors on production sites&lt;/a&gt;, and &lt;a href="https://tedagentic.com/posts/fallback-chain-error-suppression" rel="noopener noreferrer"&gt;why a fallback chain hides missing data by design&lt;/a&gt;. This post is the layer under both: why the gap gets filled with a lie in the first place.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://tedagentic.com/posts/the-listing-didnt-exist" rel="noopener noreferrer"&gt;tedagentic.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>database</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Logging Into the Higgsfield CLI on a Server With No Browser</title>
      <dc:creator>Ted</dc:creator>
      <pubDate>Wed, 08 Jul 2026 04:08:43 +0000</pubDate>
      <link>https://dev.to/henry_dan_81513dd35a2f540/logging-into-the-higgsfield-cli-on-a-server-with-no-browser-34gi</link>
      <guid>https://dev.to/henry_dan_81513dd35a2f540/logging-into-the-higgsfield-cli-on-a-server-with-no-browser-34gi</guid>
      <description>&lt;p&gt;I wanted the &lt;a href="https://github.com/higgsfield-ai/cli" rel="noopener noreferrer"&gt;Higgsfield CLI&lt;/a&gt; on my server — a terminal front-end to a pile of image and video generation models, which makes it scriptable and cron-able in a way the web UI never will be. The install was one line. Then it asked me to log in, and a specific, recurring failure showed up that has nothing to do with Higgsfield and everything to do with a word that means two different things at once.&lt;/p&gt;

&lt;p&gt;The word is &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The server is headless: no monitor, no browser, reached only over SSH from a laptop. That detail is the whole story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow that assumes a lap
&lt;/h2&gt;

&lt;p&gt;Higgsfield authenticates with OAuth 2.0 PKCE, the way most modern CLIs do, and the polite version goes like this: the CLI opens a small HTTP listener on &lt;code&gt;localhost:&amp;lt;port&amp;gt;&lt;/code&gt;, prints a login URL, and waits. You open the URL in a browser, approve, and the identity provider (Higgsfield fronts theirs with Clerk) redirects your browser to &lt;code&gt;http://localhost:&amp;lt;port&amp;gt;/callback?code=...&lt;/code&gt;. The listener catches that request, reads the code out of it, trades it for a token, and you're in.&lt;/p&gt;

&lt;p&gt;On a laptop this is seamless because the browser and the listener are the same machine. &lt;code&gt;localhost&lt;/code&gt; resolves to the same place for both halves of the handshake. The whole design quietly depends on that.&lt;/p&gt;

&lt;p&gt;On a headless server it falls apart, because the two halves are now on different machines. The listener is on the server. The browser is on my laptop. When Clerk redirects the browser to &lt;code&gt;http://localhost:&amp;lt;port&amp;gt;/callback&lt;/code&gt;, the browser dutifully connects to &lt;em&gt;the laptop's&lt;/em&gt; localhost — where nothing is listening — and shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unable to connect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The login succeeded. The callback failed. The code was issued — it just landed on the wrong machine's front door.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A loopback OAuth flow assumes the browser and the listener share a host.&lt;/strong&gt; That assumption is invisible on a laptop and load-bearing on a headless box.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tunnel, and the wall the spec put in front of it
&lt;/h2&gt;

&lt;p&gt;The reflex fix is an SSH tunnel. Forward the callback port from the laptop to the server, so the laptop's &lt;code&gt;localhost:&amp;lt;port&amp;gt;&lt;/code&gt; actually reaches the server's listener:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;ssh&lt;/span&gt; -L &lt;span class="m"&gt;8799&lt;/span&gt;:localhost:8799 you@server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sound. It works. But I hit a wall trying to pin the port, and the wall is worth understanding because it's not a bug — it's the security model doing its job.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;higgsfield auth login&lt;/code&gt; has a &lt;code&gt;--port&lt;/code&gt; flag, so I told it to use &lt;code&gt;8799&lt;/code&gt; to match my tunnel. Clerk rejected the whole login:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error: invalid_request
The 'redirect_uri' parameter does not match any of the
Client's pre-registered redirect URIs.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the thing PKCE doesn't remove: the &lt;strong&gt;redirect URI allowlist&lt;/strong&gt;. PKCE lets a CLI skip shipping a client secret — the code challenge proves the token request came from whoever started the login — but the provider still refuses to send a code to any callback URL the app didn't register in advance. That allowlist is what stops a malicious page from pointing your authenticated redirect at &lt;em&gt;its&lt;/em&gt; server. Higgsfield registered a specific handful of ports with Clerk, and those are the only ones that work. &lt;code&gt;8799&lt;/code&gt; wasn't in the club.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The redirect URI is a registered constant, not a free parameter.&lt;/strong&gt; You don't get to pick the port; you get to pick from the ports the app's developer already blessed. Drop the &lt;code&gt;--port&lt;/code&gt; flag and the CLI falls back to its own default and it works — but now your tunnel has to match a port you don't fully control, and if that port is already busy on the server (mine collided with another service on the default), the CLI silently falls back again to a &lt;em&gt;different&lt;/em&gt; registered port, and your tunnel is aimed at the wrong one.&lt;/p&gt;

&lt;p&gt;The tunnel isn't wrong. It's just more moving parts than the problem actually needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The callback is just data
&lt;/h2&gt;

&lt;p&gt;The realization that made this easy: &lt;strong&gt;the callback is not a handshake, it's a message.&lt;/strong&gt; It's a plain HTTP GET whose entire payload is visible in the URL — &lt;code&gt;?code=&amp;lt;the code&amp;gt;&amp;amp;state=&amp;lt;the anti-forgery token&amp;gt;&lt;/code&gt;. The listener doesn't care &lt;em&gt;who&lt;/em&gt; delivers that message. It cares that the message arrives with a &lt;code&gt;state&lt;/code&gt; matching the one it issued.&lt;/p&gt;

&lt;p&gt;So I stopped trying to route the browser to the listener at all. I ran &lt;code&gt;higgsfield auth login&lt;/code&gt; on the server (letting it pick its own registered port — it landed on &lt;code&gt;8766&lt;/code&gt;), opened the printed URL in my laptop browser, approved, and let the redirect fail — &lt;code&gt;Unable to connect&lt;/code&gt;, as expected. Then I copied the dead URL straight out of the browser's address bar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:8766/callback?code=OWVH...F1L&amp;amp;state=kg6P...TCY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That URL is the whole message. The listener is sitting on the server's own localhost, exactly where the CLI is watching. So I replayed it there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"http://localhost:8766/callback?code=OWVH...F1L&amp;amp;state=kg6P...TCY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The listener saw a request on its port, checked the &lt;code&gt;state&lt;/code&gt;, matched it, exchanged the code for a token, and printed &lt;code&gt;Successfully authenticated.&lt;/code&gt; No tunnel. The browser did the one thing only a browser with my logged-in Higgsfield session could do — prove I'm me — and then I hand-carried the resulting code the last hop the browser couldn't make.&lt;/p&gt;

&lt;p&gt;Two things make this safe to lean on, and both are worth saying out loud:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;state&lt;/code&gt; parameter is the check that makes replay legitimate.&lt;/strong&gt; The listener issued that random token at the start and refuses any callback that doesn't echo it. I'm not bypassing a security control by replaying the URL — I'm satisfying the exact one the flow was built around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The code is short-lived and single-use.&lt;/strong&gt; An OAuth authorization code is a password with a fuse — good for one exchange, expiring in a minute or two. Copy it, replay it promptly, and don't paste it anywhere it'll be logged. It's not a token you're storing; it's a token you're spending immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd carry to any headless login
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;localhost&lt;/code&gt; is a per-machine word.&lt;/strong&gt; Any auth flow that redirects to &lt;code&gt;localhost&lt;/code&gt; is quietly assuming the browser and the service are the same box. On a headless server they aren't, and that mismatch — not the tool — is your bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirect URIs are an allowlist, not a setting.&lt;/strong&gt; PKCE drops the client secret but keeps the registered-callback rule. If pinning a port throws &lt;code&gt;invalid_request&lt;/code&gt;, you're fighting the security model, not a config typo. Use the app's own default port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A loopback callback is replayable data.&lt;/strong&gt; It's a URL carrying a code and a state. You don't need to &lt;em&gt;route&lt;/em&gt; the browser to the listener; you can let the redirect die and deliver the URL yourself with &lt;code&gt;curl&lt;/code&gt; on the machine that's listening.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;state&lt;/code&gt; is what makes hand-delivery honest.&lt;/strong&gt; It's the flow's own anti-forgery check. Replaying a URL that carries the right &lt;code&gt;state&lt;/code&gt; isn't a workaround around the security — it's the security working.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tunnel is the textbook answer, and some days you'll want it. But when a login on a headless machine tells you it &lt;em&gt;can't connect to localhost&lt;/em&gt;, remember that it already succeeded — the code exists — and the last hop is just a URL you're allowed to carry across yourself. This worked cleanly for Higgsfield; it'll work for any CLI whose login is a loopback redirect, which by now is most of them.&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>devops</category>
      <category>cli</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
