Skip to content

Running a Self-hosted relay

A Self-hosted relay is a relay you run yourself and enroll in your Organization. It talks to Switchboard over the public internet at https://switchboard.mux.global, so you don’t need a private network, a VPN, or a custom build.

The relay is moq-relay, from the open-source moq-dev/moq project, version 0.15 or later. The moq-relay README covers installing it from packages or Docker, and has example configurations. The fastest way to start is the moqdev/moq-relay Docker image, shown in step 3.

Once it’s enrolled, your relay calls Switchboard for three things:

What When Endpoint
Enrollment Once, when you set it up POST /cluster/enroll
Peer discovery About once a second GET /cluster/connect
Admission For every session, and every 30 seconds while it lasts POST /auth

Switchboard answers Admission from the location nearest your relay, and keeps answering while the rest of Switchboard is down or being deployed. If a peer discovery poll fails, your relay keeps the peers it already has.

A Self-hosted relay carries only your Organization’s Namespaces. Whatever token a Client brings, the relay won’t let it publish or subscribe outside acme/, or whatever your Namespaces are.

You need an API key for your Organization, and the Organization’s id from GET /v1/organizations. Get the key from the dashboard at https://app.caprid.com/app, or see Credentials. The examples below use Organization 4, which owns the Namespace acme.

curl -X POST https://app.caprid.com/v1/organizations/4/enrollments \
-H "Authorization: Bearer $SB_API_KEY" \
-d '{"url": "https://relay1.example.com:4443/", "region": "iad", "gateway": false}'

The response includes the token, which is shown only this once:

{"id": "7c1e...", "token": "sbe_...", "expires_at": 1790301600, ...}

The token fixes everything about the relay’s Node, and the relay can’t change any of it later:

  • url is where other relays connect to it, and where Clients connect too, unless you set client_url.
  • region is which of your Regions the relay joins. Relays in the same Region all connect to each other. If you leave it out, the relay joins default. Region names are yours alone, so your iad has nothing to do with anyone else’s.
  • gateway is whether the relay may be picked to carry its Region’s traffic to your other Regions.
  • client_url is optional. Set it when Clients reach the relay at a different address from the one its peers dial, for example when url is a private name. Whatever hands relay addresses to your Clients can read it from the Node.

The token expires after an hour if nobody uses it. To change that, set expires_in, in seconds, up to a day. To revoke it early, call DELETE /v1/organizations/4/enrollments/{id}.

On the relay’s host, run this once:

curl -X POST https://switchboard.mux.global/cluster/enroll \
-d '{"token": "sbe_..."}' > enrollment.json
jq -r .node_key enrollment.json > /etc/moq/node.key # sbn_...
jq '{cluster, auth}' enrollment.json

The second jq prints the settings for relay.toml:

{
"cluster": {
"node": "https://relay1.example.com:4443/",
"connect_api": "https://switchboard.mux.global/cluster/connect?jwt=sbn_..."
},
"auth": {"url": "https://switchboard.mux.global/auth?jwt=sbn_..."}
}

The token works only once, and this response is the only time you see the Node key, so keep it. If you lose the Node key, enroll the relay again as a new Node, and delete the old one with DELETE /v1/organizations/4/nodes?url=....

Write /etc/moq/relay.toml:

[listen]
# Where Clients and peers connect. The certificate has to be valid for the
# host in [cluster] node.
bind = "[::]:4443"
tls.cert = ["/etc/moq/cert.pem"]
tls.key = ["/etc/moq/key.pem"]
[cluster]
# The values enrollment returned, unchanged.
node = "https://relay1.example.com:4443/"
connect_api = "https://switchboard.mux.global/cluster/connect?jwt=sbn_..."
# The Node key, as a file. The relay presents it to the peers it connects to.
token = "/etc/moq/node.key"
[auth]
# Switchboard decides every session here.
url = "https://switchboard.mux.global/auth?jwt=sbn_..."

relay.toml contains the Node key twice, in those URLs. Protect it the same way you protect node.key.

Don’t set [connect] tls.insecure. That setting turns off certificate checks for the relay’s calls to Switchboard as well as to its peers, and switchboard.mux.global has a publicly trusted certificate. Use it only if your relays present self-signed certificates to each other.

To run the relay with Docker, put relay.toml, node.key and your certificate in /etc/moq, then run:

docker run -d --name moq-relay --restart unless-stopped \
-p 4443:4443/udp \
-v /etc/moq:/etc/moq:ro \
moqdev/moq-relay:0.17.9 /etc/moq/relay.toml

The image takes the path to its config file as its only argument. QUIC runs over UDP, so open UDP port 4443, or whichever port you bound, in your firewall.

  • Polling. A few seconds after the relay starts, GET /v1/organizations/4/nodes lists the Node with "live": true.
  • Peers. With two or more relays in a Region, each relay’s log shows it connecting to the others.
  • Admission. A Client token from POST /v1/namespaces/acme/tokens works through the relay. A token for another Organization’s Namespace, or no token, is refused.

DELETE /v1/organizations/4/nodes?url=https://relay1.example.com:4443/ deletes the Node and its Node key. Switchboard refuses the relay on its next poll, and its peers drop it. To bring the relay back, enroll it again.

  • Your Regions can’t link to Mux’s relays yet. Until they can, a Namespace’s platform_peering setting has no effect.
  • To move a relay to another Region, or to change whether it can be a gateway, enroll it again.