Types Reference
crates/types -- Shared Domain Primitives
ID Types
#![allow(unused)] fn main() { const CHAT_ID_LEN: usize = 32; const USER_ID_LEN: usize = 20; const SENDER_ID_LEN: usize = 20; const MSG_ID_LEN: usize = 32; type ChatId = [u8; 32]; type UserId = [u8; 20]; type SenderId = [u8; 20]; type MsgId = [u8; 32]; }
ChatKind
#![allow(unused)] fn main() { #[serde(tag = "t", content = "d")] enum ChatKind { #[serde(rename = "0")] Dm { peer: UserId }, // Direct message (peer semantics: see below) #[serde(rename = "1")] Group { title: Option<String> }, #[serde(rename = "2")] Channel { title: Option<String> }, } }
Methods: type_id() -> u8, is_dm() -> bool
Default: Dm { peer: [0u8; 20] } (backward compatibility)
CBOR representation: {"t": "0", "d": {"peer": [...]}} -- compact tagged format.
Two wire-level notes:
- The tag
tis a CBOR text string ("0"/"1"/"2"), never an integer -- a consequence of serde's adjacently-tagged encoding, kept as-is for compatibility with already-stored data. Dm.peersemantics depend on where the value lives. In wire messages (MsgV1.kind, gossipPutMessage.kind) it is the original recipient as fixed by the sender -- identical bytes for every reader, so in a received message it equals the reader's own address (interlocutor =peer == me ? sender : peer). Inuser_inboxentries (and therefore in the/conversationsAPI) the node rewritespeerper inbox owner to be the other participant (upsert_inbox_for_membersincrates/db), so there it is always viewer-relative.
Note: User identity blobs (e.g. public keys) are stored in the dedicated
identity CF (see STORAGE.md), not inside ChatKind.
Message Types
There are no msg_type constants in code -- the field is a
client-defined opaque u8 (0 = plain message, set by the plain
message endpoints). Known control conventions (attachments 10, MLS
Welcome/Commit 20/21, retired 11/12/22, app-private
100-255) are documented in the
client guide registry.
User content never claims a control type -- it rides text, optionally
marked with a client format prefix.
HlcTimestamp
#![allow(unused)] fn main() { #[serde(transparent)] struct HlcTimestamp(u64); // packed: 48 bits physical_ms + 16 bits logical }
Hybrid Logical Clock timestamp used by CRDT-like domains (members,
identity) and as a network-wide ordering key for messages. Packed into a
single u64:
- Upper 48 bits:
physical_ms-- milliseconds since UNIX epoch - Lower 16 bits:
logical-- per-node counter for events within one ms
Methods: from_parts(phys, log), from_packed(u64), to_packed() -> u64, physical_ms() -> u64, logical() -> u16, next_logical() -> HlcTimestamp. Ord is derived from the packed u64, which gives
lexicographic (physical, logical) ordering -- the property CRDTs rely
on for last-writer-wins decisions.
Constants: MAX_PHYSICAL_MS (48 bits set, year ~10889),
MAX_LOGICAL = u16::MAX, HlcTimestamp::ZERO (sentinel for absent
optional HLC fields in record-id derivation).
Serde: #[serde(transparent)] -- wire encoding is identical to a raw
u64. The 8-byte slot for the legacy ts: u64 in seen_msg value
layout, messages CF key, and identity CF value is reused without size
change. Big-endian encoding of the packed value preserves HLC ordering
under RocksDB's lexicographic key comparison.
Clock
#![allow(unused)] fn main() { trait Clock: Send + Sync + 'static { fn now_ms(&self) -> u64; } struct SystemClock; // wraps SystemTime::now() struct MockClock { ... } // AtomicU64-backed, test-controllable }
Time-source abstraction. Production uses SystemClock; tests use
MockClock with set(ms) and advance(delta_ms). The abstraction
exists so HLC's clock-skew behaviour is reproducible in the test suite.
Shared via Arc<dyn Clock> so multiple owners (HLC state, retention
loop) can read the same source without further synchronisation.
crates/db -- Storage Types
MsgV1 (stored in CF messages)
#![allow(unused)] fn main() { struct MsgV1 { schema: u8, // Always 1 msg_id: MsgId, chat_id: ChatId, sender: SenderId, hlc: HlcTimestamp, // Server-stamped HLC (storage/CRDT/sync) origin_wall_ts: u64, // Frozen originator wall-clock (UI display) seq: u32, // Monotonic sequence within chat text: String, // current text (post-edit; empty if deleted) msg_type: u8, // client-defined; 0 = regular text control: Option<Vec<u8>>, // CBOR payload for control messages kind: ChatKind, edited_at: Option<HlcTimestamp>, // HLC of the winning Edit (absent = never edited) deleted: bool, // true once deleted (stub row, text cleared) } }
edited_at and deleted are the materialized projection of any
MessageOp records targeting this message (see PROTOCOL.md). Both are
#[serde(default, skip_serializing_if = ...)], so an untouched message
serializes byte-for-byte as before -- adding edit/delete support changed
no existing wire vector. msg_id is not recomputed on edit: it
stays the message's stable identity even though the stored text no
longer hashes to it.
ChatMeta (stored in CF chats_meta)
#![allow(unused)] fn main() { struct ChatMeta { last_ts: u64, last_seq: u32, last_msg_id: MsgId, members: Vec<UserId>, last_msg_ts: u64, last_announced_ms: u64, title: Option<String>, // Group creation title (immutable) creator: Option<UserId>, // Group creator (immutable) create_nonce: Option<Vec<u8>>, // 16-byte creation nonce (immutable) } }
The title / creator / create_nonce trio is the group's immutable
creation metadata, written once via set_group_create_meta and
verifiable against the chat_id (blake3(domain || creator || nonce || title)). All three are None for DMs; title alone is None for
unnamed groups. See STORAGE.md and SYNC.md.
InboxEntry (stored in CF user_inbox)
#![allow(unused)] fn main() { struct InboxEntry { chat_id: ChatId, kind: ChatKind, last_ts: u64, last_msg_id: MsgId, last_sender: SenderId, last_seq: u32, } // No content preview -- previews are derived client-side. }
MemberInfo (stored in CF members)
#![allow(unused)] fn main() { struct MemberInfo { role: u8, // 0 = participant, 1 = admin, 2 = owner added_at: HlcTimestamp, // HLC of the latest Add removed_at: Option<HlcTimestamp>, // HLC of the latest Remove } impl MemberInfo { fn is_active(&self) -> bool { match self.removed_at { None => true, Some(r) => self.added_at > r, } } } }
Public reads (get_member_info, list_members, is_member) filter to
is_active(). Sync helpers iterate raw seen_member and include
tombstones so Merkle anti-entropy can carry removed_at between
peers. See STORAGE.md for the full CRDT model.
MessageOpRecord (stored in CF message_ops)
#![allow(unused)] fn main() { struct MessageOpRecord { target: MsgId, // message being changed chat_id: ChatId, // chat the target belongs to editor: UserId, // must equal the target's original sender op_kind: u8, // OP_KIND_EDIT = 0, OP_KIND_DELETE = 1 new_text: String, // replacement text (empty for Delete) hlc: HlcTimestamp, // server-stamped by the originating node sig: Vec<u8>, // client sig for a Delete; empty for an Edit } }
An append-only edit/delete operation. The same struct is the stored
record, the gossip payload, and the anti-entropy wire record -- no
re-encode. op_kind == OP_KIND_DELETE is terminal. Only a delete is
signed (and re-verified by every node): erasure yields a deleted stub
that cannot be authenticated end-to-end, so the node authorizes it. An
edit is unsigned -- its content is checked by the recipient on
Layer 2, like a plain message. The message row clients read is the
materialized fold of a message and all its ops; the fold rule
(op_applies_to) and apply_message_op live in
crates/db/src/message_ops.rs. See PROTOCOL.md and SYNC.md.
Query/Result Types
#![allow(unused)] fn main() { struct PutMsg<'a> { chat: &'a ChatId, sender: &'a [u8; 20], hlc: HlcTimestamp, // Stored in CF `messages` key + msg_id hash origin_wall_ts: u64, // Frozen sender wall-clock for UI display text: &'a str, kind: ChatKind, msg_type: u8, control: Option<&'a [u8]>, } struct RangeQuery<'a> { chat: &'a ChatId, from_ts: u64, to_ts: Option<u64>, after_key_b64: Option<String>, limit: usize, reverse: bool, // false = oldest-first from `from_ts`; // true = newest-first from `to_ts` } struct RangePage { items: Vec<(String, Vec<u8>)>, // (key_b64, msg_cbor) next_after_key_b64: Option<String>, } struct ListChatsQuery<'a> { user: &'a [u8; 20], limit: Option<usize>, after_cursor_b64: Option<String>, } struct ListChatsPage { items: Vec<InboxEntryWithCursor>, next_after_cursor_b64: Option<String>, } }
crates/api -- HTTP Types
Command Enum
#![allow(unused)] fn main() { enum Command { PutMessage { chat_id: [u8; 32], kind: ChatKind, sender: [u8; 20], members: Option<Vec<[u8; 20]>>, text: String, // HLC + origin_wall_ts are stamped server-side in the MPSC // handler; clients never specify time. msg_type: u8, control: Option<Vec<u8>>, resp: oneshot::Sender<Result<PutMessageResponseRaw, String>>, }, ListUserChats { user: [u8; 20], limit: usize, after_key: Option<Vec<u8>>, resp: oneshot::Sender<Result<ListUserChatsResponseRaw, String>>, }, GetChatRange { user: [u8; 20], chat_id: [u8; 32], from_ts: u64, to_ts: Option<u64>, after_key: Option<Vec<u8>>, limit: usize, reverse: bool, // false = oldest-first; true = newest-first skip_membership_check: bool, resp: oneshot::Sender<Result<GetChatRangeResponseRaw, String>>, }, GetMessageById { // Single message by id; node authorizes caller user: [u8; 20], msg_id: [u8; 32], resp: oneshot::Sender<Result<Option<Vec<u8>>, String>>, // None = 404 }, ReadChatMessage { user: [u8; 20], chat_id: [u8; 32], seq: u32, resp: oneshot::Sender<Result<(), String>>, }, GetNetworkNodes { // Catalog of connected API-serving peers resp: oneshot::Sender<Result<Vec<NetworkNodeRaw>, String>>, }, MessageOp { // Edit/delete own message; node loads target, target: [u8; 32], // checks authorship + 48h window, stamps HLC, editor: [u8; 20], // verifies sig, then routes DbOp + gossip op_kind: u8, // 0 = Edit, 1 = Delete new_text: String, sig: Vec<u8>, resp: oneshot::Sender<Result<MessageOpResponseRaw, String>>, }, // Plus: MembershipOp, LeaveGroup, GetGroupMembers, SetIdentity, // GetIdentity -- see crates/api/src/command.rs } }
Raw Response Types
#![allow(unused)] fn main() { struct PutMessageResponseRaw { chat_id: [u8; 32], msg_id: [u8; 32], origin_wall_ts: u64, // HTTP DTO surfaces this as `ts` for backward compat } struct InboxChatRaw { chat_id: [u8; 32], kind: ChatKind, last_ts: u64, last_sender: [u8; 20], unread: u32, cursor_key: Vec<u8>, source: String, } struct ListUserChatsResponseRaw { items: Vec<InboxChatRaw>, next_after_key: Option<Vec<u8>>, } struct ChatMessageRaw { key_raw: Vec<u8>, msg_cbor: Vec<u8>, } struct MessageOpResponseRaw { msg_id: [u8; 32], op_ts: u64, // wall-clock component of the op's HLC ("edited at") } struct GetChatRangeResponseRaw { items: Vec<ChatMessageRaw>, next_after_key: Option<Vec<u8>>, } struct NetworkNodeRaw { peer_id: String, // base58 PeerId api_url: String, // advertised HTTP API base URL } struct GroupMembersRaw { members: Vec<([u8; 20], u8)>, // (address, role) pairs title: Option<String>, // group creation title from chats_meta } }
DTO Types (JSON serialization)
#![allow(unused)] fn main() { #[serde(tag = "type")] enum ChatKindDto { #[serde(rename = "dm")] Dm { peer: String }, #[serde(rename = "group")] Group { title: Option<String> }, #[serde(rename = "channel")] Channel { title: Option<String> }, } struct PostDirectMessageBody { text: String } // validate: 1-45056 bytes struct PostDirectMessageResponse { chat_id, msg_id, ts } struct PostControlDirectMessageBody { msg_type: u8, control: String } // validate: msg_type 1-255, control base64 1-43692 chars (32 KiB decoded) struct PostControlMessageResponse { chat_id, msg_id, ts } struct ListUserChatsQueryParams { limit: Option<usize>, after: Option<String> } // validate: limit 1-1000 struct InboxChat { chat_id, kind, last_ts, last_sender, unread, cursor } struct ListUserChatsResponse { items: Vec<InboxChat>, next_after: Option<String> } struct GetChatRangeQueryParams { from, to, after, limit, reverse: Option<bool> } // validate: limit 1-1000; reverse omitted => false (oldest-first) struct ChatMessage { key: String, msg_cbor: String } struct GetChatRangeResponse { items: Vec<ChatMessage>, next_after } struct GetMessageResponse { msg_id: String, msg_cbor: String } // GET /messages/{msg_id} struct ReadChatMessageBody { seq: u32 } // validate: >= 1 struct NodeInfoResponse { peer_id: String, api_url: Option<String> } // GET /node/info struct NetworkNodeDto { peer_id: String, api_url: String } struct NetworkNodesResponse { nodes: Vec<NetworkNodeDto> } // GET /network/nodes struct CompoundMembershipRequest { ops, messages, nonce: Option<String>, title: Option<String> } // title: create-only, 1-128 UTF-8 bytes, no control chars; part of the // chat_id preimage (see PROTOCOL.md) struct GroupMember { address: String, role: u8 } struct GroupMembersResponse { members: Vec<GroupMember>, title: Option<String> } }
crates/node -- Node Types
GossipMessage
See PROTOCOL.md for full specification.
Key structs (abbreviated; full spec in PROTOCOL.md):
#![allow(unused)] fn main() { struct PutIdentity { user: [u8; 20], // User address blob: Vec<u8>, // Opaque identity blob (max 1024 bytes) hlc: HlcTimestamp, // Server-stamped HLC, used for LWW origin: String, // PeerId of the originating node } // Edit/delete an existing message. Carries `MessageOpRecord` verbatim // (see crates/db), so gossip and anti-entropy ship identical bytes. // GossipMessage::MessageOp(MessageOpRecord) }
DbOp
#![allow(unused)] fn main() { enum DbOp { // HLC drives storage/CRDT/sync; origin_wall_ts is frozen sender wall-clock for UI. PutMessage { chat_id, sender, text, hlc: HlcTimestamp, origin_wall_ts: u64, kind, msg_type, control }, UpsertInbox { chat_id, kind, users, all_members, ts, seq, msg_id, sender }, SetReadProgress { user, chat_id, seq }, // Discrete Add/Remove/Create from API or gossip. `hlc` is server-stamped. MembershipOp { chat_id, target, role, op_type, hlc: HlcTimestamp, recovered_signer }, // Full peer state from anti-entropy sync. CRDT-merged with local record. ApplyMemberRecord { chat_id, target, info: MemberInfo }, // Immutable group creation trio -> chats_meta (write-once). Senders // MUST have verified chat_id == blake3(domain||creator||nonce||title). SetGroupCreateMeta { chat_id, title: Option<String>, creator, nonce: Vec<u8> }, // HLC-LWW on identity blob (max 1024 bytes). PutIdentity { user, hlc: HlcTimestamp, blob }, // Message row replicated verbatim from a peer during sync (id + // edited_at + deleted preserved). Live authoring uses PutMessage. ApplySyncedMessage { msg: Box<MsgV1> }, // Record an edit/delete op + materialize it into the target row, // one WriteBatch. From HTTP, gossip, and sync alike. MessageOp { record: MessageOpRecord }, DeleteInboxEntry { user, chat_id }, // Atomic two-record role swap: old owner -> admin, new owner -> owner. TransferOwnership { chat_id, current_owner, new_owner, hlc: HlcTimestamp }, // Bulk-tombstone all members + per-user inbox cleanup. Messages left to GC. DeleteGroup { chat_id, hlc: HlcTimestamp }, } /// Wrapper around UnboundedSender<DbOp> that increments /// db_writer_queue_depth gauge on every successful send. struct DbOpSender { inner: mpsc::UnboundedSender<DbOp> } enum MerkleUpdate { Insert([u8; 32]), Replace { old, new }, Remove([u8; 32]) } struct MerkleSenders { msg_tx: UnboundedSender<[u8; 32]>, // messages (append-only) member_tx: UnboundedSender<MerkleUpdate>, // members (mutable) identity_tx: UnboundedSender<MerkleUpdate>, // identity (mutable) op_tx: UnboundedSender<[u8; 32]>, // message ops (append-only) } }
Sync Types
See SYNC.md for SyncDomain, SyncRequest, SyncResponse, SyncState, SyncSession.
Retention Types
See RETENTION.md for the full GC design.
#![allow(unused)] fn main() { struct CycleStats { removed_count: usize, // msg_ids removed from seen_msg and the Merkle tree chats_processed: usize, // chats range-deleted in `messages` hit_limit: bool, // true if GC_BATCH_LIMIT was reached } }
Constants in crates/node/src/retention.rs: RETENTION_WINDOW,
GC_INTERVAL, GC_SHORT_INTERVAL, GC_BATCH_LIMIT, GC_CHUNK_SIZE.
Functions: cutoff_ts_at(now) -> u64, cutoff_ts_now() -> u64,
gc_cycle(db, merkle_msgs, now) -> CycleStats,
run_gc_loop(db, merkle_msgs, cancel).
HlcState (per-node HLC)
#![allow(unused)] fn main() { struct HlcState { state: AtomicU64, // packed HlcTimestamp clock: Arc<dyn Clock>, max_drift_ms: u64, } enum HlcError { DriftTooLarge { incoming_ms: u64, local_ms: u64, max_drift_ms: u64 }, } const DEFAULT_MAX_DRIFT_MS: u64 = 5 * 60 * 1000; // 5 minutes }
Per-node Hybrid Logical Clock state. Generates outgoing HLC stamps
(stamp()) and merges incoming HLC values from gossip (receive()).
Lives in-memory only -- on restart, the state resets to
(clock.now_ms(), 0) and self-corrects via the first incoming gossip.
API:
HlcState::new(clock)-- default 5-minute drift boundHlcState::with_max_drift(clock, max_drift_ms)-- custom bound (tests)stamp() -> HlcTimestamp-- always strictly greater than every prior stamp from this state; wait-free in the uncontended casereceive(remote) -> Result<HlcTimestamp, HlcError>-- advances local state to dominate both. ReturnsDriftTooLargeand leaves state unchanged ifremote.physical_ms > now + max_drift_ms
Shared via Arc<HlcState> across handlers without further
synchronisation -- the single AtomicU64 serialises all updates.
NodeHandle
#![allow(unused)] fn main() { struct NodeHandle { cmd_tx: mpsc::Sender<Command>, local_peer_id: PeerId, db: Arc<ChatDb>, listen_addrs: Arc<RwLock<Vec<Multiaddr>>>, } }
Methods: shutdown(self) -- cancel token + await task.
Returned by run_node(AppConfig).
HandlerContext
#![allow(unused)] fn main() { struct HandlerContext<'a> { db: Arc<ChatDb>, swarm: &'a mut Swarm<MyBehaviour>, pending_queries: &'a mut HashMap<[u8; 16], PendingQuery>, peer_cache: &'a PeerCache, api_registry: &'a HashMap<PeerId, String>, // connected peers' advertised API URLs local_peer_id: PeerId, local_peer_id_str: Arc<str>, // cached PeerId string (avoids Base58 per request) db_write_tx: DbOpSender, inbox_batch_tx: Option<InboxBatchSender>, hlc: Arc<HlcState>, } }
Configuration
#![allow(unused)] fn main() { struct AppConfig { keypair: Keypair, // secp256k1 listen: Multiaddr, // e.g. /ip4/0.0.0.0/tcp/4001 bootnodes: Option<Vec<Multiaddr>>, listen_api: Option<SocketAddr>, // e.g. 0.0.0.0:3000 db_path: Option<String>, expose_metrics: bool, metrics_listen: Option<SocketAddr>, sync_interval_secs: u64, // Merkle sync tick (default 30) public_api_url: Option<String>, // advertised API base URL (identify announcement) } }
TOML config file format (loaded via config::load_config):
private_key = "0x..."
listen = "/ip4/0.0.0.0/tcp/4001"
bootnodes = ["/ip4/.../tcp/4001/p2p/..."]
listen_api = "0.0.0.0:3000"
db_path = "./chatdb-data"
expose_metrics = true
metrics_listen = "0.0.0.0:9090"
# Publicly reachable base URL of this node's HTTP API. Advertised to
# peers via libp2p identify; omit on relay-only / private nodes.
public_api_url = "https://node1.example.com:8080"
public_api_url is validated on load (http(s):// scheme, no whitespace,
non-empty host, <= 256 bytes; trailing / is stripped).