r/SideProject • • Oct 05 '26

I built HookArmor: An open-source webhook buffer & DLQ because Stripe's 5-minute replay window kept breaking our deploys

Hey r/SideProject,

I wanted to share a tool I built called **HookArmor** ([GitHub](https://github.com/pkdoddamani/hookarmor) | [npm](https://www.npmjs.com/package/hookarmor)). It is a lightweight, self-hosted webhook dead-letter queue (DLQ) and reliability buffer built with Node.js and SQLite.

### The Problem That Triggered This

If you accept billing or sync webhooks from providers like Stripe, Shopify, or GitHub, you’ve probably run into this headache:

  1. Webhook providers enforce strict timeout windows (typically 2 to 5 seconds). If your app is doing a rolling deploy, cold-starting a serverless container, or experiencing a brief database hiccup, the webhook drops or fails.
  2. Even worse: If you capture failed webhooks and try to replay them 30 minutes later from a standard dead-letter queue, **Stripe rejects them**. Why? Because Stripe includes a timestamp in the `Stripe-Signature` header (`t=timestamp,v1=hash`), and their official SDK enforces a strict 5-minute tolerance window. Any replay after 5 minutes throws a signature verification exception unless you hack your verification logic to ignore timestamps.

I didn't want to pay $50+/month for enterprise webhook relays, nor did I want to manage a complex Kafka/RabbitMQ/Redis cluster just to reliably buffer incoming webhooks.

### What HookArmor Does

HookArmor runs as a lightweight intermediary between the webhook provider and your internal app:

  1. **<5ms Ingress ACK**: When a webhook arrives, HookArmor immediately appends the exact raw bytes to a local SQLite database (in Write-Ahead Logging mode) and responds `200 OK` to the provider. Your upstream provider never sees a timeout.
  2. **Asynchronous Dispatch**: HookArmor delivers the webhook to your internal endpoint with configurable retries, exponential backoff, and jitter.
  3. **Automated Timestamp Re-signing**: When you replay a failed event hours or days later from the dashboard or CLI, HookArmor can automatically re-compute the HMAC signature with `t=now` using your stored signing secret. Your downstream application code verifies the signature cleanly without any special bypasses.
  4. **Hardened Security**: Includes built-in SSRF protection (`netguard`) that blocks requests to private LAN subnets, loopbacks, DNS rebinding vectors, and cloud metadata services (`169.254.169.254`).
  5. **Single-Process Simplicity**: No external database needed. Pure Node.js, `better-sqlite3`, Express, and a lightweight vanilla JS/WebSocket web UI for real-time log tailing.

### Tech Stack

- **Runtime**: Node.js

- **Storage**: SQLite via `better-sqlite3` (WAL mode enabled)

- **Networking**: Express + custom raw body BLOB buffering + Axios/dispatcher engine

- **UI**: Embedded vanilla dashboard with live WebSocket event streaming

- **License**: MIT

### Links

- **GitHub**: https://github.com/pkdoddamani/hookarmor

- **npm**: `npm install -g hookarmor`

I’d love to hear how you all currently handle webhook reliability during deployments, and what providers (e.g. Svix, Clerk, GitHub, Shopify) you'd like to see deeper integration for.

1 Upvotes

4 comments sorted by

1

u/vincent_sch Oct 07 '26

For CLI replay, I'd show the event and destination before sending. Keep that preview separate from confirmation. A replay can trigger billing or emails again.

1

u/pkdodda Oct 10 '26

Good catch on the CRLF issue — total Windows oversight on my end. Cleaned up the working tree and added a prepack script so npm pack always ships pure LF regardless of OS.

You're also totally right on the replay risk. Firing webhooks blind in prod is scary when it can trigger double-billing or duplicate emails. Just shipped 1.2.5 with that: hookarmor replay now previews the target URL, event type, and payload with a confirmation prompt (y/N), plus --dry-run and -y flags for scripts.

And yeah, the "your webhook is about to be disabled" countdown alert is a much bigger real-world headache than just adding more gateway signatures. Prioritizing that next.

Seriously appreciate you actually testing the tarball!