Hexagonal, not by accident
mxd separates domain logic from transport and persistence using a ports-and-adapters architecture. The domain core — a library crate — owns protocol semantics without specifying how external needs are met.
Two adapters connect the domain to reality: Wireframe for transport, Diesel for storage. This boundary is enforced structurally. The domain crate has no wireframe imports. Transport-specific types do not cross the adapter threshold.
Domain core and adapters
The domain core is a library crate that owns protocol semantics: how a login is validated, how a news thread's linked-list pointers update, how privileges gate operations. It defines ports for what it needs from the outside world — a user record, a database connection, a way to push a notification — without specifying how those needs are met.
Wireframe transport adapter
Handles the Hotline binary protocol: TCP socket management, the 12-byte handshake, the 20-byte frame codec, transaction routing, XOR detection, client compatibility shims, and the streaming and push primitives that shape later subsystem work.
Diesel storage adapter
Implements database operations for SQLite and PostgreSQL through a single set of repository functions, with backend selection at compile time via Cargo features.
Structural enforcement: The WireframeRouter is the sole public routing entrypoint; internal routing functions are pub(crate). New routes cannot accidentally bypass compatibility hooks because there is no public surface through which to do so.
Runtime modes and binary selection
mxd ships two operational binaries whose availability depends on the
legacy-networking
Cargo feature. With the feature enabled (the default), both binaries
are produced. Disabling it removes the legacy frame handler, causes
server::run()
to delegate to the Wireframe runtime, and omits the
mxd
binary via
required-features.
Legacy networking
The
mxd
binary. Uses the original frame handler with the legacy
networking runtime. Available when the
legacy-networking
feature is enabled (the default).
cargo build produces both
mxd and
mxd-wireframe-server.
Wireframe-only
The
mxd-wireframe-server
binary only. Built with
--no-default-features --features "sqlite toml".
The legacy binary is omitted entirely. This is a
deliberate build configuration, not an accidental
omission. Tested with
make test-wireframe-only.
Shared CLI surface:
Both binaries share the CLI module
(mxd::server::cli),
environment overrides,
.mxd.toml
defaults, and admin commands such as
create-user
(via
server::admin::run_command).
The operator experience is identical regardless of which binary
is running.
Wireframe capabilities that power mxd
Wireframe is the transport adapter that sits behind mxd's transport port. The domain core defines what it needs — framed request dispatch, streaming bodies, server-initiated push — and Wireframe fulfils those contracts without leaking transport concerns back across the boundary.
Because the adapter owns connection lifecycle, codec selection, and middleware layering, the domain remains testable in isolation: mock the port, exercise the domain, and verify invariants without a running TCP stack. What follows are the adapter-side capabilities that make that contract work.
Framing and streaming
The adapter translates Hotline's binary wire format into the typed requests the domain port expects, and reassembles multi-fragment payloads before they cross the boundary.
- ▶
FrameCodectrait:frame_payload(),wrap_payload(),correlation_id(),max_frame_length() - ▶ Fragmenter/Reassembler with
MessageId, sequentialFragmentIndex, max assembled size,purge_expired - ▶
RequestPartsseparation of routing metadata from streaming bodies - ▶
Responseenum: single frame, vector, streamed, channel-backed multi-packet
Middleware and lifecycle
Connection setup, teardown, and middleware layering are adapter responsibilities. The domain sees dispatched requests; the adapter handles everything before and after.
- ▶
from_fnasync middleware withframe(),frame_mut(),Nextcontinuation - ▶ Setup/teardown callbacks per connection with retained state
- ▶ Read timeouts and preamble-timeout handling
Push queues and observability
Server-initiated push and metrics live in the adapter, fulfilling the domain's notification port without exposing transport-level queuing to the core.
- ▶
PushQueueswith high/low-priority capacities, rate limits, dead-letter queue - ▶
ConnectionActor::run()with biasedselect!over shutdown, push, response - ▶ Optional
metricsfeature:wireframe_connections_activegauge, frame counters, error counters, panic counter
Compatibility guardrails
WireframeRouter embeds a CompatibilityLayer that orchestrates request-side and reply-side hooks on every routed transaction. On the request path, it decodes XOR-encoded payloads and records the login version. On the reply path, it augments responses with client-specific extras — banner fields for Hotline 1.8.5 and 1.9 clients, omission of those fields for SynHX.
// Request → Compatibility hooks → Domain → Reply hooks impl CompatibilityLayer { fn on_request(&mut self, frame: &mut Frame) { if frame.is_xor_encoded() { frame.decode_xor(); self.client_uses_xor = true; } } fn on_reply(&self, reply: &mut Frame) { if self.client_kind == ClientKind::Hotline185 { reply.add_banner_fields(); } } }
Hook ordering is verified by a spy-based test: on_request → dispatch → on_reply, asserted for both login and non-login transactions. This is the kind of property that looks obvious until someone adds a new route and forgets to wire it through the router. The test exists so that "forgets" becomes "gets a failing CI run."
Authentication and reply augmentation
Login handling splits into two responsibilities. AuthStrategy owns request validation and credential checking. LoginReplyAugmenter decorates the reply with fields determined by client compatibility metadata. Both are selected by ClientKind and orchestrated by the compatibility layer.
Client-specific behaviour
Conservative fallback path. Unknown and legacy versions stay protocol-valid without claiming feature parity.
Login flow omits banner fields. Broader field-level parity remains roadmap work.
Login replies add banner fields 161/162, but user-list and messaging parity remain pending later roadmap items.
Extensibility: The split exists to give HOPE reply extensions and SynHX hashed authentication a place to land without reopening the core login handler. Those remain extension points, not current shipped behaviour.
Architecture diagrams
The architecture page is generated from docs/design.md and the three ADRs. The detailed diagrams and class relationships are pulled from the source documents.
Hexagonal architecture layers
Protocol semantics, business logic, pure functions
Transport adapter: TCP, frames, routing, compatibility
Storage adapter: SQLite/PostgreSQL, repositories, migrations
Dependency direction: Domain depends on nothing. Adapters depend on domain. Application wires adapters to domain through dependency injection.