Skip to Content
DocumentationProtocolOverview

Protocol

The wire protocol is the contract between a node and a backend. It is the only thing the node requires of a server, which is why it is node-owned, versioned, and published as a standalone specification — any backend, in any language, can implement it without reading the Android source.

Design properties

  • Transport-agnostic. SMS today; MMS, USSD and voice fit the same envelope with no redesign.
  • Platform-agnostic. Android today; a Linux or Windows GSM-modem node is a different program speaking this same protocol, and the backend cannot tell the difference.
  • Versioned from v1. Adding a version field later would be a breaking migration across every node and server simultaneously.
  • JSON over WSS for the live session, HTTPS for enrolment and fallback.

Nothing in the protocol prescribes a backend technology. Endpoints are runtime configuration set at pairing, never compiled into the node.

The four frame kinds

KindDirectionPurpose
commandbackend → nodeAsk the node to do something
eventnode → backendReport what happened
ackbothConfirm receipt of a specific frame
controlbothresync, version_negotiate, application-level ping/pong

Every frame shares one envelope, and every frame carries an id that doubles as its idempotency key.

Session shape

Node connects (WSS, credential in the handshake) → control:version_negotiate ── pick the highest mutually supported version → AUTHENTICATED → control:resync ── reconcile both directions → READY ── commands flow, events flow, heartbeats tick

The node drives this as a single-consumer loop over an explicit state machine. A 401 or 403 pauses the node rather than putting it into a reconnect loop, so a revoked credential does not become a denial-of-service against your own server.

Two implementations, one corpus

The protocol has a Kotlin implementation in the node (ProtocolCodec, kotlinx.serialization) and a TypeScript one in @luno-oss/protocol. They are cross-checked against a shared fixture corpus — the same fixtures/frames.json is read by the node’s ProtocolFixturesTest and by the TypeScript test suite, both asserting byte-identical round-trips.

This is what keeps the two codecs from drifting silently. A protocol change that breaks one side fails the other side’s tests in the same commit.

Forward compatibility

Decoding yields one of three results — Ok, Unsupported, or Malformed — and the distinction matters. An unknown frame type is quarantined, not treated as corruption, so an older node connected to a newer backend degrades rather than disconnecting.

In this section