Architecture
May 15, 2026 ยท View on GitHub
mxr is local-first email infrastructure. The daemon is the system. TUI, CLI, web, scripts, and agents are clients.
For the full design record, read docs/blueprint/README.md. This file is the short version.
Core shape
TUI / CLI / web / scripts / agents
|
v
daemon
/ | \
SQLite Tantivy runtime features
|
provider adapters
SQLite is the source of truth. Tantivy is rebuildable from SQLite. Provider adapters map Gmail and IMAP into the internal model instead of leaking provider semantics upward. Provider-agnostic at the app layer does not mean flattening away real differences: labels-vs-folders is the main seam, and threading uses native IDs when available plus reconstruction otherwise.
IPC contract
Transport stays simple: length-delimited JSON over a Unix socket using IpcMessage { id, payload }.
The contract has four buckets:
core-mailSearch, sync, envelopes, bodies, threads, labels, drafts, send, mutations, attachments, export. This is the most stable bucket.mxr-platformAccounts, rules, saved searches, subscriptions, semantic runtime/profile management. These are real mxr product/runtime features, not mail timelessness and not client-only convenience.admin-maintenanceStatus, events, logs, doctor, bug-report, local reset, repair/inspection surfaces. These stay in IPC, but are conceptually fenced off from the core mail contract.client-specificPane state, selection state, sidebar collapse, grouped rows, right-rail payloads, widget-specific shaping. These stay in clients, not in the daemon.
Daemon rule: serve reusable truth and workflows, not screen payloads.
Provider rule: provider weirdness is handled below this layer in adapter crates, but capability differences stay visible where behavior actually differs.
Semantic retrieval
Semantic search is an mxr-platform feature, not a core mail requirement.
- mail still fundamentally works without semantic retrieval
- embeddings stay local
- sync may prepare semantic chunks even while semantic retrieval is disabled
- embedding generation happens only when semantic is enabled
- hybrid search keeps lexical BM25 and fuses in dense recall with RRF
- fielded dense queries intentionally respect chunk source kinds
- OCR is not part of active semantic indexing
That boundary matters. Do not blur exact lexical behavior and semantic recall into one fuzzy system.
Activity log
User-initiated actions are recorded in user_activity, captured at the daemon's IPC dispatch seam (crates/daemon/src/handler/mod.rs::handle_request). Every IPC request that mutates state or expresses user intent (search, view, mutation, draft action) produces one row, tagged with the originating client (tui | cli | web | daemon).
Storage: dedicated table + FTS5 mirror over context_json. Append-only; redaction is a tombstone, never a hard delete. Retention is tier-aware (30 / 90 / 365 days for ephemeral / standard / important by default, configurable per tier in [activity.retention]).
Capture seam: a single Recorder writes through a bounded mpsc channel. Failures are observability-only and never propagate to user-facing responses. The list of capturable IPC verbs lives in crates/daemon/src/activity/mapper.rs โ explicit per-variant mapping for the ~40 user-intent verbs; everything else returns None with a tracing::debug! for visibility. New IPC verbs default to "not captured" until someone decides what to log; this keeps the table from accumulating noise as the protocol grows.
Compaction: write-time coalescing folds rapid-fire duplicates (same action+target_id within 250 ms) into a single row with an incremented count. Applies only to ephemeral- and standard-tier rows; important-tier mutations are always written as-is to preserve audit fidelity.
Query: AdminMaintenance IPC bucket with verbs ListActivity / CountActivity / ActivityStats / ExportActivity / RedactActivity / PruneActivity / PauseActivity / ResumeActivity. CLI: mxr activity (alias mxr act). TUI: g a chord. Web: /activity route.
Invariants:
- Never transmitted off-device. No sync, no telemetry, no remote logging.
context_jsonnever holds credentials, tokens, password hashes, attachment bytes, or full mail bodies.MXR_ACTIVITY=offdisables the recorder for the lifetime of the daemon.mxr activity pauseis the runtime equivalent.- The recorder is the only path that writes to
user_activity.
Detailed implementation plan: docs/activity-log.md. User-facing privacy guide: site/.../guides/activity-log.md.
Lifecycle guarantees
Current runtime story:
- sync writes envelopes + bodies to SQLite immediately
- sync updates Tantivy immediately and commits lexical freshness per batch
- sync maintains labels, counts, threading, and cursor state
- daemon post-sync work persists semantic chunks for the newly upserted messages
- embedding generation + ANN refresh happen only when semantic is enabled or explicitly reindexed/profile-switched
Repair boundary:
- lexical search is repairable from SQLite at daemon startup
- semantic readiness is optional platform state layered on top
Principles
- Local-first
- Provider-agnostic internal model
- Daemon-backed architecture
$EDITORfor writing- Search is first-class
- Saved searches are product primitives
- Rules are deterministic first
- Shell hooks over premature plugin systems
- Adapters are swappable
- Correctness beats cleverness
Repo reality
- First-party adapters are live for Gmail, IMAP, SMTP, and Fake.
crates/webis a current client/bridge, not future work.- The product/install/package surface is the repo-root package
mxr. - Internal crates under
crates/are real workspace crates and are private by default (publish = false). - The IMAP adapter depends on the published
mxr-async-imapfork from crates.io; vendored source is not part of the workspace boundary model. - Architectural seams are enforced with Cargo dependencies.
#[path]pseudo-crates are not allowed.
What this means in practice
- CLI, TUI, and web should reuse daemon workflows instead of inventing separate mail logic.
- Web/TUI should shape their own views from reusable daemon data.
- Providers may use shared mail utility crates like
mail-parseandoutbound, but nevercompose. - Clients may use local utility crates like
config,compose,reader, andmail-parse, but they must not depend on daemon/store/search/sync/provider crates. - Search/status/doctor/events are all available over IPC, but only mail workflows define the core contract.
mxr reset --hard/mxr burnare CLI-only maintenance commands that wipe rebuildable local runtime state while preserving config and credentials by default.- Future contributors should classify new IPC first, then add it. Do not grow a junk drawer.