Quadlets captured from the live units on bob, with the real immutable tags
(0.1.44-ge06652a) rather than a placeholder, plus the hanzalova vhost that
mounts the relay on /relay-api/ of the existing name.
Verified end to end: /relay-api/health returns ok through the prefix, the
relay's nested /v1 routes answer 401 rather than 404 (so the strip reaches
them and they are genuinely protected), the remote's own /v1 is unaffected,
and the served SPA bundle now carries the origin-relative /relay-api base.
The vhost is committed because it is half the design: the relay only works
on a path because nginx strips the prefix, and a future kanban.l4ir.net
needs exactly this location block and nothing else.
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.
These describe how this repo's image is run; they belong with it rather
than in the third-party image repo. Captured from the live units on bob,
with the postgres password redacted out of the DSN.
Records the two things that are easy to get wrong and expensive to
rediscover: the db -> server -> electric ordering (the server's migrations
create the role and publication Electric attaches to, and Electric's
Requires= means a server restart stops it without bringing it back), and
the firewalld/aardvark interaction that presents as an auth failure.
Image= is a pinned immutable tag and AutoUpdate=registry is gone:
podman-auto-update.timer is enabled on bob, so a floating tag would let
the timer decide which build is running.