Startup and Configuration
July 15, 2026 · View on GitHub
This guide explains Mostrix’s boot sequence and configuration surfaces.
Overview
- Entry:
src/main.rs:98 - Initializes database, derives identity keys, initializes settings, then logger, terminal (raw mode), shared state, Nostr client, and background tasks.
- Shows an animated startup splash (Mostro wordmark + loading dots) while post-terminal init runs; see Startup splash below.
- Enters the main event loop to handle UI updates and user input.
Startup splash
After the terminal enters alternate screen mode, Mostrix draws a full-screen splash until background boot work finishes (src/startup.rs, src/ui/startup_splash.rs).
- Wordmark: multi-line logo from the project art (same style as the desktop
logo.txtexport). - Loading dots: reusable glyph
<>repeated 1–4 times on the last logo row, each prefixed by a space, cycled every ~400 ms while the splash tick runs (~150 ms redraw interval). - Phase text: short status under the art (
Starting…,Connecting to relays…,Loading market data…,Restoring chats…,Almost ready…) updated as init steps complete. - Minimum display: splash stays visible for at least ~800 ms so fast boots do not flash the screen.
- Narrow terminals: if the terminal is narrower than the padded logo width, a one-line
mostro is loading+ dots + phase is shown instead. - Background: full-screen fill via
fill_splash_background(solidBACKGROUND_COLORblock, same pattern as the Exit tab) so ASCII art and status text do not show mismatched per-span backgrounds. - CI / scripts: set
MOSTRIX_NO_SPLASH=1to skip the splash loop and run init directly.
Initialization Sequence
1. Database Initialization
The database is initialized at startup to ensure the schema is ready.
Source: src/db.rs
- Creates the SQLite database file at
~/.mostrix/mostrix.db. - Ensures tables exist (
orders,users). - If the
userstable is empty,User::new()generates a new 12-word BIP-39 mnemonic and persists it in theuserstable (this mnemonic is the root for user identity/trade key derivation). - For existing databases, runs migrations automatically to keep the schema up to date.
2. Settings Initialization
Mostrix uses centralized settings management in src/settings.rs.
Source: src/settings.rs
pub fn init_settings(identity_keys: Option<Keys>)
-> Result<InitSettingsResult, anyhow::Error>
- On first run,
settings.tomlis generated from an embedded template compiled into the binary (rather than copying from the repo root). - If
identity_keysis provided (derived from the DB identity/index-0 key), Mostrix derives thensec_privkeyforsettings.tomlso DB keys and settings keys match. - The returned
InitSettingsResult.did_generate_new_settings_fileindicates whether this process generated a brand-newsettings.toml. - When
did_generate_new_settings_fileistrue,main.rsshows theBackupNewKeyspopup overlay immediately on the current initial tab, prompting the user to save the generated 12-word mnemonic.
Error Handling: Startup failures in init_settings() are propagated as anyhow::Error (causing a clean process exit with an error message). If settings are accessed later at runtime before initialization (via the SETTINGS global), those failures are surfaced as user-friendly messages using OperationResult::Error instead of panicking. This ensures graceful degradation and clear feedback to users in both cases.
3. Logger Setup
Logging is configured via setup_logger in src/main.rs.
Source: src/main.rs:41
fn setup_logger(level: &str) -> Result<(), fern::InitError> {
let log_level = match level.to_lowercase().as_str() {
// ... level mapping ...
};
Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"[{}] [{}] - {}",
Local::now().format("%Y-%m-%d %H:%M:%S"),
record.level(),
message
))
})
.level(log_level)
.chain(fern::log_file("app.log")?) // Writes to app.log
.apply()?;
Ok(())
}
- Sets the log level based on the
log_levelfield insettings.toml. - Outputs log messages to
app.log.
4. TUI Initialization
The TUI uses ratatui with the crossterm backend.
Source: src/main.rs:104
enable_raw_mode()?;
let mut out = stdout();
execute!(
out,
EnterAlternateScreen,
crossterm::event::EnableMouseCapture
)?;
let backend = CrosstermBackend::new(out);
let mut terminal = Terminal::new(backend)?;
- Enables terminal raw mode.
- Enters the alternate screen and enables mouse capture.
Configuration Structure
The Settings struct defines all available configuration options.
Source: src/settings.rs
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Settings {
pub mostro_pubkey: String,
pub nsec_privkey: String,
pub admin_privkey: String,
pub relays: Vec<String>,
pub log_level: String,
pub currencies_filter: Vec<String>,
#[serde(default = "default_user_mode")]
pub user_mode: String, // "user" or "admin", default "user"
#[serde(default)]
pub ln_address: String, // Lightning address for buyer receive; empty = unset
#[serde(default)]
pub blossom_servers: Vec<String>, // Blossom upload hosts; empty = built-in defaults
}
Fields:
mostro_pubkey: The public key of the Mostro instance to interact with.nsec_privkey: The user's Nostr private key (nsec format).admin_privkey: The admin's private key, required for solving disputes when in admin mode.relays: A list of Nostr relay URLs to connect to.log_level: The verbosity of logging (e.g., "debug", "info", "warn", "error").currencies_filter: Optional list of fiat currency filters (ISO codes).- When empty, all currencies published by the Mostro instance are shown.
- When non-empty (e.g.
["USD"],["USD", "EUR"]), only orders whose fiat code is in this list are displayed.
user_mode: Either "user" or "admin". Controls the UI and available actions.ln_address: Optional Lightning address (user@domain.com) used when the local user acts as buyer (receive via LNURL-pay). The embedded template includesln_address = "". Oldersettings.tomlfiles without this key still load (#[serde(default)]yields an empty string). Saving from the Settings tab runs an async check that the LNURL metadata URL returns JSON withtag: "payRequest"before writing disk (spawn_verify_and_save_ln_address_taskinsrc/ui/key_handler/async_tasks.rs, helper insrc/util/ln_address.rs). The spawned task reports onln_address_result_tx(LnAddressVerifyResult), not onorder_result_tx, so settings verification does not share the order/dispute result queue. Clear removes the value without a network call.blossom_servers: Optional list of HTTPS Blossom bases for My Trades attachment upload (Ctrl+O send). When empty, Mostrix usesDEFAULT_BLOSSOM_SERVERSinsrc/util/blossom.rs(same defaults as Mostro Mobile). Example in reposettings.toml: commented# blossom_servers = ["https://blossom.primal.net", …]. Resolved at send time viablossom_servers_from_settingsinsrc/util/send_attachment.rs(main loop reloads settings from disk when draining the send queue).
Proof-of-work for published events is taken from the Mostro instance status event (kind 38385, tag pow), not from settings.toml.
Mostro instance info (kind 38385)
Background and manual refresh (Mostro Info tab → Enter) fetch the daemon status event and update UI state:
AppState.mostro_info: parsed tags (pow,bond_enabled,protocol_version, LND metadata, …) — seemostro_info_from_tags.AppState.transport: resolved wire transport for protocol DMs viatransport_from_instance. Updated throughAppState.set_mostro_info(startup await, main loopMostroInfoFetchResult, reconnect, invalid-pubkey clear).- Startup: when relays are reachable,
run_post_terminal_startupawaitsfetch_mostro_instance_infobefore spawning the DM listener so the first subscription uses the correct transport (v1 GiftWrap or v2 kind 14). On fetch failure or offline boot, transport defaults to GiftWrap.
Displayed on the Mostro Info tab: protocol version (1 / 2 / unknown) and wire transport label (GiftWrap vs NIP-44 direct).
Nostr & Background Tasks
Nostr Client Connection
Mostrix initializes a nostr_sdk::Client with the user's keys, adds configured relays, and
connects using a panic-safe wrapper (connect_client_safely).
Current startup behavior:
- Trims relay strings and skips empty entries before adding.
- Computes
relays_reachablewithany_relay_reachablefor offline UI behavior. - Calls
connect_client_safely(&client)(instead of rawclient.connect().await) to prevent background panic crashes when connectivity is unstable. - Logs a warning if no configured relays are reachable at boot.
Background Tasks
Several background tasks are spawned to keep the UI and data in sync:
- Order Refresh: Periodically fetches pending orders from Mostro.
- Relay order DB reconcile (startup + ~30s orders updater):
run_relay_order_db_reconcile_once(bulk terminal sync from nostr order events) andrun_targeted_relay_order_db_reconcile_tick(round-robin per-order fetch for local non-terminal trades with keys). Seerelay_order_db_reconcile.rsand MESSAGE_FLOW_AND_PROTOCOL.md (Relay → SQLite section). - Trade Message Listener: Listens for new messages related to active orders.
- Network Status Monitor:
spawn_network_status_monitorruns every 5 seconds.- Re-checks relay reachability from disk settings and emits
NetworkStatus::Offline/Online. - On
Offline, startup overlay text indicates automatic retry. - On
Online,main.rstriggersreload_runtime_session_after_reconnect(...)to reconnect and reload runtime background tasks.
- Shared-key chat subscription router (
listen_for_chat_messagesinsrc/util/chat_listener.rs):- A single long-lived task (spawned in
src/startup.rs, respawned on reconnect/key reload) maintains one relay subscription whose filter batches all active shared-key pubkeys (kind: 1059,#p: [shared pubkeys]) — the same model as Mostro Mobile'sSubscriptionManager. No timed polling. - Track/untrack via the global command channel (
ChatRouterCmd, published byset_chat_router_cmd_tx). Helpers:track_order_chat/untrack_order_chat/track_dispute_chat/untrack_dispute_chat. - On
TrackChatKey, the router hydrates history once (fetch_gift_wraps_for_shared_key, filtered by the last-seen cursor). Buffered track/untrack commands are drained and applied before a single batched resubscribe (idempotent re-tracks are no-ops). Subscribe failures retry with short backoff. - Live
kind: 1059gift wraps are routed by the event'sptag to the owning shared key, decrypted withunwrap_giftwrap_with_shared_key, and emitted on the existingadmin_chat_updates/user_order_chat_updateschannels. - Application is unchanged:
ui::helpers::apply_admin_chat_updates/apply_user_order_chat_updates(insrc/ui/helpers/startup.rs) still merge updates, persist cursors (buyer_chat_last_seen/seller_chat_last_seen, order transcripts), and dedupe. - Startup track set (option B):
track_startup_chatsemits tracks forOrder::get_startup_active_ordersrows (active +success) with a resolvable shared key (User), and eachInProgressdispute's buyer/seller shared key (Admin). - Untrack when an order reaches
TERMINAL_DM_STATUSESor its row is removed/reverted (DM router hooks), or a dispute leavesInProgress. Chat is not untracked onsuccess; on-disk transcripts preserve history for untracked terminal orders.
- A single long-lived task (spawned in
Source: src/util/chat_listener.rs (router), src/startup.rs + src/ui/helpers/startup.rs (track_startup_chats), src/util/dm_utils/mod.rs (track/untrack hooks), src/ui/helpers/startup.rs (apply_admin_chat_updates)
- DM Router Wiring (trade messages):
- App channel creation includes
dm_subscription_tx/dm_subscription_rx. set_dm_router_cmd_tx(dm_subscription_tx.clone())publishes the sender globally forwait_for_dm(returnsResult; startup fails fast if the mutex is poisoned).- Before spawning the listener,
hydrate_startup_active_order_dm_stateloads non-terminal orders from SQLite and returnsactive_order_trade_indicesplusorder_last_seen_dm_tscursors;main.rsseeds the shared active-order map. listen_for_order_messages(client, mostro_pubkey, transport, pool, …, order_last_seen_dm_ts, …, dm_subscription_rx)runs as the single router loop consuming:TrackOrdercommands for long-lived trade subscriptions.RegisterWaitercommands for one-shot request/response waits.
- After bootstrapping per-order protocol-DM subscriptions (
ensure_order_dm_subscription), the listener performs afetch_eventsreplay (fetch_and_replay_startup_trade_dms) so the Messages UI is populated from relay history (in-memory messages are not stored in the DB). Replay usesnotify: falseto avoid duplicate popups/badge noise. - Startup transport:
startup.rsawaits instance info when relays are reachable, then spawns the listener with resolvedapp.transport. - Reload / reconnect transport:
dm_transport_for_mostrore-fetches instance info and updatesapp.transportbefore respawning the listener (key reload, fetch-scheduler reload, network reconnect). - This unifies in-flight response handling and background trade notifications on top of one notification stream.
- App channel creation includes
See DM_LISTENER_FLOW.md for DmSubscriptionMode (StartupCatchUp, StartupSince, LiveOnly), filter_protocol_dm_from_mostro, waiter vs TrackOrder ordering, and replay details.
Admin Chat Restore at Startup
In addition to the background scheduler, Mostrix restores admin chat state during startup:
- All persisted admin disputes are loaded from the
admin_disputestable. - For disputes in
InProgressstate,ui::helpers::recover_admin_chat_from_files:- Reads chat transcripts from
~/.mostrix/disputes_chat/<dispute_id>.txt(if present). - Reconstructs
AppState.admin_dispute_chatsso the "Disputes in Progress" tab immediately shows prior messages. - Updates in‑memory
admin_chat_last_seenentries for Buyer and Seller based on file timestamps.
- Reads chat transcripts from
- Subsequent background NIP‑59 fetches use the stored
buyer_chat_last_seen/seller_chat_last_seenvalues as cursors, ensuring:- Instant UI restore after restart.
- Incremental network sync without replaying the full chat history from relays.
User order chat restore at startup (My Trades)
For User role, Mostrix restores peer-to-peer order chat alongside trade DMs:
- Cached transcripts live under
~/.mostrix/orders_chat/<order_id>.txtand are loaded intoAppState.order_chatsbyload_user_order_chats_at_startup. - Attachment rows in transcripts are stored as JSON (
image_encrypted/file_encryptedviaserialize_attachment_for_transcript) so Ctrl+S and file counts work immediately after restart; legacy[Image: … - Ctrl+S to save]lines are hydrated in memory when relay returns the same attachment at the same timestamp. - Disk restore via
load_user_order_chats_at_startupseedsAppState.order_chatsandorder_chat_last_seen. Relay history is hydrated once by the shared-key chat subscription router whentrack_startup_chatsemitsTrackChatKey— no separate startup poll and no timed polling. apply_user_order_chat_updatesskips relay echoes of the local trade pubkey; peer dedup is scoped to existing Peer rows so optimistic You sends are not mirrored as Peer and do not suppress unrelated peer text at the same timestamp. See MESSAGE_FLOW_AND_PROTOCOL.md — "User order chat local cache".
Main Event Loop
The TUI runs in a tokio::select! loop that handles (among others):
- Fatal errors:
fatal_error_rx— aborts background work and shows an error popup. - Network status:
network_status_rx— offline overlay vs reconnect + runtime reload. - Order / dispute / attachment / observer async results:
order_result_rx—OperationResult; includes dispute-list refresh side effects for certainInfomessages and My Trades DB resync forOrderHistoryDeleted. - Lightning address verify-and-save (settings):
ln_address_result_rx—LnAddressVerifyResult; mapped toOperationResult::Info/Errorand passed tohandle_operation_resultso UI behavior matches other operation-result popups without mixing traffic intoorder_result_rx. - Key rotation / seed words / message notifications / admin & user chat fetches / Mostro instance info / user input / periodic ticks: see
src/main.rs(create_app_channelsinsrc/ui/key_handler/async_tasks.rslists all paired senders and receivers, includingsave_attachment_tx/rxfor Ctrl+S downloads andsend_order_attachment_tx/rxfor outbound My Trades uploads viaSendOrderAttachmentJob). User order chat results arrive onuser_order_chat_updates_rxand are applied viaapply_user_order_chat_updates.
Source: src/main.rs (outer loop + tokio::select! + terminal.draw).
// Simplified shape (not exhaustive — see src/main.rs for full select!)
loop {
tokio::select! {
// fatal_error_rx, network_status_rx, ...
result = order_result_rx.recv() => { apply_order_result(...) }
ln_address_verify = ln_address_result_rx.recv() => { /* map LnAddressVerifyResult → OperationResult */ }
// key_rotation_rx, seed_words_rx, message_notification_rx, ...
maybe_event = events.next() => { /* handle_key_event, paste, mouse */ }
_ = refresh_interval.tick() => { /* 150 ms — redraw even without input */ }
}
// Before every frame (not only on keypress):
drain_save_attachment_queue(...) // start Blossom downloads queued by Ctrl+S popups
drain_send_order_attachment_queue(...) // Ctrl+O / Ctrl+Shift+O jobs: encrypt → Blossom → DM
drain_order_result_queue(...) // apply OperationResult (e.g. "Saved to …", "Attachment sent: …")
expire_attachment_toast(&mut app);
terminal.draw(|f| ui_draw(f, &app, &orders, Some(&status_line)))?;
}
Why drain before draw: My Trades Enter on the save-attachment popup may enqueue the download asynchronously (DB lookup for decryption key). Outbound sends enqueue on send_order_attachment_rx the same way (FromPath or RetryPrepared). Without draining save_attachment_rx, send_order_attachment_rx, and order_result_rx on each frame, success/error popups (including upload-ok/send-failed) could appear only after an unrelated keypress. The 150 ms refresh_interval tick plus this drain keeps attachment save and send feedback timely.