deploy(compose): serve device pairing via the buzz-pair-relay sidecar - #5715
Open
derekross wants to merge 4 commits into
Open
deploy(compose): serve device pairing via the buzz-pair-relay sidecar#5715derekross wants to merge 4 commits into
derekross wants to merge 4 commits into
Conversation
The Helm chart deploys buzz-pair-relay (deploy/charts/buzz/templates/ pairing-relay.yaml); the compose bundle does not. Compose deployments therefore have nothing listening on /pair, so device pairing 404s out of the box even though the binary is already in the image (Dockerfile:171). Pairing cannot be served by the main relay: a device mid-pairing holds a fresh ephemeral key that is not a relay member yet, so it is refused by the BUZZ_REQUIRE_RELAY_MEMBERSHIP auth gate. The sidecar exists because it has no auth, no persistence and no history. - compose.yml: pairing-relay service, loopback-published, TCP healthcheck. entrypoint (not command) because the image ENTRYPOINT is buzz-relay. - Caddyfile: route /pair* to the sidecar ahead of the catch-all. - compose.caddy.yml: reset the published port when Caddy fronts it. - .env.example: document BUZZ_PAIR_RELAY_PORT and BUZZ_PAIRING_RELAY_URL. Verified: `docker compose config` renders for the base and Caddy overlay, `caddy validate` passes, and a full buzz-pair source/target handshake (SAS match + payload transfer) completes against this sidecar running on a live compose deployment. Signed-off-by: Derek Ross <derekross@gmail.com>
Follow-up to the pairing-relay service, addressing the gaps found by comparing against the other open submissions for this problem. README.md gains a Device pairing section: why the handshake cannot run on the membership-gated main relay, what to set BUZZ_PAIRING_RELAY_URL to, what a bring-your-own-proxy deployment has to provide, and how to read the diagnostic — a bare 400 on /pair means the sidecar is answering, since it serves no NIP-11 document, while 404 means no route and 401 means /pair is reaching the main relay. The Caddy route becomes an explicit `path /pair /pair/*` matcher. A `/pair*` prefix (what this branch had) and `handle_path /pair*` both also capture /pairfoo, and a bare `handle /pair` misses the trailing slash; all four shapes were driven with real handshakes to confirm. BUZZ_PAIR_RELAY_PORT stays loopback-only and now says why: the sidecar has no auth and no membership check by design, so a public bind is not an acceptable default. The README documents the override and its cost. BUZZ_PAIRING_RELAY_URL is set in .env.example alongside RELAY_URL rather than commented out, matching the file's existing convention of shipping buzz.example.com placeholders. The relay picks it up through its env_file, so no entry in the relay's environment block is needed. run.sh restart also recreates pairing-relay — it shares BUZZ_IMAGE with the relay, so restarting only the relay left the sidecar on a stale image — and the help text carries the pairing summary and the curl check. Signed-off-by: Derek Ross <derekross@gmail.com>
Three corrections to the pairing sidecar defaults. BUZZ_PAIRING_RELAY_URL goes back to commented-out. A client reads the relay's NIP-11 document and prefers pairing_relay_url when present; with no value it falls back to <RELAY_URL>/pair, which is exactly the route the bundled Caddyfile serves (desktop/src-tauri/src/commands/pairing.rs, pairing_relay_from_nip11 / resolve_pairing_relay_url). NIP-43 is advertised whenever the relay has a stable key and enforces membership — both defaults here — so the fallback is reached by construction and the default install needs no edit. Shipping the value uncommented meant an operator who changes BUZZ_DOMAIN and misses this line advertises a pairing endpoint on someone else's domain, silently, where the handshake payload is a private key. BUZZ_PAIR_RELAY_PORT splits into BUZZ_PAIR_RELAY_HOST_IP + a bare port. Every other *_PORT in .env.example is a number, so an operator moving the port would have written 5001 and silently published on 0.0.0.0, losing the loopback property the sidecar depends on for safety. compose.caddy.yml gates Caddy on the sidecar with service_started rather than service_healthy. Pairing is one route; an unhealthy sidecar should not keep messaging, media and git from coming up. Signed-off-by: Derek Ross <derekross@gmail.com>
…ndition The "Checking it works" section led with a grep for pairing_relay_url in NIP-11. That field is skip_serializing_if = "Option::is_none", so on the default install — where BUZZ_PAIRING_RELAY_URL is deliberately unset — it is absent, and the first check in the README prints nothing and exits 1 on a correctly configured stack. The /pair status probe is now the primary check; the NIP-11 grep is demoted to the case where the variable is set, with empty output called out as correct otherwise. Also state the precondition behind "nothing to configure": clients only fall back to <RELAY_URL>/pair when the relay advertises NIP-43, which needs a stable relay key and BUZZ_REQUIRE_RELAY_MEMBERSHIP=true. Both are defaults here, but an operator running an open relay gets a different resolution path and does not need the sidecar at all. Signed-off-by: Derek Ross <derekross@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Mobile QR pairing fails on compose deployments: the desktop resolves the pairing relay from the main relay's NIP-11 document, or falls back to
<relay>/pair, and the compose bundle serves neither — so the phone gets a 404.Pairing cannot run on the main relay. A device mid-pairing holds a freshly generated ephemeral key that is not a relay member yet, so
BUZZ_REQUIRE_RELAY_MEMBERSHIP— the production default in.env.example— rejects it. PointingBUZZ_PAIRING_RELAY_URLat the main relay only converts the 404 into an auth rejection. Thebuzz-pair-relaysidecar exists for this: no auth, no persistence, no history.The binary already ships in the image (
Dockerfile:178) and the Helm chart already deploys it (deploy/charts/buzz/templates/pairing-relay.yaml). Helm is the only shipped deployment config that does. This adds the same thing to the compose bundle.compose.yml—pairing-relayservice.entrypoint:, notcommand:: the image ENTRYPOINT isbuzz-relay, socommand:would pass the path as an argument to the wrong binary. Helm can usecommand:because k8scommandmaps to ENTRYPOINT.BUZZ_PAIR_RELAY_HOST_IP. A same-host reverse proxy reaches it at127.0.0.1:5000;compose.caddy.ymlunpublishes it entirely.Caddyfile—@pairing path /pair /pair/*ahead of the catch-all.compose.caddy.yml— Caddy waits on the sidecar withservice_started, notservice_healthy: pairing is one route and should not be able to hold the whole site down..env.example—BUZZ_PAIRING_RELAY_URLdocumented but left unset. With no value the client falls back to<RELAY_URL>/pair, which is exactly what the Caddyfile serves, so the default install needs no edit.run.sh—restartalso recreates the sidecar, which sharesBUZZ_IMAGEwith the relay; help text carries the diagnostic.README.md— a Device pairing section: bring-your-own-proxy requirements and how to readcurl /pair(400 healthy, 404 no route, 401 hitting the main relay).Related issue
Diagnoses #5631. Same root cause as #3779, #2734, #3842, #3291.
Six open PRs already fix this, and they are all correct about the fix: #2736, #3627, #3875, #4082, #4656, #5589. The oldest has been open since 2026-07-24; none has been reviewed. This is a seventh entry in that queue, offered as a consolidation rather than a new idea, and it should be closed in favour of any of them a maintainer prefers.
Nothing in the shape of this PR is new. #2736 and #4082 already route with
path /pair /pair/*— #2736 as@pair+handleblocks, which is this PR's routing block character for character, #4082 the same matcher inside aroute {}. #3627 already publishes the sidecar on loopback only. #2736 and #3627 already leaveBUZZ_PAIRING_RELAY_URLcommented out. Every individual decision here was already made correctly by someone in this queue; what is new is that they have not been made together, and that some of them are now checked on the wire.Where the six differ, and what this PR picks:
path /pair /pair/*(fix(deploy): bundle buzz-pair-relay sidecar so mobile pairing works out of the box #2736, fix(deploy): wire mobile pairing relay into docker-compose self-host bundle #4082) is the only precise one:handle_path /pair*(fix(compose): wire buzz-pair-relay so mobile QR pairing works #3875) also captures/pairfoo, andhandle /pair(feat(deploy): add device-pairing sidecar to the compose stack #4656, fix(deploy): enable mobile pairing relay #5589) misses a trailing slash. Evidence in Testing.BUZZ_PAIRING_RELAY_URL. fix(compose): wire buzz-pair-relay so mobile QR pairing works #3875, fix(deploy): wire mobile pairing relay into docker-compose self-host bundle #4082 and fix(deploy): enable mobile pairing relay #5589 ship a literalwss://buzz.example.com/pairin.env.example. That is a footgun: the operator who editsBUZZ_DOMAINand misses this line advertises pairing on a domain they do not control, silently, and the handshake payload is a private key. Unset is already correct —pairing_relay_from_nip11prefers a configured URL and otherwise returnsLegacyPath=<RELAY_URL>/pair, which these Caddyfiles serve. fix(deploy): bundle buzz-pair-relay sidecar so mobile pairing works out of the box #2736 and fix(compose): restore mobile pairing sidecar #3627 comment it out, which is right; feat(deploy): add device-pairing sidecar to the compose stack #4656 interpolates it from${BUZZ_DOMAIN}, which cannot go stale and is equally safe.service_started. fix(deploy): bundle buzz-pair-relay sidecar so mobile pairing works out of the box #2736, fix(compose): wire buzz-pair-relay so mobile QR pairing works #3875, fix(deploy): wire mobile pairing relay into docker-compose self-host bundle #4082 and fix(deploy): enable mobile pairing relay #5589 useservice_healthy, which lets an unhealthy sidecar keep the whole site — messaging, media, git — from coming up for the sake of one route.curl /pairand the bring-your-own-proxy requirements.Two things worth fixing wherever this lands, offered as help rather than as a comparison:
bash -ec exec 3<>/dev/tcp/127.0.0.1:5000— a colon where bash needs a slash. Run verbatim it givesNo such file or directory, so the container never reports healthy; combined with that PR'scondition: service_healthy, Caddy never starts. One character. Separately, itsBUZZ_PAIRING_RELAY_URL: ${BUZZ_PAIRING_RELAY_URL:?set BUZZ_PAIRING_RELAY_URL}makes the variable mandatory, sodocker compose configfails for every existing.env.--profile pairing, so a default deploy still 404s — worth reconsidering if that PR is the one that lands, since the bug is that pairing does not work out of the box.Testing
Verified on a live compose deployment (Docker 24.0.5, Compose 2.20.2) and a throwaway stack on the same host.
In production. This sidecar shape has served device pairing on a real relay since 2026-08-12 — behind nginx rather than Caddy — with a phone paired successfully through it.
Handshake through this PR's Caddyfile, throwaway stack,
buzz-pair sourceandbuzz-pair targetfromcrates/buzz-pairing-cli:Routing, same stack, with a 404-returning stub behind the catch-all:
400 = reached the sidecar, 404 = reached the stub. 400 is the correct answer to a non-WebSocket request: the sidecar serves no NIP-11 document and rejects anything that is not an upgrade (
crates/buzz-pair-relay/src/lib.rs).Config.
docker compose configrc=0 for base,+compose.caddy.ymland+compose.dev.yml;caddy validatereturns Valid configuration;bash -n run.shclean. Rendered output confirmsentrypoint: [/usr/local/bin/buzz-pair-relay], the sidecar bound to127.0.0.1:5000, its port removed under the Caddy overlay, and noBUZZ_PAIRING_RELAY_URLon the relay.Not tested: TLS with a real certificate — the throwaway Caddy ran on plain HTTP with no DNS. Caddy terminates TLS above the routing layer, so this should not affect
/pairmatching or the upgrade, but it is unproven here.No UI change.