Mxr Web App
Mxr web app
The mxr web app is the browser GUI for Mxr. It lives in apps/web/, uses Vite, React 19, TypeScript, TanStack Router, TanStack Query, Tailwind 4, and shadcn/Radix-style components, and talks to the daemon through the HTTP and WebSocket bridge.
It exists because terminal-first does not mean terminal-only. The daemon is the product core. The web app is one more client of that core, useful for richer inspection, dashboards, onboarding, settings, and mail-reading workflows that benefit from a graphical layout.
What it ships
- A React SPA served by
mxr web. - A bridge-auth flow that auto-fetches the local bearer token from
/api/v1/auth/local-tokenwhen the caller is on loopback. - Auth-gated OpenAPI and Swagger routes. The docs route is part of the bridge capability surface, not a public exception.
- Deep-linkable routes for mailboxes, search, compose, rules, accounts, analytics, diagnostics, invites, activity, and settings.
- An action registry so the command palette, global keymap, help dialog, settings, and status hints do not drift apart.
- Optimistic mailbox mutations with rollback and undo affordances.
- A reader that sanitizes HTML mail before rendering and keeps remote-image loading separate from tracker stripping.
Why this shape
The web app does not call providers. It does not get special access to SQLite. It does not invent a second mail model. It talks HTTP to crates/web, which talks Unix socket IPC to the daemon. That keeps the GUI honest: if a capability cannot go through the bridge, the daemon contract is probably incomplete.
The distribution choice matters too. The built SPA is embedded in the daemon binary when the web-ui feature is enabled. One artifact ships the server and UI together, so version skew is harder to create by accident. That lesson became Embedded SPA in Daemon Binary.
Boundaries worth remembering
Feature parity needs names. Daemon capability, CLI command, TUI action, web action, page-local keyboard behavior, and terminal-only view state are different promises. The web app should expose durable product actions, but it does not need to copy every terminal movement key.
The runtime identity boundary applies here too. A Vite dev server defaults to the mxr-dev instance and reads that instance's bridge files unless MXR_INSTANCE or MXR_BRIDGE_URL says otherwise. That keeps cargo run and local web development away from the installed mxr runtime.
Bridge security now has a small but important extra rule: API docs are not a free unauthenticated island. /api/v1/openapi.json and /api/v1/docs reject missing tokens, while /api/v1/health stays open for liveness and /api/v1/auth/local-token stays loopback-only for same-machine bootstrap.
Code grounding
apps/web/package.jsonnames the web app package and current frontend stack.apps/web/src/routes/owns page routes.apps/web/src/lib/actions/owns the shared action registry.apps/web/src/lib/localHandshake.tsandapps/web/src/lib/tokenStorage.tsown local bridge auth behavior.crates/web/owns the HTTP and WebSocket bridge.docs/web-app.mdis the maintainer note with locked decisions and implementation traps.site/src/content/docs/guides/web-app.mdis the user-facing docs page.
Lessons
- A GUI client should prove the daemon contract, not bypass it.
- A shared action registry is cheaper than reconciling palette, shortcuts, help, and settings later.
- Embedding the SPA into the daemon binary is boring in the best way: one release, one version, one support story.
- Dev web work needs the same runtime identity boundary as CLI and TUI work.
- API documentation is still an authority map. Keep it behind the same local bridge boundary as the API itself.