uxnan-relay
v0.0.1-alpha.20260627
Published
Uxnan Relay — forwards opaque E2EE envelopes between the mobile app and the bridge by sessionId.
Readme
uxnan-relay
A small, stateless WebSocket relay that forwards opaque E2EE envelopes between
the Uxnan mobile app and the bridge when
the two aren't on the same network. It only ever sees encrypted frames — never
plaintext, keys, code, or diffs. The envelope-forwarding path is stateless; the
optional push fallback persists a small token/dedupe file
(~/.uxnan/relay-state.json).
Status: alpha-functional — and optional / self-hosted. The product is bridge-first (LAN-direct and Tailscale-direct need zero hosting and zero credentials); the relay is just the hosted off-LAN fallback for people who want to run their own. Push notifications are sent by the bridge directly now — the relay's
/push/*endpoints stay only as a fallback. What's built and what's left is inFOR-DEV.md; history inCHANGELOG.md.
mac/iphoneare ROLES, not platforms.mac= the PC/bridge side (runs on Windows, macOS or Linux);iphone= the mobile app side (Android or iOS). The names come from the protocol spec and are fixed by the wire contract with the mobile app — they do not restrict the operating system.
When you actually need it
Most of the time, you do not. When your phone and PC share a network — the same Wi-Fi, or a Tailscale tailnet — the app reaches the bridge directly, with no relay, no hosting, and no credentials. The relay exists for the one case the direct paths cannot cover: reaching your PC from outside that network. In that case you self-host this relay, and it simply shuttles sealed envelopes between the two sides. Because everything is already end-to-end encrypted, the relay is a dumb pipe by design — it can route or drop traffic, but it can never read it.
sequenceDiagram
participant P as 📱 iphone (app)
participant R as 🔁 relay
participant B as 🌉 mac (bridge)
B->>R: connect (x-role: mac, x-session-id)
P->>R: connect (x-role: iphone, x-session-id)
Note over R: pair the two sockets sharing a sessionId
P->>R: sealed E2EE envelope
R->>B: same envelope, forwarded unchanged
B->>R: sealed E2EE envelope
R->>P: same envelope, forwarded unchanged
Note over R: never sees plaintext, keys, code, or diffsRun
uxnan-relay 8787 # or: RELAY_PORT=8787 uxnan-relayProtocol
A client connects via WebSocket presenting:
| Header | Query fallback | Values |
|---|---|---|
| x-role | ?role= | mac (bridge) or iphone (app) |
| x-session-id | ?sessionId= | the shared session id |
The relay pairs the mac and iphone sockets that share a sessionId and
forwards every frame from one to the other unchanged. GET /health returns
{"ok":true}. The full cross-component spec is
architecture/02a-system-architecture.md §5.10.
Docs
See docs/: deployment & hosting (LAN-only vs
Cloudflare Tunnel / Fly.io / Workers) · testing.
Push notifications are bridge-first now (the relay is only an optional
delivery fallback) — see
bridge/docs/push-notifications.md.
Develop
# from the repo root (npm workspaces):
npm run build && npm testRequires Node ≥ 18. ESM-only. The relay consumes
@uxnan/shared for the JSON-RPC envelope types; the
bridge-side relay-e2e.test.ts exercises the full end-to-end (relay + bridge + a
fake phone over a real WebSocket).
