Wire Protocol
Relayly is a protocol, not just a server. This page is a practical summary; the
normative spec lives in docs/PROTOCOL.md in the repository, and every official SDK is verified against it by a required
cross-language interop test matrix.
Connecting
Clients connect to the relay over a standard WebSocket, authenticating at the HTTP layer via query params. There is no in-band auth frame and no client-to-server cryptographic handshake:
ws://<host>:<port>/ws?device_id=<uuid>&token=<device-token> On a successful upgrade, the server immediately sends a welcome control message.
Two kinds of frames
| Frame type | Carries | Read by |
|---|---|---|
| Text | JSON control messages | Client ↔ server |
| Binary | An opaque E2E envelope | Device ↔ device only |
The relay parses text frames to authenticate devices and mediate pairing. Binary frames are relayed verbatim, byte for byte, between the two paired devices. The server never parses, decrypts, or modifies them, it has no key material capable of doing so.
Pairing
- Device A sends
pair_request; the server replies with apair_code(a 6-digit code, single-use, expires after 5 minutes). - The code travels out of band (spoken aloud, typed in, scanned as a QR code).
- Device B sends
pair_acceptwith that code. - The server links the two devices and sends
pair_completeto both, including each side’s previously announced static key so the peer can cross-check it once the handshake below completes.
Encryption is device-to-device, not client-to-server
Once paired, the two devices run a three-message Noise XX handshake directly with each other. Each binary frame carries a 1-byte envelope tag
(0x01 handshake, 0x02 transport) and is relayed by the server exactly as
received:
| Message | Direction | Content |
|---|---|---|
| 1 | Device A → Device B (via relay) | Ephemeral public key |
| 2 | Device B → Device A (via relay) | Ephemeral key + encrypted static key |
| 3 | Device A → Device B (via relay) | Encrypted static key |
After message 3, both devices hold a shared transport cipher. Every subsequent binary frame is Noise ciphertext the relay forwards without inspecting, it is never a party to the handshake and never holds a session key.
Reconnects and rekeying
A dropped connection can desync per-direction nonce counters, so Relayly re-handshakes proactively on every reconnect rather than waiting for a garbled frame. Whichever device’s ID is lexicographically smaller initiates the fresh handshake; the other device keeps its existing session working until the replacement completes and authenticates (make-before-break), so a rekey attempt never has a chance to interrupt a healthy connection.
Key locking
Two independent layers guard against a compromised relay:
- Client-side pinning (the real boundary): each device pins the peer’s static key the first time a handshake with it completes, and hard-fails on any later handshake presenting a different key. This is enforced entirely between the two devices; the server plays no part in it.
- Server-side announced-key locking (defense in depth): the server separately
records the first key each device announces and rejects a later announcement
that differs, catching a spoofed
pair_completeeven before layer 1 would.
CLI Reference
| Command | Description |
|---|---|
relayly start | Start relay + admin servers |
relayly start --config path/to/relayly.yaml | Use custom config |
relayly pair <name> | Register a device, print its device ID and token |
relayly pair <name> --no-qr | Print token only, no QR code |
relayly link <id-1> <id-2> | Pair two already-registered devices directly, without an in-band code |
relayly status | Show connected devices + uptime |
relayly status --format=json | Machine-readable output |