Wire Format

All trueseal-protocol messages share a common framing. Every message on the wire is:

[type: u8][len: u32 BE][body: bytes]
  • type — 1-byte message type tag
  • len — 4-byte big-endian unsigned integer, the length of body in bytes
  • body — zero or more bytes, type-dependent

Minimum message size is 5 bytes (type + len with no body). A parser that receives fewer than 5 bytes must treat the message as malformed.

Message Types

TagNameDirectionBody
0x01Pushclient → relay[recipient_pub: 32 bytes][envelope: bytes]
0x02Deliverrelay → client[blob_id: 8 bytes u64 BE][envelope: bytes]
0x03Heartbeatbothempty
0x04Ackrelay → clientempty
0x05Errorrelay → clientempty
0x06DeliverAckclient → relay[blob_id: 8 bytes u64 BE]

Unknown type tags must be treated as malformed and the connection closed.


Push (0x01)

Sent by the client to deliver a blob to a recipient device.

Body layout:

[recipient_pub: 32 bytes][envelope: bytes]
  • recipient_pub — the recipient device’s X25519 noise public key. The relay uses this to route the blob to the correct Inbox. It is the only routing information in the push body.
  • envelope — the raw serialized protobuf Envelope bytes, as defined by trueseal-sync. The relay treats this as opaque — it never inspects, modifies, or decrypts the envelope.

A Push body shorter than 32 bytes is malformed. The relay must reject it with an Error frame and must not Ack.


Deliver (0x02)

Sent by the relay to deliver a blob to a connected device.

Body layout:

[blob_id: 8 bytes u64 BE][envelope: bytes]
  • blob_id — an opaque 8-byte identifier assigned by the relay. The client echoes it back in a DeliverAck frame after durably persisting the blob. The client must strip these 8 bytes before decoding the Envelope.
  • envelope — the raw serialized protobuf Envelope bytes. Forwarded verbatim — the relay never modifies it.

The blob is not deleted from the relay’s InboxStore at delivery time. It is deleted only after the relay receives a DeliverAck for its blob_id. If the session drops before a DeliverAck arrives, the blob will be re-delivered on the next Receive Session. Clients must handle duplicate delivery — deduplication is the client’s responsibility.


Heartbeat (0x03)

Sent by either side to keep the connection alive. The receiver must respond with a Heartbeat of its own. Body is always empty.


Ack (0x04)

Sent by the relay after a Push is successfully persisted to the Inbox. Body is always empty.

Ack means: the envelope has been durably stored and will be delivered to the recipient when they connect. It does not mean the recipient has received it.

Ack does not mean: the envelope was delivered to the recipient. The relay may have stored it for later delivery if the recipient is offline.

A client that receives an Ack for a Push may mark that blob as delivered in its local outbox.


DeliverAck (0x06)

Sent by the client after it has durably persisted a Deliver frame.

Body layout:

[blob_id: 8 bytes u64 BE]
  • blob_id — the opaque identifier from the corresponding Deliver frame, echoed back verbatim.

On receipt, the relay deletes the blob from the InboxStore. A blob that has not been DeliverAck’d survives session drops and is re-delivered on the next Receive Session.


Error (0x05)

Sent by the relay when a Push is permanently rejected. Body is always empty.

Error means: the relay will not store this blob. The client must not retry the same blob — it is a permanent, non-retryable rejection.

Current rejection reasons:

  • Push body shorter than 32 bytes (malformed)
  • Envelope exceeds the protocol size limit (see below)

A client that receives an Error must remove the blob from its retry queue. Retrying will produce the same result.


Envelope Size Limit

The protocol defines a maximum envelope size of 1 MiB (1,048,576 bytes). This applies to the envelope portion of the Push body — the bytes after the 32-byte recipient_pub.

Enforcement is two-layered:

Client-side (trueseal-sync): checks before opening a Push Session. An oversized envelope is rejected immediately with a non-retryable error. No NK handshake is opened, no bytes are sent to the relay.

Relay-side (trueseal-relay): checks on receipt as a backstop. Rejects with an Error frame, does not Ack, does not store. This catches bugs in client implementations and future third-party clients that skip the client-side check.

Relay operators may configure a stricter limit. The relay communicates rejection via the Error frame regardless of which limit triggered it.


Session Types

trueseal-protocol uses two distinct session types, each with a different Noise handshake pattern:

Push Session — client opens, sends one or more Push frames, closes. Uses Noise NK — the relay authenticates itself to the client, but the client’s identity is never revealed. The relay cannot link a Push Session to any device. Short-lived by design.

Receive Session — client opens, stays connected. Uses Noise XX — mutual authentication. The relay learns the device’s stable noise public key and uses it to deliver blobs immediately on arrival. Long-lived by design.

The two session types run on separate TCP listeners. This separation keeps the routing logic clean: Push Sessions are anonymous and stateless; Receive Sessions are identified and stateful.