If you build fintech, e-commerce, or SaaS platforms, you already know the pain of testing webhooks locally.
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.
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.
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.
Here is a deep dive into how I architected it, the security challenges involved, and how you can use it (or build your own).
The Architectural Blueprint
The goal was simple: create a permanent URL in the cloud that receives webhooks and instantly pipes them down to a local development machine.
To achieve this without high latency or dropped payloads, I separated the architecture into two distinct layers:
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.
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.
Multiplexing with Channels
If an entire engineering team uses the same cloud relay, how do you prevent Developer A from receiving Developer B's webhooks?
The solution was implementing WebSocket multiplexing via URL parameters. When the local CLI connects, it subscribes to a specific channel (e.g., wss://relay.app.com/?channel=auth-service). The Express server maps active connections to these channels using a JavaScript Map>. When a webhook hits https://relay.app.com/webhook/auth-service, the relay performs an O(1) lookup and broadcasts the payload only to the local machines subscribed to that exact channel.
The Hardest Part: Preserving Cryptographic Signatures
The biggest trap when building a webhook proxy is breaking the cryptographic signatures (like x-paystack-signature or stripe-signature).
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.
To solve this, the Cloud Relay must act as a completely transparent pipe. It must not parse the JSON.
Instead, we capture the raw Node.js buffer in Express before the JSON middleware touches it:
JavaScript
// relay-server.js
const express = require('express');
const app = express();
// Capture the raw buffer to preserve exact payload signatures
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf;
}
}));
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.
Security Posture
Building a reverse tunnel requires strict security boundaries. Here is how local-webhook-relay handles it:
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://).
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.
Try It Out
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.
- Install the CLI:
Bash
npm install -g local-webhook-relay
- Start listening on a unique channel:
Bash
webhook-relay --relay wss://webhook-relay-server.onrender.com --target http://127.0.0.1:3000/api/webhook --channel your-unique-channel
- Point your payment provider to the cloud: Set your webhook URL in Stripe/Paystack to:
[https://webhook-relay-server.onrender.com/webhook/your-unique-channel](https://webhook-relay-server.onrender.com/webhook/your-unique-channel)
Open Source & Next Steps
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:
CLI Package on npm: https://www.npmjs.com/package/local-webhook-relay
GitHub Repository: https://github.com/oluibk2020
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!
Top comments (0)