Self-hosted · running on Zerops
A tunnel that doesn't lose your webhooks.
A whole tunnel network - gateway, database, queue and TLS - deployed into your own Zerops account from one YAML file. Nobody else's servers sit in the path of your traffic. Which is exactly why it can hold what arrives while your laptop is shut, and deliver it in order when you come back.
Verify it yourself Open the live dashboard MIT · one binary · no account
This gateway · last 30 minutes
Live
Mode
…
Live tunnels
…
Held right now
…
Mailbox
…
Shaded bands are minutes a tunnel was disconnected. The ticks inside them are requests held while it was.
Verify it yourself, in 90 seconds
This gateway keeps a tunnel called shop reserved with
nothing connected to it - the same situation as a laptop with the lid shut. Nothing to
install, no account, no token.
Note the held right now counter above.
Send a webhook to that offline tunnel:
Terminal $ curl -i -X POST https://…/t/shop/hook -d '{"n":1}'
You get
202 Acceptedinstead of502. It was in Postgres before that response reached you.Reload this page. The counter has gone up by one. That is your request, in the mailbox.
Now try a name nobody reserved:
Terminal $ curl -i -X POST https://…/t/zzrandom99/hook -d '{"n":1}'
404this time. Only reserved names are held, so nobody fills the mailbox by guessing URLs.
That shows a request being held. Watching one drain in order needs a tunnel of your own - three lines below.
What a tunnel is actually for
Something on the internet needs to POST to code on your laptop, and your laptop has no public address. So you open a tunnel - and then you close the lid.
StripeGitHubGitLab CISlack TwilioShopifyAuth0anything with a webhook
Same URL both times. That matters more than it looks: a sender you registered a URL with does not come back to ask for a new one.
How the others handle it
All good tools. None is built to hold a request, because none has anywhere to put one. Capability only - not price.
| Can it… | ngrok | Cloudflare Tunnel | localtunnel | frp / bore | Doorbell |
|---|---|---|---|---|---|
| Keep a request that arrives while your agent is offline | ✕ERR_NGROK_3200 | ✕origin error | ✕ | ✕ | ✓stored in Postgres |
| Answer the sender with a success, not an error | ✕ | ✕ | ✕ | ✕ | ✓202 Accepted |
| Deliver what it kept, in order, on reconnect | ✕ | ✕ | ✕ | ✕ | ✓ |
| Replay a captured request | ✓while connected | ✕ | ✕ | ✕ | ✓ |
| Show a live request inspector | ✓ | ✕ | ✕ | ✕ | ✓ |
| Run on infrastructure you control | ✕ | ✕ | ✕ | ✓ | ✓ |
| Bring gateway, database, queue and TLS up from one file | - | - | - | ✕you assemble it | ✓ |
ngrok could not hold your webhooks even if it wanted to - holding them means storing your payloads, signatures and tokens on someone else's servers. Doorbell can, because the database is yours. That is the whole argument: the holding is not a bolt-on feature, it is what owning the infrastructure buys you.
What the gateway guarantees
Not "forwards HTTP". These are the promises the system is built to keep, and every one of them is visible in the dashboard while it happens - limits included.
Nothing is dropped
Written to Postgres before the sender is answered - never held only in memory. A tunnel holds 200 requests; past that the oldest are evicted, so nothing is dropped up to that cap and not beyond it.
Answered straight away
202 Accepted, not502, so the sender's retry policy never fires. It means stored, not your app processed it.Delivered in order
The queue drains oldest first, so
createdstill arrives beforeupdated.Delivered once
An atomic lease means two laptops reconnecting cannot both drain the same queue.
Replay on demand
Re-send any stored request, body byte for byte, whenever you like - press r. Signing headers are redacted before storage, so they do not come back with it.
A URL that does not move
Names are reserved, so it survives restarts. Register it with the sender once.
Secrets are never stored
Signing and auth headers are redacted before the database, matched by shape rather than vendor name. The cost is real and worth knowing: a held webhook arrives unsigned, so GitHub-style signature checks on it will fail. Skip verification when
X-Doorbell-Replayis present.You can see all of it
Live request and response bodies, plus a timeline of exactly when a tunnel was offline.
Open a tunnel of your own
Three lines. The tunnel is live before the third one finishes, and this gateway asks for no token.
$ go install github.com/BigAchiever/doorbell/cmd/doorbell@latest $ export DOORBELL_GATEWAY=… $ doorbell -name pick-a-name 3000
Name it, or nothing is held
Only a reserved name survives a disconnect.
-name reserves one; without it you
get a random name that is dropped when you leave. Flags go before the port. Pick any
name that isn't taken - shop is
already reserved and the gateway will refuse it, which is rather the point of
reserving names.
Needs IPv6
The address above is IPv6. Zerops serves a raw TCP port from the project's own public
IP, and a shared IPv4 cannot carry one. The
curl check at the top works on
either.
No Go?
Prebuilt binaries for macOS, Linux and Windows are built by
make release.
What you get back
https://…/t/shop/
- a stable URL. Register it once with whatever is calling you.
Why this had to be built on Zerops
Doorbell is not an app that happens to run on Zerops. It is one that cannot exist unless six separate things are true at the same time - and almost no platform makes all six true at once.
On Zerops the entire middle column is one YAML file and ninety seconds.
A raw public TCP port
The multiplexed control channel. Not HTTP.
On Zerops
✓Ports 10–65435, declared in zerops.yaml
Instead
✕A VPS and your own firewall. No PaaS exposes one; managed TLS cannot sit in front of it.
A process that never sleeps
A socket held for hours. If it sleeps, every tunnel dies.
On Zerops
✓No scale-to-zero
Instead
✕A function stops when it answers. Nothing is left holding the socket.
Postgres, privately reachable
The mailbox - what makes 202 honest.
On Zerops
✓Managed, one line in the import file
Instead
✕A database to run, back up and keep off the public internet
Valkey
The routing table and event bus across containers.
On Zerops
✓Managed, same file
Instead
✕Another service to run, and a second thing that can silently break scaling
A private network
The database holds captured bodies. It must never face the internet.
On Zerops
✓VXLAN by default, hostnames resolve, nothing to configure
Instead
✕VPC, subnets, security groups, and the discipline to keep them right
Managed TLS and HTTP ingress
Senders will not deliver to a bad certificate.
On Zerops
✓Automatic on the project subdomain
Instead
✕Certificates, renewal, a reverse proxy in front of the raw port
“Could you not just put this on Vercel?”
Half of it, yes. The mailbox is an HTTP endpoint that writes to Postgres, and that runs almost anywhere.
The tunnel does not. A serverless function stops when it answers, so nothing is left holding your socket. Vercel rejects even WebSocket handshakes regardless of configuration - a raw TCP listener is far further out of reach than that.
The platform choice here is not a preference. It is the feature.
Honest caveat
Any one of these is easy somewhere - a $5 VPS gives you the raw port. What no other target gives you is all six at once, on one private network, from a single pasted file.
Deploy your own
That YAML file is the product: ngrok is a SaaS you rent, this is a network you own.
-
Copy
zerops-import.ymlfrom the repository. -
In app.zerops.io, choose Import a project using YAML template and paste it.
-
Wait about 90 seconds.
-
Open the raw TCP control port: gw → Public access → port routing, public 7000 → internal 7000, protocol
tcp.