twine · gleam / otp 26+

webhook fan-out
for the beam.

one inbox, many outboxes. toml-defined routes, otp-supervised dispatch, a tiny sqlite ring for replays, and an sse feed you can curl. i wanted webhookd | tee and kept not finding it, so this is that.

# twine.toml
[server]
port = 8800
db   = "twine.db"

[[route]]
source = "/github"
when_header = "X-GitHub-Event: push"
destinations = [
  "https://hooks.slack.com/services/...",
  "https://ci.local/trigger",
]

[[route]]
source = "/stripe"
retries = 3
secret_header = "X-Hub-Signature-256"
destinations = ["https://ledger.local/stripe"]
live feed

curl the sse stream, watch things happen.

$ curl -N http://localhost:8800/_twine/events

event: dispatch
data: {"id":"01hx...","source":"/github","ok":2,"fail":0,"ms":141}

event: dispatch
data: {"id":"01hx...","source":"/stripe","ok":1,"fail":0,"ms":63}

event: retry
data: {"id":"01hx...","dest":"ci.local","attempt":2,"wait_ms":400}

$ curl -X POST http://localhost:8800/_twine/replay/01hx...
ok
what it does

small, and on purpose.

toml routes in, fanout out.

point any service's webhook at /<something>. twine looks up the route, checks an optional header guard, verifies optional hmac, fires the body at every destination concurrently with backoff. retries are per-destination so one slow endpoint doesn't stall the others.

sqlite replay ring.

every event lands in a ~1000-row ring. replay any stored event with one POST /_twine/replay/:id. the db reaps on insert via offset 1000. on a hot server that's a tiny redundant scan. haven't benchmarked.

otp supervised.

each route has its own worker tree. if a dispatch crashes, supervisor restarts the worker. the http listener stays up. this is the part you get for free by writing it in gleam instead of node.

rough edges
install
$ git clone https://github.com/f4rkh4d/twine
$ cd twine
$ gleam run -- check --config twine.toml
$ gleam run -- serve --config twine.toml

needs gleam 1.15+ and otp 26+.