A NexaPipe instance can now answer the question its logs could not: how is this instance doing. Health, metrics and live state sit behind a loopback listener you switch on.
Pick a build below if the guess is wrong.
Client app first, then the server. Nothing needs an account, and nothing is gated.
Desktop bundles are signed with the Tauri updater key; the in-app updater reads
latest.json in the same release.
Only arm64-v8a is shipped. iOS is not a supported platform — see the
roadmap.
Each archive holds the nexapipe binary, the client cdylib
for embedding, config.toml.2fa.example, the README and the licence.
No package manager yet — these are the release assets, straight from the tag.
# Linux x86_64, static binary
curl -LO https://github.com/open-nexa/nexapipe/releases/download/v0.3.0/nexapipe-0.3.0-x86_64-unknown-linux-musl.tar.gz
tar -xzf nexapipe-0.3.0-x86_64-unknown-linux-musl.tar.gz
cd nexapipe-0.3.0-x86_64-unknown-linux-musl && ./nexapipe --version
# macOS, Apple silicon
curl -LO https://github.com/open-nexa/nexapipe/releases/download/v0.3.0/nexapipe-0.3.0-aarch64-apple-darwin.tar.gz
tar -xzf nexapipe-0.3.0-aarch64-apple-darwin.tar.gz
cd nexapipe-0.3.0-aarch64-apple-darwin && ./nexapipe --version
# Verify a download against the release checksums
curl -LO https://github.com/open-nexa/nexapipe/releases/download/v0.3.0/SHA256SUMS.txt
sha256sum -c --ignore-missing SHA256SUMS.txt
All assets: SHA256SUMS.txt.
0.3.0 follows 0.2.0, released 28 September 2026. Nothing in it breaks a working
configuration: with no [admin] section the server binds nothing extra and behaves
exactly as 0.2.0 did.
[admin] listen_addr is set. It forwards nothing: it answers questions about this instance.GET /healthz returns 200 ok for as long as the process is serving — unauthenticated, and nothing else.GET /metrics, behind [metrics] enabled = true, serves Prometheus text format. It is written by hand, so no prometheus dependency was added and the lockfile is untouched. When metrics are off the path is a 404, so a scraper can tell "disabled" from "no traffic yet".GET /v1/{status,routes,clients,connections,health}, behind a bearer token that the server generates into <config>.admin-token on first start. There is deliberately no token key in the config: a credential in the file you edit, copy and commit is the thing this design avoids. No token means 503, not an open door.nexapipe status reads those same endpoints from the command line (--json for the raw shape), so the running server stays the single source of truth about its own state.x-request-id, so a bad answer can be reported with the line that produced it. The same id sits on a request span, with the method, URI and status filled in as they become known.| Metric | Meaning |
|---|---|
| nexapipe_connections_total nexapipe_connections_active | Connections opened since start, and open right now. |
| nexapipe_connections_by_path | Open connections by path kind: direct (hole punching succeeded), relayed, unknown. Read from iroh rather than guessed from a socket address, so "nothing to rent" finally becomes a number. |
| nexapipe_requests_total | Requests answered, by status class. |
| nexapipe_request_duration_ms_total | Milliseconds answering requests, summed; divide by requests_total for a mean. |
| nexapipe_l4_flows_total | Finished TCP/UDP flows by protocol and status. Flows are counted separately from requests on purpose: a flow is a tunnel a client holds open as long as it likes. |
| nexapipe_backends_up nexapipe_backends_down | Backends in rotation, and backends health checks took out. Read from the live pools at scrape time. |
| nexapipe_in_flight nexapipe_build_info | Connections a shutdown waits for; one series identifying the build. |
# config.toml [admin] listen_addr = "127.0.0.1:9090" [metrics] enabled = true # then curl -s http://127.0.0.1:9090/healthz curl -s http://127.0.0.1:9090/metrics | grep connections_by_path curl -s -H "Authorization: Bearer $(cat config.toml.admin-token)" \ http://127.0.0.1:9090/v1/status | jq nexapipe status --json
otpauth export, and any change to the
relay configuration now go through Android's own confirmation — a biometric where the
device has one, the screen-lock credential otherwise. SecretStore already sealed
them at rest; what was missing was anything asking before handing them back.BiometricPrompt itself has been compiled, unit-tested and linted,
but not watched running. If a prompt misbehaves on your device, that is the part to look at
first — and the desktop app has no such gate yet; it is planned for 0.4.
/healthz, /metrics, the token-guarded /v1/* endpoints, and nexapipe status.request tracing span that also carries the method, URI and status.docs/iroh-boundaries.md — what still touches third-party infrastructure whatever relay_mode says. The two READMEs now point at it instead of repeating a caveat.CHANGELOG.md. 0.2.0 shipped without one, so its entry is retrospective; 0.3.0 is the first release written into it as it happened.id=<hex>. It is appended rather than inserted, so every field before it keeps its column and a script that splits on spaces finds the host, the status and the duration where it did.x-request-id. A backend that sends its own is dropped rather than duplicated, so there is one id to quote and it is ours. L4 flows and TLS passthrough are tunnels, not requests: they get an id on the access line and the span, and have no HTTP response to put a header on.^1.0.1, not pinned, and the lockfile resolves 1.2.0. docs/iroh-boundaries.md states it correctly and both READMEs defer to it.default_backend was removed in 0.2.0 and the server refuses to start on a config
that still names it — refusing is the point, since ignoring it would quietly turn
forwarding into 404s. Replace it with a catch-all route, host_pattern = "*".
[admin] section the auxiliary listener is not bound, and no extra port is held open.latest.json.