What it does
How verification works without platform code
The gateway knows nothing about Telegram. Each channel adapter publishes an ingress spec — a small declaration of how that platform’s webhook is signed and deduplicated:contracts/ingress/golden.json — 40 cases
executed by both the PHP and the Go test suites, so the two cannot drift apart.
Building from source
The gateway is a single static binary with no runtime dependencies: a build on any machine runs on any Linux of the same architecture — no matching libc, no interpreter, nothing to install alongside it. Prerequisites: Go 1.27 or newer. Nothing else; the only dependency is the Redis client, vendored throughgo.mod.
bin/gateway, with the version stamped in from git describe so a
running process can be traced back to a commit.
Go needs no per-target toolchain, so
make dist produces every supported
platform’s release binary from one developer machine.
Run
make check before shipping a gateway build. Its spec executor is held to
the same golden file as the PHP implementation, so a regression here is a
regression in webhook verification for every channel.As a container image
scratch: the shipped image holds
the binary, root certificates, and nothing else — no Go toolchain, no shell, no
package manager. That keeps it small and leaves an attacker who reaches the most
exposed process in the stack with no tools to work with.
The image runs as UID 65534 and carries no .env; configuration is injected by
the runtime, so one image is promoted unchanged from staging to production.
Installing
With Docker Compose
The gateway sits behind a profile, so it starts only when asked:Standalone
.env and generates a systemd unit, a logrotate
config or a compose fragment into gateway/dist/ for you to review and install.
Nothing is written outside the project and no service is restarted.
Configuration
Set bygateway:install, or by hand.
Routing
Limits and timeouts
Logging
Redis
Redis settings are not duplicated: the gateway reads the same
REDIS_*
variables as the application. REDIS_PREFIX in particular must match, or every
lookup misses and the gateway proxies everything while appearing healthy.
Migrating existing channels
Switching the driver only affects channels registered from that moment on. Existing ones keep the URL their provider stored, and both paths stay valid, so there is no cutover and no window to race against.setWebhook is
rate-limited; re-run until the report is empty.
Rolling back is the same command after setting the driver back to laravel.
Order of operations
- Publish the specs —
php artisan ops:ingress-specs-publish - Deploy the gateway and put TLS in front of it
- Set
WEBHOOK_INGRESS_DRIVER=gatewayandWEBHOOK_GATEWAY_URL - Migrate existing channels at your own pace
ops:webhook-warmup republishes them too, so a single command restores all
ingress state after a Redis flush.
Verifying
Operating
TLS. The gateway speaks plain HTTP; terminate in front with Traefik or Caddy. The terminator must not modify the request body — signatures cover the exact bytes the provider sent. Logs. JSON on stdout by default. In file mode,SIGHUP reopens the file, so
logrotate handles rotation; the same signal also drops cached ingress specs, so
a republish takes effect without a restart.
Shutdown. SIGTERM drains in-flight requests. This matters more than usual:
a delivery already answered with 200 will not be sent again, so cutting those
requests off loses messages outright.
What is never logged. The webhook hash is the credential for a channel, so
only a fingerprint of it appears. Request bodies and signature headers never do.