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

ProcessDoesNever
chatdAlways 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.
mediadExists 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.
tinyclientThe 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-loginOne 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.

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

DataWhere
SessionsThe system credential store, service tinyclient, one item per network. Never in the database.
Accounts, conversations, people, messageschat.sqlite in the data directory, with a 1 MB page cache.
Media bytesmedia/<network>/<account>/, 500 MB, least recently used evicted first.
Signal statesignal/<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 progresschatd’s memory, bounded, gone at restart.
Logscrash.log (cut at 256 KB) and chatd.log (cut at 4 MB at start), credentials redacted.
Window stateThe 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.

NetworkBuilt onTLS
DiscordDiscord’s HTTP API and gateway websocket, written in this repositorySystem
SlackSlack’s web API and websocket, written in this repositorySystem
TeamsThe chat service and push channel of Microsoft’s Teams client, written in this repositorySystem
Telegramgrammers, a pure-Rust MTProto clientMTProto’s own encryption
Signalpresage, on libsignal-service-rs and Signal’s libsignalrustls, 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

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

macOS arm64 release builds, 2026-10-14; chatd includes Signal’s libraries. Memory measured 2026-10-13. Every budget: 07-budgets-and-risk.md.
ProgramBinaryMemoryRuns
chatd28 MB10 MB RSS signed in to DiscordAlways
tinyclient22 MB64 MB RSSWhile the window is open
mediad5.5 MBUnder 40 MB RSS in a call (test-asserted)During a call
tinyclient-login1.8 MBDuring a sign-in

Edit this page on GitHub