Skip to main content

Messaging — /api/msg* + /ws/msg

Direct messaging on the store-nothing relay doctrine: the server relays messages between online members and writes no message content anywhere. There is no messages table. What does persist is metadata the client needs (conversations, contacts, profiles, call logs — PII fields encrypted at rest) and, for each send, a hash-only transit receipt on the sender's machine — an entangled-state audit row that proves "a message transited at Zeqond N" without storing what it said.

Source: shared/api-core/src/routes/msg.ts (REST) + shared/api-core/src/lib/msgRelay.ts (the /ws/msg hub). Identity is read from the session on every call — there's no anonymous surface.

The privacy contract (pinned by tests)

  • Content never touches the DB — the message handlers perform no message inserts.
  • Content transits as ZSP envelopes — the client protects/unprotects; the relay moves opaque bytes.
  • The state-machine tether is a hash-only transit receipt on the sender's entangled state (msg.send).
  • History is device-local. GET …/messages returns an empty page by design — your client merges its own local store. Offline members do not receive; that's honest P2P semantics, stated in the client UI.

Message relay (store-nothing)

MethodPathAuthNotes
GET/api/msg/conversations/:id/messagesBearer (session)Returns { ok: true, p2p: true, messages: [] } — history lives on your devices.
POST/api/msg/conversations/:id/messagesBearer (session)Relay a message to online members + append the transit receipt. Member-gated.
PATCH/api/msg/messages/:idBearer (session)Edit relay (requires conversationId in the body — no server-side message lookup exists). {delete:true} relays a tombstone.
DELETE/api/msg/messages/:idBearer (session)Delete relay — tombstone applied client-side.
POST/api/msg/messages/:id/reactBearer (session)Reaction relay — the {zid, emoji} delta fans out; clients aggregate locally. Nothing stored.
POST/api/msg/conversations/:id/readBearer (session)Read-receipt relay.

All relay routes are member-gated: non-members of the conversation get 403 FORBIDDEN.

/ws/msg — the realtime hub

One WebSocket per signed-in client. On connect the hub sends { "type": "connected", "zid": … } and broadcasts your presence to peers who share a conversation with you.

  • Inboundping, typing, typing_stop, status_change, call signalling.
  • Outbound (fanout)message, edit, delete, reaction, typing, typing_stop, read, presence, pong, call events.

Unknown types are dropped silently (forward-compat). REST sends and WS fanout are the same relay — a message POSTed over REST arrives at members over their sockets.

Runnable example

# Send (relay + receipt, nothing stored server-side):
curl -s -X POST "https://zeqstate.com/api/msg/conversations/$CONV/messages" \
-H "Authorization: Bearer $ZEQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{"envelope":"<ZSP-protected payload>","clientMsgId":"m-001"}'
# → ok + how many online members the relay reached; the hash-only
# transit receipt lands on YOUR machine's entangled state.

Profile

MethodPathAuthNotes
GET/api/msg/profileBearer (session)Your own profile.
PATCH/api/msg/profileBearer (session)Update display name, bio, status.
GET/api/msg/profile/linksBearer (session)Your public profile-links blob.
PUT/api/msg/profile/linksBearer (session)Replace the links blob.
GET/api/msg/profile/links/:zidBearer (session)Another machine's public links.
GET/api/msg/profile/:zidBearer (session)Another machine's public profile.

Conversations & contacts

MethodPathAuthNotes
GET/api/msg/conversationsBearer (session)Conversation list.
POST/api/msg/conversationsBearer (session)Open / append to a conversation.
GET/api/msg/contactsBearer (session)Contact list.
POST/api/msg/contactsBearer (session)Add a contact.
GET/api/msg/contacts/search/:termBearer (session)Search contacts.
PATCH/api/msg/contacts/:zidBearer (session)Edit a contact (nickname etc.).
DELETE/api/msg/contacts/:zidBearer (session)Remove a contact.

Settings, presence & calls

MethodPathAuthNotes
GET/api/msg/settingsBearer (session)UI preferences (non-PII, settings_json blob).
PATCH/api/msg/settingsBearer (session)Update preferences.
PUT/api/msg/statusBearer (session)Set presence / status.
GET/api/msg/ice-serversBearer (session)ICE servers for WebRTC calls.
GET/api/msg/calls/historyBearer (session)Call history.
GET/api/msg/calls/missed-countBearer (session)Missed-call badge count.
POST/api/msg/calls/logBearer (session)Record a call.

Honest limits

  • No server-side history means no multi-device backfill — a fresh device starts empty.
  • Delivery is best-effort to currently online members; there is no store-and-forward queue.
  • The transit receipt proves a send happened on your entangled state; it cannot prove the recipient read it (read receipts are relays, also unstored).

See also