Webhook-to-SSE gateway with hierarchical topic routing and signature verification

wicket#

A webhook-to-SSE gateway. Receives webhook POSTs, delivers them to subscribers as Server-Sent Events.

How it works#

The entire HTTP API is two verbs on the same path:

POST  /<path>                              →  publish an event
GET   /<path>  Accept: text/event-stream   →  subscribe (SSE stream)

The path is an arbitrary topic name. Path separators create a hierarchy — subscribing to a prefix gets you all events underneath it:

GET /github.com/chrisguidry/docketeer  →  just docketeer events
GET /github.com/chrisguidry/           →  all chrisguidry repo events
GET /github.com/                       →  all GitHub events
GET /                                  →  everything

A POST to /github.com/chrisguidry/docketeer delivers to all four subscribers above.

Zero configuration by default#

wicket works out of the box with no configuration — any path accepts POSTs and SSE connections. A YAML configuration file layers in authentication only where needed:

paths:
  github.com/chrisguidry/docketeer:
    verify: hmac-sha256
    secret: "the-github-webhook-secret"
    signature_header: X-Hub-Signature-256
    subscribe_secret: "token-for-docketeer-subscribers"

Values support environment variable interpolation via $VAR or ${VAR} syntax, so secrets can come from the environment rather than being hardcoded:

paths:
  github.com/chrisguidry/docketeer:
    verify: hmac-sha256
    secret: "${WEBHOOK_SECRET}"
    signature_header: X-Hub-Signature-256
    subscribe_secret: "${SUBSCRIBE_TOKEN}"
  • Unconfigured paths accept POSTs without verification
  • Configured paths verify webhook signatures and reject invalid ones
  • Subscribe secrets require Authorization: Bearer <token> for SSE connections
  • Configuration is hot-reloaded on file change or SIGHUP

Browser support#

CORS headers are sent on all responses, so browser apps can POST events and subscribe via EventSource. One limitation: the browser's EventSource API doesn't support custom headers, so browser clients can only subscribe to paths without a subscribe_secret. Protected paths are for server-to-server use.

Event format#

Every event is wrapped in an envelope and delivered as SSE:

id: unique-event-id
data: {"id":"...","timestamp":"...","path":"github.com/chrisguidry/docketeer","headers":{...},"payload":{...}}

Subscribers can reconnect with Last-Event-ID to replay missed events from an in-memory ring buffer (default 1000 events).

Filtering#

Optional query params filter events on the subscribe side:

GET /github.com/chrisguidry/docketeer?filter=payload.ref:refs/heads/main

Simple dot-path equality matching. Multiple filters are AND'd.

Usage#

wicket                                        # listen on :8080, no configuration
wicket -address :9090                         # custom listen address
wicket -configuration wicket.yaml             # with webhook verification
wicket -buffer-size 5000                  # larger replay buffer

Building#

go build -o wicket .

Or with Docker:

docker build -t wicket .

Why the name "wicket"?#

A "wicket" is a small door or gate next to a larger one.