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 typeCarriesRead by
TextJSON control messagesClient ↔ server
BinaryAn opaque E2E envelopeDevice ↔ 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

  1. Device A sends pair_request; the server replies with a pair_code (a 6-digit code, single-use, expires after 5 minutes).
  2. The code travels out of band (spoken aloud, typed in, scanned as a QR code).
  3. Device B sends pair_accept with that code.
  4. The server links the two devices and sends pair_complete to 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:

MessageDirectionContent
1Device A → Device B (via relay)Ephemeral public key
2Device B → Device A (via relay)Ephemeral key + encrypted static key
3Device 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:

  1. 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.
  2. 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_complete even before layer 1 would.

CLI Reference

CommandDescription
relayly startStart relay + admin servers
relayly start --config path/to/relayly.yamlUse custom config
relayly pair <name>Register a device, print its device ID and token
relayly pair <name> --no-qrPrint token only, no QR code
relayly link <id-1> <id-2>Pair two already-registered devices directly, without an in-band code
relayly statusShow connected devices + uptime
relayly status --format=jsonMachine-readable output