<?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: Taiwo Olugbenga</title>
    <description>The latest articles on DEV Community by Taiwo Olugbenga (@taiwo_olugbenga).</description>
    <link>https://dev.to/taiwo_olugbenga</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%2F4209092%2F864e9aeb-6c52-4a33-a3ac-00e43b3df026.png</url>
      <title>DEV Community: Taiwo Olugbenga</title>
      <link>https://dev.to/taiwo_olugbenga</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/taiwo_olugbenga"/>
    <language>en</language>
    <item>
      <title>Stop Fighting Ngrok Limits: Architecting a Secure Local Webhook Relay with Node.js &amp; WebSockets</title>
      <dc:creator>Taiwo Olugbenga</dc:creator>
      <pubDate>Mon, 26 Oct 2026 15:50:23 +0000</pubDate>
      <link>https://dev.to/taiwo_olugbenga/stop-fighting-ngrok-limits-architecting-a-secure-local-webhook-relay-with-nodejs-websockets-35kc</link>
      <guid>https://dev.to/taiwo_olugbenga/stop-fighting-ngrok-limits-architecting-a-secure-local-webhook-relay-with-nodejs-websockets-35kc</guid>
      <description>&lt;p&gt;If you build fintech, e-commerce, or SaaS platforms, you already know the pain of testing webhooks locally.&lt;/p&gt;

&lt;p&gt;You integrate Stripe, Paystack, or GitHub. You need your local API to catch incoming POST requests. You fire up a local tunneling tool like ngrok. It works perfectly—until your free session expires, your URL changes, and you have to update your payment provider's dashboard all over again.&lt;/p&gt;

&lt;p&gt;I got tired of the connection limits, the dropped sockets, and the constant URL rotation. I needed a persistent, team-friendly way to route cloud webhooks to localhost.&lt;/p&gt;

&lt;p&gt;So, I built and open-sourced local-webhook-relay—a lightweight CLI and cloud bridge architecture that uses Node.js and secure WebSockets to bypass tunneling limits entirely.&lt;/p&gt;

&lt;p&gt;Here is a deep dive into how I architected it, the security challenges involved, and how you can use it (or build your own).&lt;/p&gt;

&lt;p&gt;The Architectural Blueprint&lt;br&gt;
The goal was simple: create a permanent URL in the cloud that receives webhooks and instantly pipes them down to a local development machine.&lt;/p&gt;

&lt;p&gt;To achieve this without high latency or dropped payloads, I separated the architecture into two distinct layers:&lt;/p&gt;

&lt;p&gt;The Cloud Bridge (Express + ws): A lightweight server deployed on the edge (e.g., Render or GCP) that maintains persistent WebSocket connections with local clients.&lt;/p&gt;

&lt;p&gt;The Local CLI (Node.js): A globally installed npm package (local-webhook-relay) that connects to the cloud bridge, listens for payloads, and forwards them as standard HTTP requests to localhost.&lt;/p&gt;

&lt;p&gt;Multiplexing with Channels&lt;br&gt;
If an entire engineering team uses the same cloud relay, how do you prevent Developer A from receiving Developer B's webhooks?&lt;/p&gt;

&lt;p&gt;The solution was implementing WebSocket multiplexing via URL parameters. When the local CLI connects, it subscribes to a specific channel (e.g., wss://&lt;a href="https://relay.app.com/?channel=auth-service" rel="noopener noreferrer"&gt;relay.app.com/?channel=auth-service&lt;/a&gt;). The Express server maps active connections to these channels using a JavaScript Map&amp;gt;. When a webhook hits &lt;a href="https://relay.app.com/webhook/auth-service" rel="noopener noreferrer"&gt;https://relay.app.com/webhook/auth-service&lt;/a&gt;, the relay performs an O(1) lookup and broadcasts the payload only to the local machines subscribed to that exact channel.&lt;/p&gt;

&lt;p&gt;The Hardest Part: Preserving Cryptographic Signatures&lt;br&gt;
The biggest trap when building a webhook proxy is breaking the cryptographic signatures (like x-paystack-signature or stripe-signature).&lt;/p&gt;

&lt;p&gt;Payment providers hash the exact raw body payload they send you. If your relay server parses that body into a JSON object and then re-stringifies it to send over the WebSocket, the formatting (like spacing or newline characters) might change by a single byte. When your local server tries to verify the signature, the hashes won't match, and the webhook will be rejected.&lt;/p&gt;

&lt;p&gt;To solve this, the Cloud Relay must act as a completely transparent pipe. It must not parse the JSON.&lt;/p&gt;

&lt;p&gt;Instead, we capture the raw Node.js buffer in Express before the JSON middleware touches 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="nx"&gt;JavaScript&lt;/span&gt;
&lt;span class="c1"&gt;// relay-server.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&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="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Capture the raw buffer to preserve exact payload signatures&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
    &lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
        &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt; 
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server then packages the headers, the HTTP method, and the untouched raw body, and ships it over the WebSocket. The local CLI reconstructs the request identically, ensuring that your local security middleware operates exactly as it will in production.&lt;/p&gt;

&lt;p&gt;Security Posture&lt;br&gt;
Building a reverse tunnel requires strict security boundaries. Here is how local-webhook-relay handles it:&lt;/p&gt;

&lt;p&gt;End-to-End TLS Termination: By deploying the cloud bridge on Render, we get automatic TLS termination. The webhooks arrive via https:// and traverse the tunnel via secure WebSockets (wss://).&lt;/p&gt;

&lt;p&gt;Stateless Execution: The relay holds payloads in memory for milliseconds. It does not write to a database, log payloads to the console, or queue missed webhooks. If a CLI isn't actively listening, the payload is immediately garbage collected.&lt;/p&gt;

&lt;p&gt;Try It Out&lt;br&gt;
If you want to stop fighting tunneling limits, you don't even need to deploy the cloud server yourself. I host a free community relay you can use right now.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the CLI:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Bash
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; local-webhook-relay
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Start listening on a unique channel:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Bash
webhook-relay &lt;span class="nt"&gt;--relay&lt;/span&gt; wss://webhook-relay-server.onrender.com &lt;span class="nt"&gt;--target&lt;/span&gt; http://127.0.0.1:3000/api/webhook &lt;span class="nt"&gt;--channel&lt;/span&gt; your-unique-channel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Point your payment provider to the cloud:
Set your webhook URL in Stripe/Paystack to:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[https://webhook-relay-server.onrender.com/webhook/your-unique-channel](https://webhook-relay-server.onrender.com/webhook/your-unique-channel)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open Source &amp;amp; Next Steps&lt;br&gt;
This project is completely open-source. Whether you want to self-host the cloud bridge on your own Ubuntu VPS or contribute to the CLI's codebase, you can find the repositories here:&lt;/p&gt;

&lt;p&gt;CLI Package on npm: &lt;a href="https://www.npmjs.com/package/local-webhook-relay" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/local-webhook-relay&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;If you found this helpful for your local development workflow, drop a star on the repo or let me know in the comments how you're using it!&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>ngrok</category>
      <category>webhook</category>
      <category>tunnel</category>
    </item>
  </channel>
</rss>
