Skip to content

Message formats

Every message is a CBOR map. Deterministic encoding ([RFC 8949] section 4.2.1) is REQUIRED wherever bytes are hashed or signed; message envelopes themselves are neither, and need not be deterministic — though emitting them deterministically anyway is the simpler implementation.

Every message carries an explicit ssp_version member, an integer. This revision defines the value 1.

A receiver encountering an unknown ssp_version MUST fail closed — refuse the exchange rather than attempt a best-effort parse.

This is a deliberate departure. A predecessor carried no in-band version member at all and delegated versioning entirely to a transport identifier, on the reasoning that the transport already distinguished versions. That couples protocol identity to transport choice: the same protocol over a second transport becomes unversioned, and a version bump requires a transport change. An in-band member costs a few bytes and makes the message self-describing.

Exchanged before the first sync of a session.

Member Type Description
ssp_version unsigned 1
site_id text Sender’s site_id
have_max_hlc map Highest HLC the sender holds: {physical_ms, logical}
supports_realtime bool Whether the sender can hold a live subscription; see realtime mode
secure_channel bool Whether the binding authenticated the peer
rtt_ms unsigned or null Sender’s measured round-trip estimate, advisory

A receiver MUST fail closed to batch mode on any unknown member value rather than guessing a capability.

secure_channel is advisory and MUST NOT be treated as a security input. A peer can claim anything; the receiver knows what its own binding provided. It exists so a node can warn when a peer believes the channel is authenticated and the local side disagrees, which usually indicates a misconfigured binding.

The same shape travels in both directions, which keeps a sync a single symmetric round trip rather than two asymmetric protocols.

Member Type Description
ssp_version unsigned 1
from_site_id text Sender’s site_id
have_max_hlc map Highest HLC the sender holds
events array of byte strings Zero or more encoded COSE_Sign1 change events, forwarded byte for byte
records array of byte strings Zero or more encoded COSE_Sign1 control records — vouches, role assignments, transport bindings, authorization records. MAY be empty; MUST be present
applied_watermarks array Zero or more watermark records

Events travel as the exact bytes their producer signed — event identity depends on it — so the envelope carries them as opaque byte strings rather than re-encoding them.

records is the wire path for the durable control records the registry defines. Each is self-verifying, so a receiver processes them independently of the events in the same exchange and in any order; a record that fails verification is dropped and surfaced without affecting the rest of the body.

Selection: a responder returns events at or above the requester’s have_max_hlc, bounded by an implementation-chosen batch limit.

The bound is inclusive — at or above — deliberately. An exclusive bound loses any event sharing the boundary HLC with one already delivered, and because apply is idempotent the cost of re-sending a boundary event is nil while the cost of skipping one is a permanent gap.

A receiver MUST drop events whose protected kid equals its own site_id.

This is normative and unconditional. Without it, an event that a node authored and that returns via a peer is re-ingested, re-offered, and can circulate indefinitely — and the combination of an inclusive selection bound and at-least-once delivery makes that loop reachable in normal operation rather than only under attack.

A predecessor stated this rule only inside the description of one deployment epoch, so a reader implementing the other epoch would omit it. It belongs here, applying to every profile.

A receiver MUST reject a sync body that is not a CBOR map with exactly the members above.

A predecessor instructed receivers to be liberal at this boundary — reading a bare array as an event list and anything else as empty. That converts a malformed or hostile body into a silent no-op, which is indistinguishable from a peer with nothing to send. SSP/1 rejects explicitly instead, so a protocol error surfaces as one.

The registry registers two transport modes, batch and realtime. Everything above this section describes batch mode: cycle-driven request/response synchronization. Realtime mode is the second, and it is deliberately not a second protocol: a realtime push is a sync-exchange body — the same CBOR map, the same members, the same rules — sent unsolicited over a held live connection as events are produced, with no response awaited.

Realtime mode operates only when both peers advertised supports_realtime in the capability handshake. A sender MUST NOT push unsolicited bodies to a peer that did not advertise it, and a receiver MUST reject an unsolicited body on a session where the capability was not mutually established — reject, not drop, so a protocol error surfaces as one.

Realtime mode changes when a body is sent — at production time rather than on the next cycle — and nothing else:

  • Delivery semantics are unchanged. At-least-once delivery, idempotent apply, self-echo suppression and the watermark rules apply to a pushed body exactly as to a requested one.
  • Cursors remain authoritative. A sender advances its durable cursor only on a successful push, solicited or not. A push that fails, or whose outcome the sender never observes, is re-offered by the batch path.
  • Ordering is still not guaranteed. A pushed body may arrive before, after or interleaved with overlapping batch bodies. Idempotent apply absorbs every interleaving, and a receiver MUST NOT infer order from the mode a body arrived through.

4.1. Batch remains the correctness backstop

Section titled “4.1. Batch remains the correctness backstop”

Realtime mode is a latency optimization, never a correctness mechanism — the same posture as applied watermarks. A node operating in realtime mode MUST continue running its periodic batch cycle.

That rule is what allows the push path to be cheap. A push may be lost to disconnection, backpressure or a crash without threatening convergence, because everything it failed to deliver is re-offered from the durable cursor on the next cycle. An implementation whose only delivery path is push has quietly taken on an exactly-once delivery obligation the transport cannot meet, and will not converge.

A sender MAY coalesce events produced faster than the link drains into fewer, larger pushed bodies, and the sender-enforced events-per-response limit applies to a pushed body unchanged. A receiver MUST NOT assume one event per push, one push per event, or any particular pacing.

A push at production time is a timestamp on the wire. Whoever can observe the channel — a relay operator, a network-path observer — learns the mesh’s activity rhythm even with payloads encrypted and padded, because padding hides sizes, not timing. Where that matters, a sender SHOULD flush on a fixed cadence rather than on production, which converts the timing channel into a constant signal at the cost of one cadence interval of latency. The trade is a deployment policy, not a protocol member.

Realtime mode exists only on a live binding; the mailbox is store-and-forward by definition and nothing in this section applies to it. A binding claiming realtime mode MUST carry peer-initiated one-way bodies (see framing), and trust resumption applies to a held session exactly as to a fresh one.

Where SOP/1 is deployed, the same held connection typically also carries the object plane’s MLS-protected control messages (SOP/1 section 10) — which is how a freshly published object’s catalog entry reaches online peers with the same immediacy. That path is the object plane’s, not this one’s; realtime mode carries only SSP/1 bodies.

Member Type
site_id text
table_schema text
table_name text
hlc_physical_ms unsigned
hlc_logical unsigned

Semantics, monotonicity and the fail-closed rule are in semantics.

Watermarks are not signed. They are an efficiency hint, not an authorization input, and a peer that lies about its watermark harms only its own convergence. A receiver MUST NOT grant anything on the basis of a watermark.