Pairing needs a third service we never deployed. Hosts run the ordinary
local vibe-kanban server (the `server` crate, shipped as `npx
vibe-kanban`); it registers with a relay over a websocket control channel,
and the browser reaches the host's local API through that relay over
WebRTC. The remote server we deploy contains none of it -- its hosts.rs is
a single read-only GET /hosts -- so the UI could list hosts and never pair
one.
Mount it on a path of the existing name (https://kanban.internal/relay-api/)
rather than giving it a domain. Every relay URL on both sides is composed
as {base}/v1/..., the host turns the base into wss:// by stripping only the
scheme, and RelayServerConfig is just database_url/listen_addr/jwt_secret
with no notion of its own public address and no redirects -- so a prefix is
invisible to it. That buys same-origin (no CORS), one less certificate and
one less renewal timer, and when kanban.l4ir.net lands it inherits the
relay by copying one nginx location block.
For that second hostname to work from the SAME build, the SPA's relay base
is origin-relative ("/relay-api") rather than an absolute URL baked at
build time. Two consequences:
- The previous empty value was not "relay disabled", as the comment
inherited from lair/containers claimed. Empty makes Bootstrap.tsx fall
back to window.location.origin, aiming relay calls at the remote API,
which does not serve them. Comment corrected.
- fetch() paths take a relative base fine (plain concatenation), but
relayHostApi built its websocket by string-replacing http->ws, which
silently leaves a relative URL that the WebSocket constructor rejects.
It now uses openBrowserWebSocket, already imported in that file, which
resolves wss://, https:// and relative alike.
The relay shares the remote's JWT secret -- that is how it trusts tokens
the remote issued -- so both read /etc/vibe-kanban/env.