Docs Menu

Specs

spec/01-messaging.md · shown exactly as it appears in the repository

01 — Messaging

Status: Draft, v0.1. License: This text is CC-BY-4.0; the field names, enum values, and other machine-readable schema elements defined here are also MPL-2.0 — see LICENSE-DOCS and LICENSE.

Overview

Every exchange between two connected parties — establishing a connection, extending it to a new domain, granting a subscription, sharing or mutating content — travels as a message: a single envelope addressed to a specific sendKey. This chapter defines that envelope and the generic mechanism an implementation uses to route an incoming message to the code that handles it. It does not define what any particular message means — that's each other chapter's job for the message types it owns — only the shape every message shares, regardless of what it's for.

The envelope

A message has two parts: an envelope, sent in the clear, and a payload, always encrypted before it leaves the sender (see 07-security.md for the cryptography itself). The payload is opaque to the router — only the two connected parties can read it.

The envelope carries:

Field Purpose
version Envelope format version.
messageId Unique identifier for this message, used for deduplication (below).
sendKey The key this message is addressed to.
domain Which of the connection's domains this message belongs to.

The payload carries type, operation, and messageContent — covered in the next section.

Message types and operations

type identifies which generic category a message belongs to, and MUST be one of a small set defined by the protocol itself: connection, subscription, content, or command. operation identifies what's being done within that type (e.g. subscribe, cxnEstablished, create), and is likewise a protocol-defined set, not something a domain invents its own values for. This is different from a domain's own content types (see 05-content-model.md), which are domain-defined — type and operation are not.

Planned extension (non-normative preview): both sets are closed in the current protocol version; a future version may open operation (and possibly type) to domain extension.

messageContent carries the actual data for that type/operation pair — an array of content items, whose shape is defined elsewhere (by the chapter that owns the type, or by the domain's own schema for content messages).

Deduplication

messageId MUST be unique per message. An implementation MUST treat a message whose messageId has already been seen for that sendKey as already delivered, not reprocess it — this is the generic, envelope-level guard against at-least-once redelivery, independent of what the message's type/operation/messageContent actually is. In the current reference implementation this is checked twice: the router may reject a duplicate send with a 409 the sender treats as "already delivered," and the receiving agent independently keeps a bounded, per-sendKey window of recently-seen messageIds as a second line of defense.

This is a cheaper, generic safety net, not the only one — content messages carry a separate, stronger ordering guarantee built on shareSequence and version (see 06-propagation.md). This section only covers the transport-level guard that applies uniformly to every message, regardless of type.

Handler dispatch

An implementation MUST register exactly one handler per type before it can process incoming messages of that type — attempting to dispatch a message whose type has no registered handler MUST fail rather than be silently dropped. A handler has the shape (context, connection, operation, messageContent) => ...; by the time it's invoked, the connection the message's sendKey resolves to has already been found and passed in. The handler itself, not the dispatch mechanism, is responsible for interpreting operation and deciding what to do with messageContent.

Exception: the very first handshake message (connection / cxnEstablished, see 02-connections.md) is handled specially, before the generic type-based dispatch — no connection exists yet to resolve the message against, so it can't go through the normal (context, connection, operation, messageContent) path.