Skip to content
Doorbell
Dashboard

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.

  1. Note the held right now counter above.

  2. Send a webhook to that offline tunnel:

    Terminal
    $ curl -i -X POST https:///t/shop/hook -d '{"n":1}'

    You get 202 Accepted instead of 502. It was in Postgres before that response reached you.

  3. Reload this page. The counter has gone up by one. That is your request, in the mailbox.

  4. Now try a name nobody reserved:

    Terminal
    $ curl -i -X POST https:///t/zzrandom99/hook -d '{"n":1}'

    404 this 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

Every other tunnel
ngrok http 3000https://…ngrok.io ^Claptop closed 3 callbacks arrive502 Bad Gateway ngrok http 3000gone. new URL. re-send by hand.
Doorbell holds it
doorbell -name shop 3000https://…/t/shop/ ^Claptop closed 3 callbacks arrive202 Accepted, stored doorbell -name shop 3000all 3 delivered, in order

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.

Capability comparison between Doorbell and other tunnels
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.

  1. 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.

  2. Answered straight away

    202 Accepted, not 502, so the sender's retry policy never fires. It means stored, not your app processed it.

  3. Delivered in order

    The queue drains oldest first, so created still arrives before updated.

  4. Delivered once

    An atomic lease means two laptops reconnecting cannot both drain the same queue.

  5. 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.

  6. A URL that does not move

    Names are reserved, so it survives restarts. Register it with the sender once.

  7. 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-Replay is present.

  8. 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.

Terminal
$ 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.

  1. Copy zerops-import.yml from the repository.

  2. In app.zerops.io, choose Import a project using YAML template and paste it.

  3. Wait about 90 seconds.

  4. Open the raw TCP control port: gw → Public access → port routing, public 7000 → internal 7000, protocol tcp.