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.
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.
One failed delivery and it's your problem, unless you open the UI and redeliver by hand.
| Provider | What happens when your tunnel is down | |---|---| | GitHub | No automatic retries at all | | Stripe (test mode) | 3 attempts over a few hours | | Shopify | 8 attempts over 4 hours, then deletes your webhook subscription |
Read the last one again. Shopify doesn't just give up on the event. It unsubscribes your app.
Here's what I hadn't thought about properly. A tunnel turns your laptop into production infrastructure for somebody else's system.
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.
When that happens the sender gets a 502. It burns one of the three or four attempts it was ever going to make, and then the event is gone.
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.
A tunnel is a pipe, not a mailbox. If nothing is listening at your end, the request has nowhere to go.
So I built the mailbox. It's called Doorbell, and it's a tunnel with a database in the path.
You have to take my word for everything else in this post. This part you can check yourself.
I'm running a public gateway — the always-on server that holds tunnels open. It has a tunnel named shop with nothing connected to it, which is the same state as your laptop with the lid shut.
On a phone? Just open the live page and watch the held right now counter. At a keyboard, send it a webhook:
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.
That 404 matters as much as the 202. Only reserved 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."
A held webhook will fail a signature check. 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.
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 X-Doorbell-Replay header, so you can skip verification when you see it.
