Architecture
Four programs with strict jobs, one local protocol between them, and no server of our own anywhere.
Processes, and what each may never do
| Process | Does | Never |
|---|---|---|
chatd | Always on, one per data directory (it holds chatd.lock). Answers its socket first, then reads the sessions. Holds the sessions, the network sockets, SQLite and the media cache. Call signalling. | Decode media, link a codec or audio code, hold a member list or history in memory, fetch a link from a message, send anything you did not ask for. |
mediad | Exists from “connecting” to the end of one call. Voice gateway, UDP, Opus, Discord’s end-to-end encryption (DAVE), audio devices. | Run with no call, touch SQLite, the credential store or the chat socket. It speaks only to chatd, over stdin and stdout. |
tinyclient | The window. Draws what chatd reports and decodes images for the rows on screen. Starts chatd when nothing answers. | Talk to a network, read the database or the credential store, or block its main thread on a socket, file or decode. |
tinyclient-login | One sign-in window, then exits with the session on stdout. | Stay running. |
The local protocol
The window and chatd talk over a local socket: a Unix domain socket at chat.sock in the data directory, or the matching named pipe on Windows. Nothing listens on a network port.
- Frames: a 4-byte big-endian length, then JSON, at most 1 MiB. Either side refuses a larger frame before writing or reading it.
- Requests: one request, one response. A frame
chatdcannot parse is answeredUnsupported(“versions differ”) rather than dropped. - Changes: the window asks
Wait {since}, which blocks up to 30 seconds and answers every change newer thansince. The change list is bounded at 256 entries; a window that fell further behind is told to read again. - Bounded: pages are capped at 200 rows, and so is everything a network could make large: 25 voice occupants, 10 card fields, a character cap on every string that is not a message’s text.
- Compatible: every field added after the first version is optional and left off the wire when empty, so an older frame still reads.
chatd and mediad use the same framing over mediad’s stdin and stdout. Closing stdin is the hang-up. tinyclient-login prints its session as one line of JSON on stdout and says how it ended with its exit status.
The full contract is the code in crates/protocol, and the map of it is docs/architecture.md.
Storage
| Data | Where |
|---|---|
| Sessions | The system credential store, service tinyclient, one item per network. Never in the database. |
| Accounts, conversations, people, messages | chat.sqlite in the data directory, with a 1 MB page cache. |
| Media bytes | media/<network>/<account>/, 500 MB, least recently used evicted first. |
| Signal state | signal/<account>.state: keys, sessions, contacts and groups, each value sealed with a key that only the credential store item holds. |
| Presence, typing, voice occupants, upload progress | chatd’s memory, bounded, gone at restart. |
| Logs | crash.log (cut at 256 KB) and chatd.log (cut at 4 MB at start), credentials redacted. |
| Window state | The window’s own preferences file: last conversation, pane widths, scroll positions. |
Retention: the newest 2,000 messages per conversation, and nothing older than 90 days unless you opened the conversation in that time. Pruned at start and daily.
Adapters
Each network is one adapter inside chatd, on its own thread, mapping the network’s model onto one shared one: Spaces (a server, workspace or team), groups, conversations, threads and messages. No network markup reaches the window.
| Network | Built on | TLS |
|---|---|---|
| Discord | Discord’s HTTP API and gateway websocket, written in this repository | System |
| Slack | Slack’s web API and websocket, written in this repository | System |
| Teams | The chat service and push channel of Microsoft’s Teams client, written in this repository | System |
| Telegram | grammers, a pure-Rust MTProto client | MTProto’s own encryption |
| Signal | presage, on libsignal-service-rs and Signal’s libsignal | rustls, pinned to Signal’s certificate authority |
“System” TLS is the operating system’s: Security.framework on macOS, SChannel on Windows, OpenSSL on Linux. Telegram and Signal run on one single-threaded async runtime each, created only when such an account exists or a sign-in begins.
Idle means idle
- Adapters block on their sockets and command queues with no polling timer. The whole of
chatdhas a budget of about one wakeup a second. - No polling request to any network: sockets push.
- The window draws a frame only when state changed. A blinking caret, typing dots and animation stop when the window is not focused.
- Only rows on screen are drawn; lists are virtualized and image decodes are capped.
Calls
A call on Discord starts mediad, which connects to Discord’s voice servers itself, runs Discord’s end-to-end encryption (DAVE, through davey on OpenMLS) and encodes with Opus (libopus 1.3.1 transpiled to Rust, so no C toolchain). Every way a call can fail ends the same way: the reason stays on screen until dismissed, mediad is gone, and the network is told we left. Calls on other networks open in that network’s own app.
Sizes
| Program | Binary | Memory | Runs |
|---|---|---|---|
chatd | 28 MB | 10 MB RSS signed in to Discord | Always |
tinyclient | 22 MB | 64 MB RSS | While the window is open |
mediad | 5.5 MB | Under 40 MB RSS in a call (test-asserted) | During a call |
tinyclient-login | 1.8 MB | During a sign-in |