website/docs/user-guide/messaging/relay.md
:::warning Experimental Relay is experimental. The wire contract, auth scheme, and configuration may change without a deprecation cycle while the system is being validated. :::
Hermes Relay is not a chat platform itself — it is a connector system that lets your gateway front one or more real messaging platforms (Discord, Telegram, Slack, WhatsApp, …) without holding any platform credentials. A separate service, the connector, owns the platform bot tokens and sockets. Your gateway dials out to the connector over a single authenticated WebSocket, receives a capability descriptor at handshake, and then exchanges normalized message events (inbound) and actions (outbound) over that socket.
Key properties:
The formal gateway ⇄ connector interface lives in the repository at
docs/relay-connector-contract.md.
Relay is for deployments where a hosted or shared connector service manages the platform side — for example multi-tenant hosting where one shared bot fronts many users' agents, or setups where you don't want bot tokens on the gateway machine. If you run your own bots directly, use the native platform adapters (Telegram, Discord, etc.) instead.
A self-hosted gateway authenticates to the connector with a per-gateway
secret. hermes gateway enroll redeems a single-use enrollment token
(minted by the connector when your tenant's route is provisioned and delivered
with your gateway config) for that secret:
hermes gateway enroll \
--token <enrollment-token> \
--connector-url wss://connector.example.com/relay
What it does:
~/.hermes/auth.json) — this proves which Nous org (tenant) you own. If
gateway.idp.token_url is configured, a generic OAuth2 client-credentials
token from your own IdP is used instead (the air-gapped / self-hosted-IdP
path, no Nous Portal involved)./relay/enroll endpoint over TLS.~/.hermes/.env:
GATEWAY_RELAY_ID, GATEWAY_RELAY_SECRET, GATEWAY_RELAY_DELIVERY_KEY
(plus GATEWAY_RELAY_URL / GATEWAY_RELAY_WAKE_URL when supplied).Restart the gateway afterwards to pick up the new environment.
Flags:
| Flag | Description |
|---|---|
--token | The single-use enrollment token. Also settable via GATEWAY_RELAY_ENROLL_TOKEN. |
--connector-url | Connector base or relay URL (wss://…/relay or https://…). Also settable via GATEWAY_RELAY_URL or gateway.relay_url in config.yaml. |
--gateway-id | Stable id for this gateway instance (used for kill-switch granularity). Defaults to gw-<hostname>. |
--wake-url | Optional reachable URL the connector pokes (payload-free GET) to wake this gateway when buffered work arrives while it is idle. Persisted as GATEWAY_RELAY_WAKE_URL. Without it the gateway still drains buffered messages whenever it next reconnects. |
:::note Managed installs
hermes gateway enroll refuses to run in managed/hosted installs — there the
hosting platform provisions the relay secret directly into the container
environment.
:::
Relay activates when a connector relay URL is configured — there is no separate feature flag. Deployments that don't set it are unaffected.
| Setting | Where | Meaning |
|---|---|---|
GATEWAY_RELAY_URL | env (~/.hermes/.env) | Connector relay WebSocket URL. Presence enables the relay platform. |
gateway.relay_url | config.yaml | Same as above, config-file form (env takes precedence). |
GATEWAY_RELAY_ID | env | This gateway instance's id (written by enroll). |
GATEWAY_RELAY_SECRET | env | Per-gateway secret authenticating the WebSocket upgrade (written by enroll). |
GATEWAY_RELAY_DELIVERY_KEY | env | Per-tenant delivery key (written by enroll; retained for forward-compat). |
GATEWAY_RELAY_WAKE_URL / gateway.relay_wake_url | env / config.yaml | Optional wake-poke target for idle/suspended gateways. |
GATEWAY_RELAY_PLATFORMS | env | Comma-separated list of platforms this gateway fronts over one connection (e.g. discord,telegram). Usually stamped by the deployment/orchestrator. |
GATEWAY_RELAY_BOT_IDS | env | JSON map of per-platform bot identities, e.g. {"discord": {"botId": "…"}}. Paired with GATEWAY_RELAY_PLATFORMS. |
gateway.idp.token_url | config.yaml | When set, enrollment/provisioning authenticates via generic OAuth2 client-credentials against your own IdP instead of Nous Portal. |
What actually works over a relay connection is negotiated at handshake: the
connector advertises a supported_ops list, and the gateway only uses an
operation the connector explicitly advertises (older connectors fall back to a
legacy send/edit/typing/follow_up set). Per-platform capability flags
(edit-based streaming, threads, draft streaming, markdown dialect, message
length limit) also come from the handshake descriptor. Subject to that
negotiation, the relay supports:
thread_create / thread_rename operations, with a no-clobber guard so a
human's manual rename wins. Availability depends on the platform (e.g.
Slack threads can't be renamed; WhatsApp has no threads).get_chat_info lookups are proxied to the connector
when advertised.Multi-platform fronting is supported: one gateway can front several platforms (e.g. Discord and Telegram) over a single relay connection, with each outbound message tagged for the platform it targets.
Enrollment fails with 401 — the connector could not verify your identity
token. Re-login with hermes auth add nous (or hermes setup) and retry.
Enrollment fails with 403 — the enrollment token is invalid, expired, already used, or belongs to a different tenant. Enrollment tokens are single-use; request a fresh one from whoever provisioned your tenant route.
"Could not reach the connector" — check the connector URL. You can paste
either the wss://…/relay dial URL or the https://… base URL; the CLI maps
between them automatically.
enroll refuses to run — you are in a managed/hosted install, where the
relay secret is provisioned by the hosting platform. Self-enrollment is only
for self-hosted gateways.
Relay platform shows as disabled after it previously worked — a WebSocket close with code 4401 after a successful handshake means the gateway's secret was revoked (e.g. the instance was deprovisioned). The gateway deliberately stops reconnecting and reports relay as disabled rather than retrying. A 4401 before any successful handshake is treated as a transient not-yet-provisioned race and retried normally.
Nothing changed after enrolling — the gateway reads GATEWAY_RELAY_* at
startup. Restart it (hermes gateway restart).
A feature (buttons, media, threads…) silently degrades to plain text — the
connector for your platform did not advertise that operation in its handshake
supported_ops. The gateway intentionally falls back to the text behavior
rather than sending an op the connector can't handle.