Design Philosophy
p2p-mes makes a small number of deliberate, sometimes uncompromising choices. Together they explain almost every "why" behind the wire formats, the storage layout, and the API surface described elsewhere on this site. Read this page first: much of the rest of the specification is the mechanical consequence of the five principles below.
Minimal state, maximal derivation
The protocol stores data only when there is no alternative. Anything that can be computed from inputs, or derived from data already stored, is never written down.
Two examples that recur throughout the design:
- A direct-message conversation has no stored membership and no stored
identifier. Its
chat_idis derived deterministically from the two participant addresses (see Cryptography & Authentication), so access control falls out of the math: only the two parties can compute it. - Unread counts are never stored. They are derived at read time by comparing a chat's latest sequence number against the reader's stored read progress.
Every candidate for persistence is challenged with one question: can we avoid storing it? Less stored state means less to synchronize, less to keep consistent, and fewer ways for two nodes to disagree.
Performance is a feature
Throughput and latency are treated as correctness properties, not tuning knobs bolted on at the end.
- HTTP writes are fire-and-forget: a request is validated, signed gossip is published, a write is queued to an asynchronous DB writer, and the client gets its response before the write commits. The event loop never blocks on disk.
- All persistence flows through a single asynchronous DB writer, which is also the single source of truth for metrics and synchronization state. One path means one place to reason about ordering.
- Storage keys are laid out for the access pattern -- time-ordered message scans, reverse-time inbox listing, prefix bucket scans for sync -- so the hot reads are sequential range iterations rather than random lookups.
Where readability and speed conflict, the rule is to measure first; but once a path is proven hot, it is optimized without apology.
Full replication over sharding
Every node is a full replica: it stores all messages, members, and identity records, and can answer any query on its own. There is no ownership, no routing by key, no "ask the responsible node."
This is a deliberate trade. An earlier design sharded data by XOR distance with a replication factor; it was removed in favor of full replication because the consistency story is dramatically simpler. With every node holding everything, agreement reduces to a single question -- do two nodes hold the same set of records? -- which is answered efficiently by Merkle-tree anti-entropy sync across three independent domains: messages, members, and identity.
The cost is storage and write amplification; the benefit is that any node can serve any client, synchronization is uniform, and there is no rebalancing. Because of this choice, all persistent data must be syncable -- any new stored field has to be covered by an anti-entropy domain, or it will silently diverge between nodes.
Two layers: a dumb transport, a smart client
The node deliberately understands as little as possible. It is split into two layers that behave identically for direct messages and groups.
Layer 1 -- node-enforced. These behaviors are built into every node and cannot be bypassed: signed message delivery with deduplication, inbox maintenance, group membership with add-wins CRDT semantics, authentication and authorization on every request, anti-entropy sync, and retention.
Layer 2 -- client-defined, opaque to the node. A few fields are stored and relayed verbatim, never interpreted:
msg_type(u8) -- the client assigns the meaning (for example: text, handshake, key rotation); the node treats every value as valid and opaque.control(byte string) -- an opaque payload carried alongside a message.- the per-DM
identityblob -- stored last-write-wins, never inspected.
The consequence is the central architectural bet: semantics live in the client, not the network. A minimal client can send plaintext using Layer 1 alone. A privacy-focused client can build end-to-end encryption, key exchange, or any other protocol entirely on Layer 2 -- the network neither needs to understand it nor is able to interfere with it. See Building a Client.
What "interoperable" means
The interoperability contract is Layer 1: any client that signs requests correctly can exchange plaintext messages, manage groups, track read progress, and publish identity with any other client, through any node. That is the guarantee the word "interoperable" carries here.
Layer 2 is deliberately not interoperable by default. The msg_type
values, the structure of control payloads, and any encryption scheme are
defined by the client, not the protocol. There is no msg_type registry and no
normative end-to-end-encryption profile yet, so two independently built clients
interoperate only in plaintext (Layer 1) unless they separately agree on a
Layer 2 profile. Specifying one normative profile (handshake, key rotation,
ciphertext framing) is future work; until then, "E2EE" is a capability the
transport enables, not a scheme the protocol pins down.
Trust model
The protocol is precise about what a node can and cannot prove, and a client should assume nothing beyond it.
What the node enforces. Every state-changing operation is signed. The node recovers the author's address from the ECDSA signature (see Cryptography & Authentication) and checks authorization -- group admin rights, membership for group sends -- before accepting a write. So authorship and authority are verifiable: a node can prove who requested a change and that they were allowed to make it. Message identity and deduplication use a BLAKE3 content hash. A Hybrid Logical Clock timestamps every conflict-bearing operation and rejects timestamps from peers that exceed local wall-clock by more than a fixed drift bound, so a single misclocked or malicious peer cannot drag the cluster's logical time forward.
What the node does not provide. It does not read the meaning of Layer 2 payloads, so confidentiality from the node operator is not a transport guarantee -- it is delegated to the client via Layer 2 encryption. The network also assumes a population of honest full replicas: anti-entropy converges what honest nodes hold, but the protocol does not by itself defend against a Byzantine replica that selectively withholds or serves stale records. A formal adversary model is still being developed; today's guarantees are authenticity and authorization -- not confidentiality from the operator (delegated to clients) and not Byzantine fault tolerance. Clients that need stronger properties should layer them on top.
Privacy: what the node operator sees
Confidentiality in p2p-mes is content-only, and only when a client adds Layer 2 encryption. Everything a node needs to route, store, and synchronize is plaintext to the operator of any node the data reaches -- which, under full replication, is every node:
- Who talks to whom. Sender and group members are plaintext. A DM
chat_idisblake3(domain || min(a,b) || max(a,b)), so an operator who suspects a pair of addresses can confirm they are conversing. - When. HLC and
origin_wall_tsstamps expose timing and activity patterns. - How much. Message sizes, group sizes, and frequency are observable.
Message bodies can be hidden with Layer 2 E2EE, but the social graph and metadata cannot -- they are inherent to a fully replicated store. A client should say this plainly to its users: p2p-mes protects message contents from the operator (with E2EE), not the fact that, when, or with whom you communicate.