- IRIS_PUSH_BACKEND now defaults to ntfy (keeps push metadata on your own infrastructure); FCM is opt-in via IRIS_PUSH_BACKEND=fcm - build_push_backend(): ntfy for empty/unknown names, FCM only on explicit 'fcm' - gateway setup: warn when FCM is chosen (metadata routed via Google's servers) - README: privacy note + dedicated push section; new docs/playstore-listing.md with the FCM/ntfy privacy note for the Play Store listing - docs: 00/02/03/08/12/16 + setup.md updated to ntfy-default wording - tests: default-backend assertion updated (86/86 pass)
7.1 KiB
Setup — Pairing a Device
User-facing guide: get a phone or desktop talking to your hermes gateway in
under 10 minutes. Design rationale lives in the numbered docs
(09-pairing-security.md,
08-push.md, 12-toolchain.md); this page is
just the steps.
Prerequisites
| Where | You need |
|---|---|
| Gateway host | hermes installed with its venv (cd hermes-agent && uv sync, see 12-toolchain.md §12.4) |
| Android build machine | JDK 17, Android SDK with ANDROID_HOME set (or app/local.properties), ADB with a connected device |
| Desktop build machine | JDK 17 only |
Gradle needs no system install — both apps use the project wrapper
(./gradlew). First-time machine setup: 12-toolchain.md.
1. Gateway setup (on the gateway host)
Install the plugin into the live hermes home (dev: a symlink from the monorepo root):
mkdir -p ~/.hermes/plugins
ln -s "$PWD/gateway-plugin" ~/.hermes/plugins/iris
hermes gateway status # should list "iris"
Run the interactive setup:
hermes gateway setup
What it does:
- Generates
IRIS_TOKEN(64 hex chars) if none exists and stores it in~/.hermes/.env(it prints the token once, at generation). - Prompts for the WS bind host (default
127.0.0.1), port (default8790), and push backend (ntfyorfcm, defaultntfy); warns whenfcmis chosen (push metadata via Google's servers). - Prints the pairing payload (a QR-encodable
iris://pair?host=…&port=…&token=…string), a scannable QR of that payload, and the server URL (ws://<host>:8790/ws).
Then start the gateway:
hermes gateway # or: hermes gateway restart after config changes
Note: the default bind host
127.0.0.1only accepts connections from the gateway host itself (e.g. a desktop app on the same machine). For a phone on the LAN, re-runhermes gateway setup(or edit~/.hermes/.env) and setIRIS_WS_HOSTto the host's LAN IP (e.g.192.168.1.10).
2. Android app
Build and install (ADB device connected):
cd app
./gradlew :androidApp:installDebug
First run opens the Connect screen:
- Server URL —
ws://<gateway-ip>:8790/ws(the URL printed byhermes gateway setup; use the LAN IP, not127.0.0.1, from a phone). - Pairing token — from the
hermes gateway setupoutput, orgrep IRIS_TOKEN ~/.hermes/.envon the gateway host. - Test & Connect — performs a real
hello(the auth leg), then saves the pairing and connects.
Scan QR (Android): the Connect screen has a Scan QR button (CameraX + ML Kit) that reads the QR printed by
hermes gateway setupand pre-fills the URL + token. Desktop has no camera, so it uses manual entry. Aniris://pairdeep link (from any scanner) pre-fills the same way.
3. Desktop app
cd app
./gradlew :desktopApp:run # dev run
./gradlew :desktopApp:jpackage # native app-image (bundles the JRE)
Pairing is the same Connect screen (URL + token); the token is stored in the OS keyring (with an encrypted-file fallback). Desktop push is tray icon + OS notifications (no FCM).
Known issue: on Linux with JDK 17 the jpackage launcher prints a non-fatal
pure virtual method calledwarning (JDK-8348560, a jpackage/Linux launcher bug). The app runs and connects regardless.
4. Push notifications
Push wakes a backgrounded/offline device; on reconnect the app syncs the outbox, so nothing is lost. Push fires when the device is offline, plus for high-priority events (approvals, clarifies, cron) even when a device is live.
ntfy (default; zero-config)
IRIS_PUSH_BACKEND=ntfy # the default — can be left unset
- The device generates its own topic automatically (no
NTFY_TOPICneeded); the server publishes to it. NTFY_SERVER_URLdefaults tohttps://ntfy.sh. Self-hosted ntfy is recommended — the public ntfy.sh SSE endpoint is flaky (it has served its web UI instead of the stream), while a self-hosted instance gives reliable SSE. For a real trust boundary use a private topic +NTFY_AUTH_TOKEN.- Privacy: ntfy keeps push metadata (title, topic) on your own infrastructure — this is the backend for truly private communication.
What you see: a low-priority foreground "ntfy listener" notification while the app is off; incoming pushes trigger a silent sync.
FCM (opt-in; needs a Firebase project)
Privacy note: FCM push metadata (notification title, device token) is routed through Google's servers. If you want truly private communication, use ntfy (self-hosted) instead — it is the default.
- Create a Firebase project (console.firebase.google.com) and add an Android
app with the app's applicationId; download
google-services.jsonintoapp/androidApp/. - Create a service account (Project settings → Service accounts → Generate new
private key) and store the JSON path in
~/.hermes/.env:IRIS_FCM_SERVICE_ACCOUNT=/path/to/service-account.json. - Set
IRIS_PUSH_BACKEND=fcm.
Without a Firebase project the FCM path is inert (the app's FCM service does nothing) — use ntfy (the default), or add Firebase later.
5. Remote access
- Tailscale / WireGuard (recommended): the gateway gets a stable tailnet IP;
the app connects to
ws://<tailnet-ip>:8790/ws. No public exposure. - Reverse proxy / tunnel (Caddy, Cloudflare Tunnel, ngrok): terminate TLS at
the edge, forward the WebSocket to
127.0.0.1:8790. - WSS: set
IRIS_WS_CERT/IRIS_WS_KEY(paths, in~/.hermes/.env) and the server serveswss://instead ofws://.
Honest limitation: the app has no certificate pinning yet, so self-signed certs won't work — remote access requires CA-signed WSS for now. Plain
ws://on a trusted LAN (or inside Tailscale) stays the default.
6. Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
auth failed / error {code:"auth"} on connect |
Wrong token. Check IRIS_TOKEN in ~/.hermes/.env on the gateway host (setup prints it only when it generates it). |
| Connection refused | Gateway not running (hermes gateway status); wrong URL (port 8790, path /ws, LAN IP instead of 127.0.0.1 from a phone); firewall blocking the port. |
| Push not arriving | Backend not configured (gateway log: push backend … not configured); app backgrounded with no working backend; ntfy.sh SSE flakiness — use a self-hosted ntfy. |
| Desktop jpackage launcher warning | Non-fatal (JDK-8348560 on Linux JDK 17); the app runs and connects regardless. |
Smoke test without the app (from the gateway host):
python - <<'PY'
import asyncio, json, websockets
async def main():
async with websockets.connect("ws://127.0.0.1:8790/ws") as ws:
await ws.send(json.dumps({"v":1,"type":"hello","payload":{
"token":"<IRIS_TOKEN>","device_id":"test","device_name":"probe",
"caps":{"min_protocol":1}}}))
print("recv:", await ws.recv())
asyncio.run(main())
PY
Expect a hello.ack. If you get error {code:"auth"}, the token/host/port is
wrong.