Buzz Relay Self-Host (+ Hermes gateway)
Repeatable workflow for Block Buzz — a Nostr-based human+agent workspace relay — and optional Hermes Agent native gateway integration.
All examples use placeholders only (no real hostnames, IPs, keys, channel IDs, or operator identities). Keep site inventory in a private runbook outside this skill.
What it is
- One Rust service: WebSocket relay + REST + web UI.
- Production deps: Postgres, Redis, MinIO (S3), git data volume.
- Official prod bundle: repo
deploy/compose/(not the root devdocker-compose.yml). - Image:
ghcr.io/block/buzz— pin a digest/tag after first healthy boot.
Placement policy
- Run the relay in a dedicated utility guest (VM/LXC), not on the bare hypervisor host.
- Publish only the relay HTTP port (default 3000) to the edge proxy.
- Never expose Postgres / Redis / MinIO ports publicly.
- Prefer a non-root operator account on the guest (
opsor similar) with docker group access.
Suggested guest baseline (adjust to load): 4 vCPU, 8G RAM, 80G disk, static LAN IP, auto-start on boot.
Critical invariants
RELAY_URLis the community identity
Must match client URL byte-for-byte (scheme + host + port).
Public example:wss://buzz.example.com
Changing it later seeds a new empty community. Set the final public URL before real members/data.
For pure-local tests prefer127.0.0.1overlocalhost(agents often canonicalize localhost).Two Nostr identities at bootstrap
BUZZ_RELAY_PRIVATE_KEY— relay signing secret (64-hex).RELAY_OWNER_PUBKEY— owner public key (64-hex), not nsec.
Generate:
docker run --rm --entrypoint /usr/local/bin/buzz-admin \ ghcr.io/block/buzz:main generate-keyBack up like TLS keys. Owner private key stays offline / in a secrets directory — not pasted into chat logs.
Closed membership by default
Typical flags:BUZZ_REQUIRE_RELAY_MEMBERSHIP=trueand auth token requirements enabled.
Add members:./run.sh add-member <npub-or-hex> --role member|admin ./run.sh list-membersPublic URL env vars must agree
BUZZ_DOMAIN=buzz.example.com RELAY_URL=wss://buzz.example.com BUZZ_MEDIA_BASE_URL=https://buzz.example.com/media BUZZ_MEDIA_SERVER_DOMAIN=buzz.example.com BUZZ_CORS_ORIGINS=https://buzz.example.com BUZZ_AUTO_MIGRATE=true # first boot / fresh DB
Bring-up (guest)
git clone --depth 1 https://github.com/block/buzz.git
cd buzz/deploy/compose
cp .env.example .env
# Replace every CHANGE_ME_RANDOM with: openssl rand -hex 32
# Set relay private key + owner pubkey + public URL vars by hand
./run.sh start # do NOT enable bundled Caddy when an external proxy terminates TLS
curl -fsS http://127.0.0.1:3000/_liveness # expect: ok
./run.sh status
Pin image after success:
BUZZ_IMAGE=ghcr.io/block/buzz@sha256:<digest>
Backup checklist: ./run.sh backup-hint plus .env, owner key, Postgres, MinIO, git data volume.
Reverse proxy / edge (Pangolin, Caddy, nginx, Traefik, …)
Do not stack the compose Caddy/Let’s Encrypt path when an external edge already terminates TLS.
| Setting | Value |
|---|---|
| Hostname | buzz.example.com |
| Backend target | http://<guest-lan-ip>:3000 |
| WebSockets | ON |
| Health probe | /_liveness or /_readiness (prefer these over bare HTML /) |
| Path rewrite | none (root → root) |
Auth wall: keep it OFF for the relay
Buzz Desktop, Hermes gateway, and other Nostr clients authenticate with keypairs (NIP-42), not browser cookies. Edge SSO / PIN / shared password breaks them.
Browser-only apps (e.g. a Hermes dashboard) may keep SSO. The Buzz relay resource must not.
Verify unauthenticated edge:
curl -fsS https://buzz.example.com/_liveness
curl -fsS -H 'Accept: application/nostr+json' https://buzz.example.com/
# Expect NIP-11 JSON — not an HTML login page
Clients join Desktop with wss://buzz.example.com (not https://).
WebSocket check note
curl -sS -D- -o/dev/null --http1.1 \
-H 'Connection: Upgrade' -H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
https://buzz.example.com/
# Expect HTTP 101. Unauthenticated real clients may still get NIP-42 AUTH — expected.
curl without --http1.1 may return 200 NIP-11 JSON; that alone is not proof WebSockets are broken.
Desktop vs CLI
| Binary / package | Role | URL scheme |
|---|---|---|
buzz on PATH |
Agent/script CLI (JSON I/O) | https:// |
| Buzz Desktop | Human GUI | Join with wss:// |
Bare buzz printing JSON help is normal — that is not the GUI.
Onboarding humans (Desktop)
- Install Buzz Desktop (not only the CLI package).
- Identity:
- Daily path: generate key in GUI → copy public npub/hex → guest
./run.sh add-member <pubkey> --role admin|member→ joinwss://…. - Owner path: import owner secret from your offline secrets store (never paste private keys into agent chat).
- Daily path: generate key in GUI → copy public npub/hex → guest
- Join URL exactly:
wss://buzz.example.com(no path, no trailing slash, nothttps://). - "Need invite" on a closed relay = pubkey not on roster →
add-member, then retry join / restart Desktop.
Mobile pairing (Desktop QR → phone)
Class of failure: Desktop Settings → Mobile shows WebSocket 404 on /pair while the community relay is otherwise healthy (NIP-11 lists NIP-43, no pairing_relay_url).
| Truth | Detail |
|---|---|
| Cause | Main buzz-relay does not serve pairing; need buzz-pair-relay sidecar + advertisement |
| QR source | Buzz Desktop only (NIP-AB). Never mint QR from Hermes/agent/owner secrets |
| Hermes | Unrelated to human phone pairing |
Preferred fix (self-host): pair sidecar + path-proxy /pair on the same host port your reverse proxy already targets, plus:
BUZZ_PAIRING_RELAY_URL=wss://<buzz-fqdn>/pair
Quick workaround: BUZZ_PAIRING_RELAY_URL=wss://pairing.buzz.xyz on the main relay only.
Full recipe, compose fragment, Caddyfile, verify script:
references/mobile-pairing.mdtemplates/compose-pair-edge.fragment.ymltemplates/Caddyfile.pairscripts/verify-pair-endpoint.sh https://<buzz-fqdn>
Always-on ACP agents (buzz-acp bridge)
For ACP agents that stay online without Buzz Desktop (Hermes ACP, Copilot CLI ACP, goose, …):
- Mint a dedicated agent key (not gateway adapter key, not human Desktop key).
add-member+ join channels + descriptiveset-profile/ avatar.- Run
buzz-acpunder systemd withBUZZ_ACP_AGENT_COMMAND/ARGS/ optionalBUZZ_ACP_MODEL. - Prefer
BUZZ_ACP_RESPOND_TO=owner-only(headless tools often auto-approve).
Discover models: buzz-acp models (catalog depends on the ACP backend — Copilot ≠ Nous).
Full recipe: references/buzz-acp-bridge.md.
Native gateway (mode ③) remains the deepest Hermes integration for memory/cron/multi-platform.
Hermes integration (after relay healthy)
| Mode | When |
|---|---|
| ① Desktop runtime | Laptop: Settings → Runtimes → Hermes; prefer owner-only talk gates. |
② buzz-acp bridge |
Buzz owns transport; server bridge → hermes acp. |
| ③ Native gateway | Preferred for always-on agents: host hermes-gateway as a messaging platform. |
Details for mode ③: references/hermes-gateway.md.
Mode ③ short checklist
- Hermes install must include
plugins/platforms/buzz/(update Hermes ifConnecting to buzz…never appears). - Install
buzzCLI on the gateway host. Public relay images often shipbuzz-admin/buzz-relayonly — build CLI if needed:cargo build --release --locked -p buzz-cli # install binary onto gateway host PATH, e.g. /usr/local/bin/buzz - Mint a dedicated agent keypair — not the human Desktop key, not
BUZZ_RELAY_PRIVATE_KEY. ./run.sh add-member <agent-pub> --role member; join watched channels; set profile/avatar (references/agent-profile-avatar.md).- Put secrets in
~/.hermes/.envand ensure the gateway unit loads that file (see reference). - Allowlist operators with hex and npub (deny logs show hex).
- Restart gateway outside an in-gateway agent tool session (self-restart is often blocked).
- Verify log line:
Buzz: connected … via websocket/✓ buzz connected.
Symptom: eyes / read reaction, no reply
- Search gateway logs for
Unauthorized user. - If present: missing hex on allowlist, gateway process missing env from
.env, or process not fully recycled after env change. - Else inspect model/provider errors after a successful authorize.
Default recommendation: only listed users until you deliberately open the community.
Operations
cd /path/to/buzz/deploy/compose
./run.sh status
./run.sh upgrade # after pinning image digest
./run.sh list-members
./run.sh add-member <npub-or-hex> --role admin
./run.sh backup-hint
Pitfalls
Mobile pairing QR 404 on
/pair→ mainbuzz-relaydoes not serve pairing. Runbuzz-pair-relaysidecar, path-proxy/pair(or subdomain), setBUZZ_PAIRING_RELAY_URL=wss://<fqdn>/pairso NIP-11 advertisespairing_relay_url. Verify WS upgrade returns 101. Workaround:wss://pairing.buzz.xyz. QR is still generated in Desktop, not by Hermes/CLI. Seereferences/mobile-pairing.md.Reuse gateway agent key for buzz-acp → identity lock / two harnesses fighting one pubkey. Mint a new key per always-on ACP agent.
Expect Nous-only model IDs on Copilot ACP → catalogs differ; query
buzz-acp modelsfor the configured agent binary.Avatar upload 422 metadata → strip PNG metadata (Pillow re-encode) before
buzz upload file. Seereferences/agent-profile-avatar.md.Human Desktop key reused as agent identity →
BUZZ_PRIVATE_KEYmust be a dedicated agent secret, not the human Desktop nsec. Allowlist = human public hex/npub (who may talk). Verify withbuzz users get(agent name/pubkey, not human). Seereferences/hermes-gateway.md.buzzCLI mistaken for Desktop → JSON usage help; install/open the GUI package.Temporary LAN
ws://then publicwss://→ different communities; set publicRELAY_URLbefore real onboarding.Caddy + external TLS double-termination → skip compose Caddy behind the edge proxy.
Edge SSO on the relay → breaks Nostr clients.
Health on
/with SSO → false unhealthy; use/_liveness.curl WS without HTTP/1.1 → 200 JSON not 101; not always a failure.
"Need invite" → closed roster;
add-memberthe public key.Unpinned
:mainimage → pin digest after healthy boot.Compose dumps in chat → leak DB/MinIO secrets; redact.
Human key as Hermes agent → dedicated agent keypair (adapter scopes lock per relay+pubkey).
CLI missing from image → build
-p buzz-cli.Hermes without buzz plugin → update install.
Eyes / no reply → hex allowlist + load
.envinto gateway unit + full process recycle.Gateway restart from agent tools → often blocked; use an external shell.
Site-specific inventory (do not commit here)
Keep a private runbook next to the deploy with:
- Guest VMID/IP, FQDN, image digest
- Owner/agent public keys only (secrets stay in
chmod 600files) - Channel UUIDs and home channel
- Edge proxy resource IDs
References
references/mobile-pairing.md— Desktop QR //pair404 fix (sidecar + NIP-11)references/buzz-acp-bridge.md— always-on ACP agents (Hermes ACP, Copilot ACP, …)references/agent-profile-avatar.md— agent display name + Blossom avatar (+ metadata strip)templates/+scripts/verify-pair-endpoint.sh+scripts/set-agent-avatar.shreferences/hermes-gateway.md— native Hermes gateway wiring (placeholders only)- Upstream blog: https://engineering.block.xyz/blog/run-your-own-buzz-relay
- Mobile pairing background: https://engineering.block.xyz/blog/a-buzz-on-your-phone
- Hermes messaging: https://hermes-agent.nousresearch.com/docs/user-guide/messaging/buzz
- Hermes Buzz modes: https://hermes-agent.nousresearch.com/docs/integrations/buzz
- buzz-acp: https://github.com/block/buzz/tree/main/crates/buzz-acp
- Copilot ACP: https://docs.github.com/en/copilot/reference/copilot-cli-reference/acp-server
- Repo: https://github.com/block/buzz
- Issue class: https://github.com/block/buzz/issues/2734