Upgrading.
What happens to a live pool when the hub or a node binary moves forward — and why the mTLS migration is something you run, not something that runs on you.
0.1.0-alpha.N → -beta.N → -rc.N → 0.1.0 ladder. The wire protocol between hub and node, the on-disk state layout and the CLI surface can all change without a deprecation period; /v1 tracks the OpenAI shape and is the most stable thing here. Nothing is accredited or covered by a compatibility promise.Getting the new binary
There's no separate upgrade command: moving a hub or a node forward means re-running the same installer used for the first install. Both scripts overwrite the binary already on PATH:
The source and releases open soon. Until then there is nothing to install.
The same script covers Windows too — there's no install.ps1. Run it under Git Bash (Git for Windows); it installs the windows_amd64 build and adds it to your user PATH.
With no flag, it resolves --channel auto (also settable as RELAY_CHANNEL): try the newest stable release first and, since the project hasn't shipped a stable 0.1.0 yet, fall back to the newest release of any kind. Pin an exact release instead with --version (or RELAY_VERSION), which always wins over the channel:
The source and releases open soon. Until then there is nothing to install.
| Control | What it does |
|---|---|
--version | Install this exact tag, e.g. v0.1.0-alpha.3. Always wins over --channel. |
--channel | auto (default), stable, or prerelease (alias rc). stable errors clearly instead of silently falling back when no stable release exists yet. |
RELAY_VERSION / RELAY_CHANNEL | The same two controls as environment variables, for a scripted upgrade. |
The Linux .deb/.rpm/.apk packages upgrade the way any system package does — install the newer package for your architecture over the old one. The installer scripts' --version/--channel controls don't apply to that path.
/releases/latest only ever resolves to a non-prerelease, so it stays unresolved for the whole alpha/beta/rc ladder — --channel auto already accounts for that on its own. A link meant to reach the current release reliably during the alpha ladder needs an exact tag, e.g. /releases/download/v0.1.0-alpha.3/relay_linux_amd64.tar.gz.Coming from the old opal CLI
The product renamed from opal to relay: the binary, the tray app, every OPAL_* environment variable, and every on-disk path. Nothing has to be done by hand — the new binary migrates a pre-rename install itself, once, the first time a state-touching command runs (serve, status, bench, chat, doctor, up, down, token, node, model, key, audit, cache, uninstall).
| Before | After |
|---|---|
opal | relay |
tray app (opalrelay) | relay-tray |
OPAL_* | RELAY_* |
hub state — ~/.local/share/opal | ~/.local/share/relay |
node config — ~/.config/opal | ~/.config/relay |
database — opal.db (+ -wal/-shm) | relay.db |
| systemd: opal.service / opal-node.service | relay.service / relay-node.service |
Migration moves the old directories to the new ones, renames the files inside them, and rewrites the absolute model paths and operator pins the database holds so they still resolve after the move. If a directory from before the rename still exists and the new one already exists too, relay keeps the new one and leaves the old one alone, with a one-time warning — it never merges or deletes on its own.
RELAY_*. Any OPAL_* variable still set logs one startup warning naming it and its replacement — it is never silently honored. Migration refuses to run — rather than move a directory out from under a live process — while a pre-rename opal serve still has the database open, or a pre-rename agent recorded in the old node directory's pid files is still running; the error names exactly what to stop first.
An upgraded hub doesn't break an already-enrolled node
The legacy plaintext /api/nodes/enroll and /api/nodes/tunnel routes keep working, completely unchanged, on the original /v1 port after a hub upgrade — unless RELAY_REQUIRE_MTLS is set. A node with an old-style credential reconnects exactly as it always has, for as long as it hasn't migrated.
relay doctorreports tls-cert: still on the plaintext tunnel for a node that has not migrated, so flip it only once no node reports that.Migration is explicit and operator-triggered
An earlier version of this wave migrated a node's transport automatically, on every relay up, over a plaintext connection — the hub's response included a private key it had generated for the node, in the clear. That had two real problems: a passive observer could capture the node's private key in transit, and an active machine-in-the-middle could answer the plaintext request itself, re-pinning the node to an attacker's CA before it ever talked to the real hub. Migration now fixes both:
| Property | How |
|---|---|
| A CSR exchange, not a key handoff | The node generates its own key pair and sends the hub only a certificate signing request; the hub signs it and returns a certificate, never a private key, in either direction. |
| Carried entirely over TLS, pinned by an out-of-band fingerprint | The request runs against the mutual-TLS node listener, not the plaintext one, trusting that connection only by a CA fingerprint the operator supplies from a fresh relay token — the same out-of-band bootstrap a first enrolment uses. |
| Opt-in | You run it yourself; nothing migrates a node on its own. |
relay token
relay up --pool https://hub:8443 --token <that token> --upgrade-tlsDo this on a network you trust: the fingerprint pin protects the exchange, but the CSR round trip is still worth doing somewhere you aren't actively worried about an adversary on the wire — the same caution you'd apply to a first enrolment.
Or rejoin from scratch with a fresh token
No different from migrating, and slightly simpler to reason about: the same command as a first join, minus the flag.
relay token
relay up --pool https://hub:8443 --token <token>This creates a new node identity rather than preserving the old row, which matters only if something outside relay keys off the node's UUID.
Migrating existing /v1 clients onto TLS
A deployment pointed at http://hub:8383 starts getting TLS on its next relay serve restart, because RELAY_LISTEN's common default (:8383) isn't a loopback bind. An existing client needs one of:
| Option | What it does |
|---|---|
| Switch to https:// and trust the hub CA | e.g. curl --cacert RELAY_STATE_DIR/tls/ca.pem https://hub:8383/v1/models, or point an SDK's custom CA-bundle option at the same PEM file. |
relay chat/relay bench's built-in fingerprint pinning | Resolves which CA fingerprint to pin instead of trusting the CA file directly — see the resolution order below. |
-k / --insecure | A stopgap while you sort out trust distribution — not recommended beyond that, since it accepts any certificate, not just this hub's. |
RELAY_V1_TLS=off | Keeps the old plaintext behaviour until every client has migrated — but only with a loopback RELAY_LISTEN once an API key is in play: relay serve refuses to start plaintext /v1 on a network-reachable address with a key configured. |
relay chat and relay bench resolve which CA fingerprint to pin in this order:
| Order | Resolves to |
|---|---|
| 1 | An explicit --ca-fingerprint flag. |
| 2 | RELAY_CA_FINGERPRINT in the environment. |
| 3 | The local daemon's /status, when it reports an RELAY_TLS_DOMAIN matching the pool URL's host — a publicly-trusted ACME certificate needs no pin. |
| 4 | The local daemon's own /status, when the pool URL names this machine's own daemon. |
| 5 | This machine's node credential (relay up's credential.json), when the pool URL names the same host as the hub it enrolled with — matched by hostname only, not port, since enrollment persists the mutual-TLS node-listen address (default 8443), never the /v1 port (default 8383) a pool URL here names. |
| 6 | Otherwise the system trust store — the right fallback for a hub with a real, publicly-trusted certificate. |
Get the fingerprint to pin with relay status on the hub — it may be pasted with or without a leading sha256: label, either way. relay doctor <https-url> runs a v1-tls check using the same order (minus the flag and local-daemon steps, since a doctor target is a remote hub by definition), reporting ok, bad ("wrong hub or machine-in-the-middle"), or skip with the same pinning hint when nothing resolves.