<?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>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>
