<?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: Danish ali siddiqui</title>
    <description>The latest articles on DEV Community by Danish ali siddiqui (@bigachiever).</description>
    <link>https://dev.to/bigachiever</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%2F3484623%2F77e6292f-b374-492c-af47-3b05075e593b.jpeg</url>
      <title>DEV Community: Danish ali siddiqui</title>
      <link>https://dev.to/bigachiever</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bigachiever"/>
    <language>en</language>
    <item>
      <title>The same Rust gave two different answers, and neither matched JavaScript</title>
      <dc:creator>Danish ali siddiqui</dc:creator>
      <pubDate>Tue, 18 Aug 2026 08:45:11 +0000</pubDate>
      <link>https://dev.to/bigachiever/the-same-rust-gave-two-different-answers-and-neither-matched-javascript-j03</link>
      <guid>https://dev.to/bigachiever/the-same-rust-gave-two-different-answers-and-neither-matched-javascript-j03</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo Video
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Co3vZk1U0Sw"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I was porting &lt;a href="https://github.com/bgrins/TinyColor" rel="noopener noreferrer"&gt;TinyColor&lt;/a&gt;, a fifteen-year-old JavaScript colour library, to Rust. The rule I set myself was that the original test suite had to pass unmodified. Not a translation of it. The actual file, byte for byte, sha256-pinned, loading my Rust instead of the JavaScript.&lt;/p&gt;

&lt;p&gt;That works because upstream's test file loads the library on exactly one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tinycolor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./tinycolor.js&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;So I compiled the Rust to WebAssembly, put a shim at that path, and let the suite run. Forty-four of forty-five tests passed on the first try.&lt;/p&gt;

&lt;p&gt;The forty-fifth is what this post is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  One bit
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;readability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#111&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;expected&lt;/span&gt;  &lt;span class="mf"&gt;1.1121078324840545&lt;/span&gt;
&lt;span class="nx"&gt;got&lt;/span&gt;       &lt;span class="mf"&gt;1.1121078324840543&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two doubles that differ in the last place. That's the smallest possible disagreement between two f64 values, and there's no smaller one to find.&lt;/p&gt;

&lt;p&gt;It's also an unusual thing for a test to catch, because most test suites don't compare floats exactly. They use an epsilon. TinyColor's suite asserts the literal value to sixteen decimal places, which struck me as fragile when I first read it and turned out to be the reason I found any of this.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;readability&lt;/code&gt; is a WCAG contrast ratio, and it's built on relative luminance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getLuminance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;rgb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toRgb&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rgb&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;/&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.03928&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;rgb&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;/&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;12.92&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;rgb&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;/&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.055&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1.055&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// same for G and B&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.2126&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.7152&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;G&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.0722&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;B&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;Math.pow&lt;/code&gt; per channel. That's the whole surface area.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I assumed, and why I was wrong
&lt;/h2&gt;

&lt;p&gt;My first theory was rounding. My port has a whole module reproducing JavaScript's numeric quirks, because they bite constantly: &lt;code&gt;Math.round(-1.5)&lt;/code&gt; is &lt;code&gt;-1&lt;/code&gt; in JS and &lt;code&gt;-2&lt;/code&gt; in Rust, &lt;code&gt;parseFloat("50%")&lt;/code&gt; is &lt;code&gt;50&lt;/code&gt; rather than an error. I'd already been caught by both. So a rounding difference in luminance felt like more of the same, and I went looking for one.&lt;/p&gt;

&lt;p&gt;There wasn't one. The arithmetic was identical. I printed intermediate values at full precision and they matched all the way to the &lt;code&gt;pow&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;Then I ran the same code natively instead of through WebAssembly, mostly out of frustration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;native Rust        1.1121078324840545   ← matches JavaScript
the same Rust,
compiled to WASM   1.1121078324840543   ← doesn't
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same source file. Same input. Different answer.&lt;/p&gt;

&lt;p&gt;That reframed the whole thing. I'd been looking for a bug in my code, and my code was fine. The disagreement was underneath it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Math.pow is not one function
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Math.pow&lt;/code&gt; isn't defined to be bit-exact. IEEE 754 mandates correct rounding for add, subtract, multiply, divide and square root. Transcendental functions like &lt;code&gt;pow&lt;/code&gt; are explicitly left alone, because a correctly-rounded &lt;code&gt;pow&lt;/code&gt; is expensive and nobody wanted to require it.&lt;/p&gt;

&lt;p&gt;So every implementation makes its own accuracy-versus-speed trade, and they disagree in the last bit or two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;V8&lt;/strong&gt; uses its own port of fdlibm&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust on macOS&lt;/strong&gt; calls the system libm, which is Apple's&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust on wasm32&lt;/strong&gt; has no system libm, so it links a MUSL-derived implementation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three different &lt;code&gt;pow&lt;/code&gt;s. I was comparing two of them against a third.&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;getLuminance&lt;/code&gt; only ever sees integer channel values, I could check the whole input space rather than guess. All 256 of them, against V8:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;implementation&lt;/th&gt;
&lt;th&gt;matches V8 exactly&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rust std &lt;code&gt;powf&lt;/code&gt; (macOS libm)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;207 / 256&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust std &lt;code&gt;powf&lt;/code&gt; (wasm32, MUSL)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;225 / 256&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V8 (fdlibm)&lt;/td&gt;
&lt;td&gt;the reference&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Neither one matches. Not "close enough" — they each disagree with V8 on a few dozen of the 256 possible inputs, and they disagree on &lt;em&gt;different&lt;/em&gt; ones.&lt;/p&gt;

&lt;p&gt;That was the moment the bug stopped being annoying and got interesting. There was no libm I could pick that would be right. Switching to the MUSL version would have fixed 18 more cases and broken others.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix was in the question, not the answer
&lt;/h2&gt;

&lt;p&gt;I'd been asking "which &lt;code&gt;pow&lt;/code&gt; do I use." Wrong question.&lt;/p&gt;

&lt;p&gt;Look again at what feeds it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;rgb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toRgb&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// r, g, b are ROUNDED here&lt;/span&gt;
&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;rgb&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;/&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.055&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1.055&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;toRgb()&lt;/code&gt; rounds. So &lt;code&gt;rgb.r&lt;/code&gt; is always an integer from 0 to 255. &lt;code&gt;pow&lt;/code&gt; never sees anything else. Ever.&lt;/p&gt;

&lt;p&gt;That isn't a function call with a continuous domain. It's a lookup with 256 possible inputs, and I'd been treating it as maths because it was written as maths.&lt;/p&gt;

&lt;p&gt;So I generated the table. Ran V8 once over all 256 channel values, captured the exact f64 bit patterns, and emitted them as Rust:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;SRGB_LINEAR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="mi"&gt;0x0000000000000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 0 -&amp;gt; 0&lt;/span&gt;
    &lt;span class="mi"&gt;0x3f33e45677c176f7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 1 -&amp;gt; 0.0003035269835488375&lt;/span&gt;
    &lt;span class="mi"&gt;0x3f43e45677c176f7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 2 -&amp;gt; 0.000607053967097675&lt;/span&gt;
    &lt;span class="c1"&gt;// ... 253 more&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;srgb_linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="nf"&gt;.fract&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;..=&lt;/span&gt;&lt;span class="mf"&gt;255.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;channel&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="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// outside the domain: fall back to computing it&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="nn"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_bits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SRGB_LINEAR&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;usize&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;Bit patterns rather than decimal literals, so nothing depends on how a float parser rounds the text.&lt;/p&gt;

&lt;p&gt;The test passed. And so did the other 44, on both build targets, which is the part that matters: the port stopped being sensitive to what it was compiled for.&lt;/p&gt;

&lt;p&gt;It's also faster than calling &lt;code&gt;pow&lt;/code&gt;, though that was luck rather than design.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I keep thinking about
&lt;/h2&gt;

&lt;p&gt;The test that caught this looked like bad practice. Asserting a float to sixteen decimal places is exactly what you're told not to do, and if TinyColor had used an epsilon like a sensible person, my port would have shipped with a silent cross-platform inconsistency that no test anywhere would have found.&lt;/p&gt;

&lt;p&gt;I'd have had a colour library that computed contrast ratios differently depending on whether you ran it natively or in a browser. It would never have shown up as a bug. Just two systems quietly disagreeing about whether some text passes WCAG AA.&lt;/p&gt;

&lt;p&gt;I'm not going to argue everyone should assert exact floats. But the strict version of that test found something a tolerant version couldn't, and I've stopped assuming an epsilon is always the grown-up choice.&lt;/p&gt;

&lt;p&gt;The other lesson is cheaper: &lt;strong&gt;check the domain before optimising the function.&lt;/strong&gt; I spent a couple of hours on which &lt;code&gt;pow&lt;/code&gt; implementation to trust, and the answer was that the question didn't apply, because the input space was 256 values and had been the whole time.&lt;/p&gt;




&lt;p&gt;The port, and a decision log with 22 of these written up: &lt;strong&gt;&lt;a href="https://github.com/BigAchiever/tinycolor-rs" rel="noopener noreferrer"&gt;github.com/BigAchiever/tinycolor-rs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>javascript</category>
      <category>rust</category>
    </item>
    <item>
      <title>analogous(-1): how a default hid a heap-exhaustion bug for fifteen years</title>
      <dc:creator>Danish ali siddiqui</dc:creator>
      <pubDate>Mon, 17 Aug 2026 18:07:37 +0000</pubDate>
      <link>https://dev.to/bigachiever/analogous-1-how-a-default-hid-a-heap-exhaustion-bug-for-fifteen-years-380e</link>
      <guid>https://dev.to/bigachiever/analogous-1-how-a-default-hid-a-heap-exhaustion-bug-for-fifteen-years-380e</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/bgrins/TinyColor" rel="noopener noreferrer"&gt;TinyColor&lt;/a&gt; is a colour library for JavaScript. Parse a colour in basically any format, convert it, lighten it, generate a palette from it. It's fifteen years old, has 5.2k stars, and ships on npm as &lt;code&gt;tinycolor2&lt;/code&gt; underneath a lot of design tooling.&lt;/p&gt;

&lt;p&gt;I found this while porting it to Rust. I'd built a differential fuzzer for the port: run the original on V8 and my version side by side, same inputs, compare the outputs bit for bit. Thirty-one million comparisons, zero disagreements in the colour maths.&lt;/p&gt;

&lt;p&gt;It found nothing here though. Not because the code is fine, but because for these inputs the original doesn't return a wrong answer. It doesn't return at all, and you can't diff against a process that's died.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Co3vZk1U0Sw"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;analogous(results)&lt;/code&gt; and &lt;code&gt;monochromatic(results)&lt;/code&gt; loop forever on a negative or fractional count, and take the heap with them.&lt;/p&gt;

&lt;p&gt;Both decrement a counter and test it for truthiness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// analogous&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hsl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hsl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// monochromatic&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A counter that never lands exactly on &lt;code&gt;0&lt;/code&gt; never stops. From &lt;code&gt;1.5&lt;/code&gt; the sequence goes &lt;code&gt;0.5, -0.5, -1.5, -2.5…&lt;/code&gt;. From &lt;code&gt;-1&lt;/code&gt; it goes &lt;code&gt;-2, -3, -4…&lt;/code&gt;. Every pass pushes another colour object onto an array nobody will ever read.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;node &lt;span class="nt"&gt;--max-old-space-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;256 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"require('tinycolor2')('red').analogous(-1)"&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;&amp;lt;--- Last few GCs ---&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="go"&gt;134
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six cases, all exit 134: both functions at &lt;code&gt;-1&lt;/code&gt;, &lt;code&gt;1.5&lt;/code&gt; and &lt;code&gt;0.5&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's the part that makes it worth guarding rather than shrugging at. &lt;code&gt;polyad()&lt;/code&gt; is the third combination function, sitting in the same file, taking the same shape of argument. It already validates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;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;Argument to polyad must be a positive number&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;So the hazard is recognised in one of the three and not the other two. Any caller passing a user-supplied palette size into &lt;code&gt;analogous()&lt;/code&gt; or &lt;code&gt;monochromatic()&lt;/code&gt; has an unauthenticated way to kill the process. A number field in a colour picker. A value out of a config file. No exotic input needed, just a minus sign.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Issue:&lt;/strong&gt; &lt;a href="https://github.com/bgrins/TinyColor/issues/280" rel="noopener noreferrer"&gt;bgrins/TinyColor#280&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Pull request:&lt;/strong&gt; &lt;em&gt;(link)&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; function analogous(color, results, slices) {
   results = results || 6;
   slices = slices || 30;
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+  if (isNaN(results) || results &amp;lt;= 0) {
+    throw new Error("Argument to analogous must be a positive number");
+  }
+
&lt;/span&gt;   var hsl = tinycolor(color).toHsl();
   var part = 360 / slices;
   var ret = [tinycolor(color)];
&lt;span class="err"&gt;
&lt;/span&gt;   for (hsl.h = (hsl.h - ((part * results) &amp;gt;&amp;gt; 1) + 720) % 360; --results; ) {
     hsl.h = (hsl.h + part) % 360;
     ret.push(tinycolor(hsl));
   }
   return ret;
 }
&lt;span class="err"&gt;
&lt;/span&gt; function monochromatic(color, results) {
   results = results || 6;
&lt;span class="gi"&gt;+
+  if (isNaN(results) || results &amp;lt;= 0) {
+    throw new Error("Argument to monochromatic must be a positive number");
+  }
+
&lt;/span&gt;   var hsv = tinycolor(color).toHsv();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tests mirroring the ones &lt;code&gt;polyad&lt;/code&gt; already has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;assertThrows&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;tinycolor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;red&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;analogous&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;assertThrows&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;tinycolor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;red&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;analogous&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;assertThrows&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;tinycolor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;red&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;monochromatic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;assertThrows&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;tinycolor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;red&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;monochromatic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&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;
  
  
  My Improvements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why nobody hit this in fifteen years
&lt;/h3&gt;

&lt;p&gt;This is the bit I found genuinely interesting, and it's why I nearly dismissed my own bug report.&lt;/p&gt;

&lt;p&gt;My first three probes came back clean. &lt;code&gt;analogous(0)&lt;/code&gt; returns six colours. &lt;code&gt;analogous(null)&lt;/code&gt; returns six. &lt;code&gt;analogous(NaN)&lt;/code&gt; returns six. I assumed I'd fat-fingered the argument and moved on, and only came back to it an hour later because the shape of "some counts are fine" bothered me.&lt;/p&gt;

&lt;p&gt;The answer is on the first line of the function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a default, not a guard. But it happens to catch almost everything a developer would think to try:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;you pass&lt;/th&gt;
&lt;th&gt;truthy?&lt;/th&gt;
&lt;th&gt;loop actually gets&lt;/th&gt;
&lt;th&gt;outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;code&gt;6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;code&gt;6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;code&gt;6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NaN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;code&gt;6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;code&gt;6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;heap death&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;heap death&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;heap death&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every falsy value gets swapped for &lt;code&gt;6&lt;/code&gt; before the loop ever sees it. The default is accidentally guarding the entire class of input you'd reach for first.&lt;/p&gt;

&lt;p&gt;So the function is safe for every value you'd casually try and unsafe for the ones you wouldn't. That's a nasty shape for a bug: it hides from exactly the person looking for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Checking the blast radius
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;analogous&lt;/code&gt; takes a second argument, &lt;code&gt;slices&lt;/code&gt;, guarded the same lazy way with &lt;code&gt;slices || 30&lt;/code&gt;. I assumed it had the same hole and it doesn't. &lt;code&gt;slices&lt;/code&gt; only feeds &lt;code&gt;part = 360 / slices&lt;/code&gt;, so a bad value gives you a wrong hue or a &lt;code&gt;NaN&lt;/code&gt; channel, but the loop is driven entirely by &lt;code&gt;results&lt;/code&gt;. &lt;code&gt;slices&lt;/code&gt; at &lt;code&gt;-1&lt;/code&gt;, &lt;code&gt;1.5&lt;/code&gt; and &lt;code&gt;NaN&lt;/code&gt; all return six colours and terminate.&lt;/p&gt;

&lt;p&gt;Worth checking rather than assuming, and it let me narrow the report instead of overclaiming.&lt;/p&gt;

&lt;h3&gt;
  
  
  Guard, or coerce?
&lt;/h3&gt;

&lt;p&gt;Two ways to fix it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Guard and throw, the way &lt;code&gt;polyad()&lt;/code&gt; does.&lt;/li&gt;
&lt;li&gt;Coerce with &lt;code&gt;results = Math.max(1, Math.floor(results))&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Coercion is friendlier to anyone currently passing &lt;code&gt;1.5&lt;/code&gt; and getting something back. But it silently changes their result and buries the mistake, and I don't think a colour library should be quietly reinterpreting your arguments. The guard also matches what the same file already does for the same input, so a maintainer can merge it without deciding a new policy. Consistency inside a codebase beats my personal taste in error handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying before filing
&lt;/h3&gt;

&lt;p&gt;I checked against current &lt;code&gt;main&lt;/code&gt;, not just the copy I'd vendored: byte-identical, both loops still there, all six cases still exit 134. Then I read a hundred existing issues looking for a duplicate. Closest are &lt;a href="https://github.com/bgrins/TinyColor/issues/116" rel="noopener noreferrer"&gt;#116&lt;/a&gt; and &lt;a href="https://github.com/bgrins/TinyColor/issues/204" rel="noopener noreferrer"&gt;#204&lt;/a&gt;, neither of which is this.&lt;/p&gt;

&lt;p&gt;The PR is open rather than merged, and I'll be straight about why: TinyColor hasn't merged anything since February 2023.&lt;/p&gt;

&lt;h3&gt;
  
  
  What my port does instead
&lt;/h3&gt;

&lt;p&gt;It returns a finite list. &lt;code&gt;analogous(-1)&lt;/code&gt; gives one entry, &lt;code&gt;1.5&lt;/code&gt; gives two.&lt;/p&gt;

&lt;p&gt;That's the single deliberate behavioural divergence in an otherwise bit-exact port, documented as D-022 in &lt;a href="https://github.com/BigAchiever/tinycolor-rs/blob/master/DECISIONS.md" rel="noopener noreferrer"&gt;the decision log&lt;/a&gt;. Reproducing the original faithfully would've meant shipping a denial of service on purpose. And there's nothing to reproduce anyway: the original doesn't return something wrong here, it stops existing.&lt;/p&gt;




&lt;p&gt;The port, the decision log, and the fuzzer that sent me looking: &lt;strong&gt;&lt;a href="https://github.com/BigAchiever/tinycolor-rs" rel="noopener noreferrer"&gt;github.com/BigAchiever/tinycolor-rs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How do you form a group nobody can admit they're in?</title>
      <dc:creator>Danish ali siddiqui</dc:creator>
      <pubDate>Sun, 16 Aug 2026 21:37:08 +0000</pubDate>
      <link>https://dev.to/bigachiever/how-do-you-form-a-group-nobody-can-admit-theyre-in-2mjl</link>
      <guid>https://dev.to/bigachiever/how-do-you-form-a-group-nobody-can-admit-theyre-in-2mjl</guid>
      <description>&lt;p&gt;Arun invoiced a design agency ₹1,20,000 in January. It's August.&lt;/p&gt;

&lt;p&gt;He is in a 4,000-member designers' Discord. He could post the agency's name right now and warn everyone. He won't, and you already know why: the freelancer who publicly names a client stops getting briefs. He'd pay for it alone, and everyone else would benefit.&lt;/p&gt;

&lt;p&gt;Here's the part that makes it a &lt;em&gt;systems&lt;/em&gt; problem rather than a sad story. &lt;strong&gt;Three other people in that same Discord are owed money by that same agency.&lt;/strong&gt; None of them knows. Each one is running the same arithmetic Arun is, arriving at the same answer, and saying nothing.&lt;/p&gt;

&lt;p&gt;Four people who together have real leverage. Individually, none of them can afford the first move.&lt;/p&gt;

&lt;p&gt;I built an agent for this over a hackathon weekend. The interesting part wasn't the AI. It was that &lt;strong&gt;every obvious solution destroys the thing you're trying to protect.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The obvious version, and why it dies
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"Just make a private channel for victims of bad clients."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To join, you say who burned you. Now the group knows. One screenshot and Arun is on a list.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Okay, collect reports centrally and only reveal at a threshold."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Better. This is roughly how &lt;a href="https://www.projectcallisto.org/" rel="noopener noreferrer"&gt;Callisto Vault&lt;/a&gt; handles assault reports, and it's a good pattern. But it reveals the group &lt;strong&gt;to its own members&lt;/strong&gt; at the threshold. Four people now know each other's names and amounts. Four times the leak surface, arriving exactly when things get tense.&lt;/p&gt;

&lt;p&gt;The requirement I ended up with was stricter than I expected:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nobody is exposed. Not to the channel, not to the accused, and &lt;strong&gt;not to each other&lt;/strong&gt; — not even after it works.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which sounds impossible, because how do four people coordinate if they can't know who they are?&lt;/p&gt;

&lt;p&gt;They don't. The agent knows. Nobody else does.&lt;/p&gt;




&lt;h2&gt;
  
  
  The public board that can't name the client
&lt;/h2&gt;

&lt;p&gt;Here's what actually appears in the Discord:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PICKET · matter #1
&amp;gt; "invoiced in January, still chasing in August"
₹50k–2L · 180d+ overdue

🟩⬜⬜⬜  1/4 joined
[ JOIN ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One sentence Arun wrote himself. An amount &lt;strong&gt;band&lt;/strong&gt;, not his figure. A counter. &lt;strong&gt;The agency's name is nowhere on it, in any state, ever.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the whole trick, and it's easy to misread as "we're being cautious". It isn't caution. The accused agency might be &lt;em&gt;in that Discord&lt;/em&gt;. The board has to be safe to read for the very person it's about.&lt;/p&gt;

&lt;p&gt;So the board can't tell you who it is. Which raises the obvious problem: &lt;strong&gt;how does a real fellow-victim recognise it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They don't recognise it. They &lt;em&gt;guess&lt;/em&gt;. And then they prove it.&lt;/p&gt;

&lt;p&gt;You tap JOIN, and the agent DMs you the intake steps: email us, and &lt;strong&gt;name the client yourself&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's the admission test. Not a password, not an invite code. &lt;strong&gt;It's the one thing only someone in the same situation would know.&lt;/strong&gt; Get it right and your pip fills. Get it wrong and your claim is held, sealed, and nobody is told anything. Crucially you don't learn whether you guessed right, so nobody can use the system to fish for the name.&lt;/p&gt;

&lt;h2&gt;
  
  
  The column I refused to write
&lt;/h2&gt;

&lt;p&gt;When you tap JOIN, the bot knows your Discord identity. When you email, it knows your address. Storing &lt;code&gt;discord_id → claimant_id&lt;/code&gt; would be one line and would make a dozen features easier.&lt;/p&gt;

&lt;p&gt;That line is the entire attack surface. One leaked table and every claimant is public.&lt;/p&gt;

&lt;p&gt;So the bot never says who tapped, and the join reply is written to be &lt;strong&gt;impossible to personalise&lt;/strong&gt;:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_join_instruction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MatterPublic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;intake_address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Deliberately does NOT name who tapped: a Discord button tap is invisible
&lt;/span&gt;    &lt;span class="c1"&gt;# to other members, and naming the tapper here would be the exact
&lt;/span&gt;    &lt;span class="c1"&gt;# first-mover exposure this system exists to prevent.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a side effect I like: &lt;strong&gt;you can tap JOIN twice and the bot won't remember you.&lt;/strong&gt; It looks like a bug. It's the guarantee, visible. To recognise you, it would have to have stored the link it promised not to store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Negotiating for people you never introduce
&lt;/h2&gt;

&lt;p&gt;At four matching claims, one email goes to the agency: four documented claims, the combined total, the age of the oldest. No names.&lt;/p&gt;

&lt;p&gt;The agency offers 60%. And this is where the design either holds or collapses.&lt;/p&gt;

&lt;p&gt;The tempting move is a group thread: &lt;em&gt;"they've offered 60%, what do we all think?"&lt;/em&gt; Convenient, and it hands everyone's identity to everyone else at the worst possible moment.&lt;/p&gt;

&lt;p&gt;Instead each claimant gets their own sealed email:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For your claim it means: &lt;strong&gt;₹72,000&lt;/strong&gt; (60% of your ₹1,20,000)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Their slice. Their decision. Three accept, one refuses. Three claims settle, one stays open independently. &lt;strong&gt;Nobody was outvoted, because there was never a vote.&lt;/strong&gt; The client is told "3 of 4 accepted", not which three.&lt;/p&gt;

&lt;p&gt;They still don't know each other. That's not a limitation of the build. That's the product.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two problems I didn't see coming
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Who decides admission?&lt;/strong&gt; Matching &lt;em&gt;"that media agency in Andheri, the blueprint one"&lt;/em&gt; to &lt;code&gt;Blueprint Media Pvt. Ltd.&lt;/code&gt; is exactly what an LLM is good at. So my first version asked the model: &lt;em&gt;does this claim belong?&lt;/em&gt; That makes a hallucination-prone component the doorman for a group of people trying to stay hidden.&lt;/p&gt;

&lt;p&gt;The fix is one word. The model is never asked &lt;em&gt;"should this be let in?"&lt;/em&gt;, only &lt;em&gt;"which of these existing things does it look like, if any?"&lt;/em&gt; Admission is then &lt;code&gt;if key != matter.canonical_key&lt;/code&gt;. I tested it with &lt;strong&gt;"Blueprint Constructions"&lt;/strong&gt;. Same first word, different company. Returns &lt;code&gt;None&lt;/code&gt;. Not because the prompt asks nicely, but because the comparison happens in Python.&lt;/p&gt;

&lt;p&gt;Same rule everywhere: the model can read a messy claim, read the attached invoice, classify a reply. It cannot let anyone in, move any state, or put a number in an outgoing email. When the client writes &lt;em&gt;"sixty percent"&lt;/em&gt;, the &lt;code&gt;60&lt;/code&gt; is only used because code checks that number literally appears in their own sentence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why should the agency believe any of this?&lt;/strong&gt; Their reasonable reply to "four people are owed money" is: prove it isn't one person with four inboxes and a good afternoon.&lt;/p&gt;

&lt;p&gt;I reached for a hash chain first. It proves nothing. I control the whole thing and can rebuild it in a second with any timestamps I want. It's a diary that catches you erasing a page, written by the person holding the pen.&lt;/p&gt;

&lt;p&gt;What saved it was already there: the comms gateway stamps every event with a monotonic sequence number that &lt;strong&gt;I don't issue and can't move&lt;/strong&gt;. So every ledger row stores the seq that caused it, and a verifier re-fetches them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[3] independence: last claim admitted at seq 21392,
    first client REPLY at seq 21420
    -&amp;gt; claims PRECEDE the client's first reply ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The four claims existed before the agency was ever contacted, and that ordering is enforced by infrastructure I don't own. They don't have to trust me. They have to trust that I can't rewrite someone else's log.&lt;/p&gt;

&lt;p&gt;If your system makes a claim about time or order, anchor it to a counter you don't control.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three things that bit me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A card body that vanished.&lt;/strong&gt; Posting a Discord card with a &lt;code&gt;body&lt;/code&gt; rendered the title and button and silently dropped the body. Because edits are text-only, the board looked correct from its &lt;em&gt;second&lt;/em&gt; update onward — so the bug only appeared on a tile's very first render. Which is the exact frame a demo video opens on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A poller that ate its own history.&lt;/strong&gt; My event cursor defaulted to &lt;code&gt;0&lt;/code&gt;. Delete the local DB, restart, and the agent replays every event the project has ever seen, reopening closed cases and re-posting their public messages. If your poller has a cursor, decide what a &lt;em&gt;missing&lt;/em&gt; cursor means, and make it mean "now".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Being right and not recognising it.&lt;/strong&gt; I added a vision model that reads the attached invoice and rejects a claim if the document disagrees. Then a test claim said ₹95,000 while its invoice said ₹1,20,000, and my system refused it. I debugged the "bug" for a minute before realising it was working.&lt;/p&gt;




&lt;h2&gt;
  
  
  The thing worth taking
&lt;/h2&gt;

&lt;p&gt;The hard constraint turned out to be generative rather than limiting. &lt;em&gt;Nobody may be exposed to anybody&lt;/em&gt; forced the amount bands, the name-it-yourself admission test, the column I didn't write, and the sealed per-person negotiation. Every one of those is a better design than what I'd have built without the constraint.&lt;/p&gt;

&lt;p&gt;If you're building something where people have to cooperate but can't afford to be seen cooperating — whistleblowing, wage disputes, deposit disputes, bad-landlord registries, the shape is reusable: &lt;strong&gt;a public counter, a private admission test only a real participant can pass, and an agent that acts on the group's behalf without ever assembling it.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;What would you have done differently? I'm most unsure about the guess-and-prove admission test — it works, but it assumes enough density that guessing is worth someone's time.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>architecture</category>
      <category>agents</category>
    </item>
    <item>
      <title>GitHub doesn't retry webhooks. So I gave my tunnel a database.</title>
      <dc:creator>Danish ali siddiqui</dc:creator>
      <pubDate>Sun, 09 Aug 2026 19:42:34 +0000</pubDate>
      <link>https://dev.to/bigachiever/github-doesnt-retry-webhooks-so-i-gave-my-tunnel-a-database-4aa0</link>
      <guid>https://dev.to/bigachiever/github-doesnt-retry-webhooks-so-i-gave-my-tunnel-a-database-4aa0</guid>
      <description>&lt;p&gt;&lt;em&gt;Built solo over a weekend for the WeMakeDevs × Zerops challenge. There's a live gateway you can poke at, and a section near the end about what it can't do.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I was testing a GitHub integration with a tunnel open to my laptop. I shut the lid to go get coffee. When I came back, the push event was gone.&lt;/p&gt;

&lt;p&gt;Not delayed. Not sitting in a queue somewhere. Gone.&lt;/p&gt;

&lt;p&gt;I figured I'd misconfigured something. I hadn't. From GitHub's own docs:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"GitHub does not automatically redeliver failed deliveries."&lt;br&gt;
— &lt;a href="https://docs.github.com/en/webhooks/using-webhooks/handling-failed-webhook-deliveries" rel="noopener noreferrer"&gt;docs.github.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One failed delivery and it's your problem, unless you open the UI and redeliver by hand.&lt;/p&gt;

&lt;p&gt;I checked other providers, expecting GitHub to be unusual. It isn't:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;What happens when your tunnel is down&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No automatic retries at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Stripe&lt;/strong&gt; (test mode)&lt;/td&gt;
&lt;td&gt;3 attempts over a few hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Shopify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8 attempts over 4 hours, then deletes your webhook subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the last one again. Shopify doesn't just give up on the event. It unsubscribes your app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody tells you about tunnels
&lt;/h2&gt;

&lt;p&gt;Here's what I hadn't thought about properly. A tunnel turns your laptop into production infrastructure for somebody else's system.&lt;/p&gt;

&lt;p&gt;Senders don't wait for you. A customer checks out at 2am. CI finishes after you've left for lunch. Your wifi drops for thirty seconds on a train.&lt;/p&gt;

&lt;p&gt;When that happens the sender gets a &lt;code&gt;502&lt;/code&gt;. It burns one of the three or four attempts it was ever going to make, and then the event is gone.&lt;/p&gt;

&lt;p&gt;Someone is about to say "ngrok already has replay." It does, and it's good. But ngrok can only replay a request it already saw, which means you were connected when it arrived. The ones that hurt are the ones that arrive when you aren't.&lt;/p&gt;

&lt;p&gt;That's structural, not a missing feature:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A tunnel is a pipe, not a mailbox. If nothing is listening at your end, the request has nowhere to go.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I built the mailbox. It's called Doorbell, and it's a tunnel with a database in the path.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can break it right now
&lt;/h2&gt;

&lt;p&gt;You have to take my word for everything else in this post. This part you can check yourself.&lt;/p&gt;

&lt;p&gt;I'm running a public gateway — the always-on server that holds tunnels open. It has a tunnel named &lt;code&gt;shop&lt;/code&gt; with &lt;strong&gt;nothing connected to it&lt;/strong&gt;, which is the same state as your laptop with the lid shut.&lt;/p&gt;

&lt;p&gt;On a phone? Just open &lt;a href="https://gw-2ad0-3000.prg1.zerops.app/" rel="noopener noreferrer"&gt;the live page&lt;/a&gt; and watch the &lt;code&gt;held right now&lt;/code&gt; counter. At a keyboard, send it a webhook:&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="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://gw-2ad0-3000.prg1.zerops.app/t/shop/hook &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"n":1}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="m"&gt;202&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;202 Accepted&lt;/code&gt;, where you'd expect a &lt;code&gt;502&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is no laptop on the other end. Your request is now a row in Postgres, and it gets delivered the moment someone connects. Reload that live page and the counter is one higher. That's yours.&lt;/p&gt;

&lt;p&gt;Now try a name nobody reserved:&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="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://gw-2ad0-3000.prg1.zerops.app/t/zzrandom99/hook &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"n":1}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="m"&gt;404&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;404&lt;/code&gt; matters as much as the &lt;code&gt;202&lt;/code&gt;. Only &lt;em&gt;reserved&lt;/em&gt; names get held. If any name worked, anyone could fill my database by inventing URLs, and "we store everything" would just mean "free disk for strangers."&lt;/p&gt;

&lt;p&gt;And here's what you get back when you reconnect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;doorbell &lt;span class="nt"&gt;-name&lt;/span&gt; demo 3000
&lt;span class="go"&gt;  https://gw-2ad0-3000.prg1.zerops.app/t/demo/
  → forwarding to 127.0.0.1:3000

  ▲ requests held while you were away are arriving now
  ✓ 11:11:23 POST   /hooks/github    200  held 4s
  ✓ 11:11:23 POST   /hooks/github    200  held 2s
  ✓ 11:11:24 POST   /hooks/github    200  held &amp;lt;1s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oldest first, each showing how long it waited.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you get excited: is this for you?
&lt;/h2&gt;

&lt;p&gt;One thing you should know 30 seconds in rather than 10 minutes in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A held webhook will fail a signature check.&lt;/strong&gt; GitHub and Stripe sign each delivery so your app can prove it really came from them. Doorbell strips anything that looks like a signing header before writing the row, so the signature is gone by the time the request reaches you.&lt;/p&gt;

&lt;p&gt;That's a deliberate trade. I'd rather hand you a request whose signature no longer verifies than keep someone's live signing secret sitting in my database. Held requests carry an &lt;code&gt;X-Doorbell-Replay&lt;/code&gt; header, so you can skip verification when you see it.&lt;/p&gt;

&lt;p&gt;If your handler verifies signatures and you can't branch on that header, this tool will annoy you. Better you know now.&lt;/p&gt;

&lt;p&gt;Two more things, and then the good part:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You run this yourself.&lt;/strong&gt; The gateway above is mine, for trying it. For real use you deploy your own from one YAML file, and nobody else's servers sit in the path of your traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a development tunnel.&lt;/strong&gt; Don't put it in front of production traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;202&lt;/code&gt; actually means
&lt;/h2&gt;

&lt;p&gt;This is where a lot of "reliable webhook" tooling quietly lies to you.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;202 Accepted&lt;/code&gt; does not mean your app handled it. It means: I have taken responsibility for this request. The row is on disk before the sender gets any answer at all.&lt;/p&gt;

&lt;p&gt;Two things make that promise real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ordering.&lt;/strong&gt; Each tunnel's queue drains oldest first, one at a time. That's the only order your handler can make sense of. If the delete arrives before the create, your code does the wrong thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No duplicates.&lt;/strong&gt; If you reconnect twice quickly, two drains could race for the same stored request and send the same webhook twice. A duplicate payment notification is somebody's real problem.&lt;/p&gt;

&lt;p&gt;So the database decides, not the application. Claiming a request and marking it taken happen in a single SQL statement, so only one drain can ever win it. There's a test that runs eight of them at once to prove it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;won&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%d of %d claimers won the same row; every extra winner is a webhook delivered twice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;won&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;racers&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 whole thing rests on one function
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;doorbell 3000&lt;/code&gt;, the CLI opens a single outbound connection to the gateway and holds it open. Nothing listens on your machine. Nothing gets opened on your router.&lt;/p&gt;

&lt;p&gt;It's like calling a support line. They can't call you. But while you're on the line, they can talk to you.&lt;/p&gt;

&lt;p&gt;Each incoming request becomes its own stream on that one connection, using &lt;a href="https://github.com/hashicorp/yamux" rel="noopener noreferrer"&gt;yamux&lt;/a&gt; to run many conversations down a single socket. The proxying itself is just Go's standard library, with one substitution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;Transport&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transport&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// The whole trick: instead of dialling a network address, open a&lt;/span&gt;
    &lt;span class="c"&gt;// new multiplexed stream on the socket the laptop already holds open.&lt;/span&gt;
    &lt;span class="n"&gt;DialContext&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap &lt;em&gt;dial an address&lt;/em&gt; for &lt;em&gt;open a stream&lt;/em&gt; and you're done. Chunked bodies, keep-alive and WebSocket upgrades all still work, because &lt;code&gt;httputil.ReverseProxy&lt;/code&gt; is doing the actual HTTP.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug I shipped, and how I found it
&lt;/h2&gt;

&lt;p&gt;Doorbell has a dashboard. It shows the request and response bodies it captured, and it can re-send any stored request to whoever's laptop is connected. Both were behind a token. Good.&lt;/p&gt;

&lt;p&gt;The problem: that same token also guarded the port the CLI dials to open a tunnel. One check was answering two completely different questions. &lt;em&gt;May you open a tunnel?&lt;/em&gt; and &lt;em&gt;may you read everyone's payloads?&lt;/em&gt; ran through the same line of code.&lt;/p&gt;

&lt;p&gt;So anyone I let tunnel through my gateway could also read every captured body on it, and re-send any of them.&lt;/p&gt;

&lt;p&gt;The file's own doc comment said the opposite:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Tunnels themselves are not gated by this — only the surfaces that expose captured request and response bodies."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The comment described the intent. The code did something else. Nobody catches that in review, because both call sites look correct on their own.&lt;/p&gt;

&lt;p&gt;The fix was to split them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// tokenOK gates the control port: may this client open a tunnel at all?&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;tokenOK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;presented&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clientToken&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;subtle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConstantTimeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// adminTokenOK gates the dashboard. Kept separate on purpose.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;adminTokenOK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;presented&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;subtle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConstantTimeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I nearly made it worse.&lt;/p&gt;

&lt;p&gt;The dashboard also accepts &lt;code&gt;?token=&lt;/code&gt; in the URL, and that path called &lt;code&gt;tokenOK&lt;/code&gt; too. Look at what &lt;code&gt;tokenOK&lt;/code&gt; does when no client token is set: it returns &lt;code&gt;true&lt;/code&gt; for anything. My public gateway has no client token, on purpose, so anyone can tunnel. Which means &lt;code&gt;?token=literally-anything&lt;/code&gt; would have opened the dashboard to the first person who tried it.&lt;/p&gt;

&lt;p&gt;I caught it before shipping. There's now a test whose only job is to fail if it comes back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestGuessedQueryTokenCannotUnlockTheDashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gatewayWithTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"operator-secret"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;reachedOperator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/dashboard?token=anything"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"?token=anything unlocked the dashboard on a public gateway"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two privileges, two tokens. Opening a tunnel is a small thing to be allowed to do. Reading what went through it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this can't run on most of the internet
&lt;/h2&gt;

&lt;p&gt;Holding that connection open gives you two requirements you can't negotiate away:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A raw TCP port on the gateway.&lt;/strong&gt; Plain TCP, not HTTP, because the client speaks its own protocol on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A process that never sleeps.&lt;/strong&gt; It sits there holding the socket for hours.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those two rule out every serverless platform, by definition. A function stops existing the moment it returns a response, so nobody is left holding the line. And a platform that handles HTTP for you can't give you a port that isn't HTTP.&lt;/p&gt;

&lt;p&gt;I'll be straight with you, because a Fly.io user is already typing: Fly.io gives you a raw TCP port too. So does Railway. Zerops isn't magic here.&lt;/p&gt;

&lt;p&gt;But the port was never the hard part. The hard part is the database. It's storing captured webhook bodies, so it must never face the internet, and I didn't want to spend a hackathon weekend wiring up a private network by hand to make that true.&lt;/p&gt;

&lt;p&gt;Paste one YAML file into Zerops and about ninety seconds later you have a gateway, a Postgres, a Valkey and a private network between them, in your own account. (Valkey is a Redis fork. It tracks which gateway container currently owns which tunnel, so this still works when there's more than one.)&lt;/p&gt;

&lt;p&gt;One assumption sat underneath all of it: that a raw TCP port on Zerops is genuinely reachable from the public internet. The docs said ports 10–65435 were available. But &lt;strong&gt;documented isn't measured&lt;/strong&gt;, and if that line was wrong the whole project was dead. So before writing any of the gateway, I deployed a thirty-line echo server on port 7000 and dialled it from my laptop. It answered. That throwaway test is still sitting in my account next to the real project.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One honest note: that YAML can't open the raw port for you. Zerops' import format has no field for it, so it's still one click in the dashboard afterwards.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What still doesn't work
&lt;/h2&gt;

&lt;p&gt;The signature thing above is the big one. Two more:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The CLI needs IPv6 to reach my public gateway.&lt;/strong&gt; Webhooks don't — every &lt;code&gt;curl&lt;/code&gt; in this post runs over ordinary IPv4, because that's HTTP going through Zerops' shared load balancer. A raw TCP port can't use that path, and Zerops won't publish raw ports on a shared IPv4. That leaves IPv6 or a paid dedicated IPv4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom domains are written and unit-tested but have never run end to end.&lt;/strong&gt; Proving that needs a real domain and a wildcard certificate, which I didn't have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things you're about to ask
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How long are requests held?&lt;/strong&gt; Until you reconnect. The cap is 200 per tunnel and 1 MB per body. Past 200 the oldest undelivered ones get dropped, because without a cap the table becomes a way to fill someone's disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the ordering per tunnel or global?&lt;/strong&gt; Per tunnel. Each queue drains on its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Doesn't that eight-way race contradict the ordering?&lt;/strong&gt; No. That's the test, hammering one row on purpose to prove only one claimer wins. The real drain loop is sequential.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if two people connect with the same tunnel name?&lt;/strong&gt; A reservation belongs to whoever made it. The second one gets &lt;code&gt;the name "shop" is reserved by someone else&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The two curls at the top take about thirty seconds and need nothing installed. For your own tunnel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/BigAchiever/doorbell/cmd/doorbell@latest
doorbell &lt;span class="nt"&gt;-name&lt;/span&gt; pick-a-name 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;🎥 &lt;a href="https://youtu.be/KIYEptODPeo" rel="noopener noreferrer"&gt;4-minute demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 &lt;a href="https://gw-2ad0-3000.prg1.zerops.app/" rel="noopener noreferrer"&gt;Live gateway&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/BigAchiever/doorbell" rel="noopener noreferrer"&gt;Source&lt;/a&gt; (MIT)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CI fails the build if the Postgres and Valkey test suites &lt;em&gt;skip&lt;/em&gt;, because a green run that quietly tested nothing is worse than a red one. That's the check I'm proudest of.&lt;/p&gt;

&lt;p&gt;If you break it, I'd genuinely like to know how.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>go</category>
      <category>webhook</category>
      <category>devops</category>
    </item>
    <item>
      <title>WhatsApp Knowledge Miner: Transforming Group Chats into a Searchable Knowledge Base</title>
      <dc:creator>Danish ali siddiqui</dc:creator>
      <pubDate>Sun, 07 Sep 2025 10:12:36 +0000</pubDate>
      <link>https://dev.to/bigachiever/whatsapp-knowledge-miner-transforming-group-chats-into-a-searchable-knowledge-base-4e4d</link>
      <guid>https://dev.to/bigachiever/whatsapp-knowledge-miner-transforming-group-chats-into-a-searchable-knowledge-base-4e4d</guid>
      <description>&lt;p&gt;In today's fast-paced digital communication landscape, WhatsApp has become a central hub for group discussions, brainstorming sessions, and knowledge sharing. However, the transient nature of these conversations often leads to valuable insights being lost amidst the chat clutter. Enter the WhatsApp Knowledge Miner, a tool designed to extract, organize, and transform your WhatsApp group chats into a structured, searchable knowledge base.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;The WhatsApp Knowledge Miner is an open-source application that bridges the gap between unstructured WhatsApp group chats and structured knowledge management. By leveraging the WhatsApp Business API and advanced natural language processing (NLP) techniques, the application captures messages, analyzes their content, and converts them into question-answer pairs that can be easily searched and referenced.&lt;/p&gt;

&lt;p&gt;🧩 Core Components&lt;br&gt;
&lt;strong&gt;1. WhatsApp Bridge&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The WhatsApp Bridge serves as the interface between your WhatsApp account and the application. It connects to your WhatsApp account and securely stores messages in a local SQLite database.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd whatsapp-bridge
go run main.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upon running, scan the QR code displayed to log in (similar to WhatsApp Web). The application will then begin capturing messages from your WhatsApp groups.&lt;br&gt;
**&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Message Capture and Storage**&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Captured messages are stored in a local SQLite database, ensuring that all data remains secure and accessible. This local storage approach facilitates easy retrieval and analysis of messages without relying on external servers.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;3. Message Processing Server&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
The message processing server analyzes the captured messages to identify key information, such as questions and answers. Using NLP techniques, it extracts relevant content and organizes it into structured data.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;4. Streamlit Web Interface&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
The Streamlit web interface provides a user-friendly platform to interact with the processed data. Users can search for specific questions and view the corresponding answers, making it easy to access valuable information from past discussions.&lt;/p&gt;
&lt;h2&gt;
  
  
  🔧 Technical Highlights
&lt;/h2&gt;

&lt;p&gt;Go (Golang): Used for building the WhatsApp Bridge, ensuring efficient and concurrent message capturing.&lt;/p&gt;

&lt;p&gt;SQLite: Provides a lightweight and reliable local database for storing messages.&lt;/p&gt;

&lt;p&gt;Natural Language Processing: Employed to analyze and extract meaningful information from unstructured messages.&lt;/p&gt;

&lt;p&gt;Streamlit: Facilitates the creation of an interactive web interface for users to search and view Q&amp;amp;A pairs.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧪 Installation and Usage
&lt;/h2&gt;

&lt;p&gt;Clone the Repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/BigAchiever/whatsapp-knowledge-miner.git
cd whatsapp-knowledge-miner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Set Up the WhatsApp Bridge:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd whatsapp-bridge
go run main.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scan the QR code to log in to your WhatsApp account.&lt;/p&gt;

&lt;p&gt;Run the Message Processing Server:&lt;/p&gt;

&lt;p&gt;Navigate to the whatsapp-mcp-server directory and start the server to begin processing captured messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Launch the Streamlit Web Interface:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the streamlit-app directory, run the following command to start the web interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;streamlit run app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Access the interface through your web browser to search and view Q&amp;amp;A pairs.&lt;/p&gt;

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

&lt;p&gt;Seamless Integration: Effortlessly connects to your WhatsApp account to capture messages.&lt;/p&gt;

&lt;p&gt;Automated Q&amp;amp;A Extraction: Utilizes NLP to automatically generate question-answer pairs from conversations.&lt;/p&gt;

&lt;p&gt;Local Data Storage: Ensures data privacy by storing messages in a local SQLite database.&lt;/p&gt;

&lt;p&gt;Interactive Search Interface: Provides a user-friendly web interface for searching and viewing Q&amp;amp;A pairs.&lt;/p&gt;

&lt;p&gt;Open Source: Freely available for modification and enhancement.&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 Use Cases
&lt;/h2&gt;

&lt;p&gt;Team Collaboration: Easily reference past discussions and decisions made in group chats.&lt;/p&gt;

&lt;p&gt;Knowledge Management: Organize and access valuable information shared within WhatsApp groups.&lt;/p&gt;

&lt;p&gt;Educational Purposes: Create a repository of questions and answers for study groups or tutoring sessions.&lt;/p&gt;

&lt;p&gt;Customer Support: Analyze customer inquiries and responses to improve support services.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Explore the Project
&lt;/h2&gt;

&lt;p&gt;To delve deeper into the WhatsApp Knowledge Miner, explore the &lt;a href="https://github.com/BigAchiever/whatsapp-knowledge-miner" rel="noopener noreferrer"&gt;Github Repository&lt;/a&gt;&lt;br&gt;
. Here, you'll find comprehensive documentation, installation instructions, and the complete source code.&lt;br&gt;
For more checkout my &lt;a href="https://www.linkedin.com/feed/update/urn:li:activity:7369301043966377993/" rel="noopener noreferrer"&gt;LinkedIn &lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The WhatsApp Knowledge Miner exemplifies the power of combining messaging platforms with advanced data processing techniques. By transforming unstructured group chats into a searchable knowledge base, it enhances collaboration, preserves valuable information, and streamlines access to past discussions. Whether for personal use, team collaboration, or educational purposes, this tool offers a practical solution to manage and utilize the wealth of information shared within WhatsApp groups.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
