Skip to main content
An optional Go service that accepts provider webhooks, verifies them and queues them, taking that work off PHP. It is optional and the application is complete without it. Without the gateway, webhooks go to the Laravel route exactly as they always have. Deploy it when webhook volume becomes a bottleneck, not because it exists.

What it does

Per delivery: rate-limit by channel, read the routing entry from Redis, verify the signature, claim an idempotency key, publish the job. About a millisecond, no PHP process involved. The second arrow matters more than the first. Redis is a cache for the gateway, never the source of truth. On a cache miss, an unreadable Redis, an unknown platform or a spec it cannot execute, it forwards the request to the application, which resolves the channel from its database and repopulates the cache. A cold or broken Redis costs throughput, not deliveries.

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:
Adding a channel therefore stays a pure PHP change and needs no gateway release. A platform whose verification is too complex to express declaratively simply does not publish a spec, and its webhooks stay on the PHP path. Both implementations are held to 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 through go.mod.
That produces 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

The build is two-stage and the final stage is 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

Then generate the supervisor files:
It asks how the gateway will run, where providers will deliver, and where to log, then writes the settings into .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 by gateway: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.
Re-registration reuses the same public hash and secret — only the host changes — so the provider sees no interruption. Batching exists because 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

  1. Publish the specs — php artisan ops:ingress-specs-publish
  2. Deploy the gateway and put TLS in front of it
  3. Set WEBHOOK_INGRESS_DRIVER=gateway and WEBHOOK_GATEWAY_URL
  4. Migrate existing channels at your own pace
Step 1 comes first for a reason: without published specs the gateway has nothing to verify against and proxies every delivery straight back to the application. ops:webhook-warmup republishes them too, so a single command restores all ingress state after a Redis flush.

Verifying

Checks the driver, that Redis is reachable, that the key prefix is what the gateway will resolve, that published specs match the adapters, that the gateway’s health endpoint answers, and how many channels are still on the old URL.

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.