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.
Installing
With Docker Compose
The gateway ships as a published image,ghcr.io/fapost-lab/gateway, built by
the same workflow that publishes core and web. Nothing is compiled locally.
It sits behind a Compose profile, so it starts only when asked:
docker/compose.yaml already references the image; GATEWAY_VERSION pins the
tag and defaults to latest.
The image is built in two stages and the final stage is scratch: it 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. It runs as UID 65534 and
carries no .env, so one image is promoted unchanged from staging to production.
Standalone
Without Docker the gateway needs a binary rather than an image. Neither way of getting one requires a Go toolchain on the server. Out of the published image, if Docker is available on any machine — no build at all:scratch with the binary at /gateway, so this lifts
it straight out. The machine you do it on need not be the target server: the
binary is static, so only the architecture has to match.
From source, if you would rather not pull an image, or you are changing the
gateway:
go.mod. The version is 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 cross-compiles from whatever
machine you have in front of you: build on a Mac, copy the linux/amd64 binary
to the server.
If you are modifying the gateway rather than deploying it, its test and lint
targets are covered in Testing.
Generating the service files
Either way, once the gateway is available:.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.