---
name: App lifecycle
type: architecture-map-page
slice: 02-tauri-runtime
last_verified: 2026/05/12
---

# App lifecycle

> **Where you are:** [Architecture map](../README.md) → [Tauri runtime](README.md) → App lifecycle

**Plain English summary.** This is the entry point. `main.rs` calls `lumotia_lib::run()` and `lib.rs::run` does everything that has to happen before the user sees a window: initialises tracing, checks the Linux launcher env-var contract, installs the Rust panic hook, registers Tauri plugins, opens the SQLite database, prunes the error log, builds a JS preferences-injection script, configures the WebKit media-permission grant on Linux, wires close-to-tray, populates `AppState` and the per-domain managed states, emits any runtime warnings, sets up the system tray, and finally registers all 71 Tauri commands.

## At a glance

- Path: `src-tauri/src/main.rs`, `src-tauri/src/lib.rs`.
- LOC: 5 (main) + 491 (lib).
- Tauri commands exposed directly here: `save_preferences` (string preferences -> SQLite settings table). All other commands live under `commands::*` and are registered via `tauri::generate_handler!`.
- Events emitted directly here: none (runtime warnings are emitted by `commands::models::emit_runtime_warnings`, called from setup).
- Depends on: `tauri`, `sqlx::SqlitePool`, `lumotia_core::types::EngineName`, `lumotia_llm::LlmEngine`, `lumotia_storage::{init, database_path, get_setting, set_setting, prune_error_log}`, `lumotia_transcription::LocalEngine`, plus the `commands::*` and `tray` modules.
- Called from frontend at: every `invoke()` site in slice 01 lands in the handler list registered here.

## What's in here

### `main.rs`

Single function. Sets `windows_subsystem = "windows"` for release builds (no console window) and calls `lumotia_lib::run()`. (`src-tauri/src/main.rs:1`).

### `lib.rs`

Module declarations (`src-tauri/src/lib.rs:1`):

- `mod commands;`
- `#[cfg(not(target_os = "android"))] mod tray;` — tray uses Tauri's `tray-icon` feature which is desktop-only.

Constants:

- `ERROR_LOG_RETENTION_DAYS: i64 = 90` (`src-tauri/src/lib.rs:22`). Used by the startup prune.

Types managed in Tauri state:

- `AppState` (`src-tauri/src/lib.rs:26`). Holds `Arc<LocalEngine>` for whisper and parakeet, the `SqlitePool`, and an `Arc<LlmEngine>`. This is the central state that almost every command queries.
- `PreferencesScript(pub String)` (`src-tauri/src/lib.rs:34`). Cached preferences-injection JS used when secondary windows are built (so they do not flash unstyled before Svelte mounts).

Functions:

- `build_preferences_script(prefs_json: Option<String>) -> String` (`src-tauri/src/lib.rs:38`). Builds an IIFE that reads saved preferences (theme, zone, accessibility settings: font family, font size, letter spacing, line height, transcript size, bionic reading, reduce motion) and applies them to `<html>` before the rest of the document loads. Embeds the JSON via `serde_json::to_string` to keep it safe.
- `save_preferences(state, preferences) -> Result<(), String>` (`src-tauri/src/lib.rs:73`). The single command in `lib.rs`. Persists the preferences blob to the SQLite settings table under key `lumotia_preferences`.
- `init_tracing()` (Linux/macOS/Windows). Initialises the process-wide tracing subscriber once, honours `RUST_LOG`, and writes structured startup/runtime logs to stderr.
- `warn_if_x11_env_unset_on_wayland()` (Linux only). Emits a `lumotia_startup` warning when the launcher has not pre-set `WEBKIT_DISABLE_DMABUF_RENDERER` (always expected on Linux), or `GDK_BACKEND=x11` / `WINIT_UNIX_BACKEND=x11` when `XDG_SESSION_TYPE=wayland`. It does not mutate the process environment; `run.sh` owns the dev-time contract and package wrappers/.desktop files must own the distribution-time contract.
- `run()`. The Tauri builder pipeline.

### `run()` step-by-step

1. **Tracing init.** Calls `init_tracing()` before any startup warnings/logs are emitted.
2. **Linux launcher contract check.** Calls `warn_if_x11_env_unset_on_wayland()` on Linux. Missing env vars produce warnings only; runtime env-var mutation was removed because Rust 2024 treats environment mutation in multi-threaded programs as unsafe.
3. **Panic hook.** Calls `commands::diagnostics::install_panic_hook()` to dump panic info to `crashes_dir()`.
4. **Plugin wiring (always-on).** `tauri_plugin_opener`, `tauri_plugin_dialog`, `tauri_plugin_notification` (`src-tauri/src/lib.rs:144`).
5. **Plugin wiring (desktop-only).** `tauri_plugin_global_shortcut`, `tauri_plugin_autostart` (LaunchAgent on macOS), `tauri_plugin_window_state` (`src-tauri/src/lib.rs:158`).
6. **Setup hook.** This is where the bulk of startup work lives:
    - Initialise SQLite via `lumotia_storage::init(&database_path()).await` using `tauri::async_runtime::block_on` (`src-tauri/src/lib.rs:180`). The `Instant::now()` timing is logged.
    - Prune `error_log` rows older than 90 days (`src-tauri/src/lib.rs:189`). Best-effort: a failure logs but does not block startup.
    - Load saved preferences from the settings table; build the JS injection script (`src-tauri/src/lib.rs:204`).
    - Apply the injection script to the main window via `WebviewWindow.eval()` (`src-tauri/src/lib.rs:215`).
    - On Linux, configure `webkit2gtk` permission requests: enable `media_stream` and `media_capabilities` settings; auto-grant audio capture but deny everything else (camera, geolocation, pointer lock, etc.) (`src-tauri/src/lib.rs:222`). This is the critical piece that makes `getUserMedia` work on Linux without a permission dialog (because WebKitGTK has no dialog, it just silently denies by default).
    - Wire close-to-tray on desktop: intercept `WindowEvent::CloseRequested` and call `window.hide()` instead of letting the platform exit (`src-tauri/src/lib.rs:281`).
    - Stash the `PreferencesScript` and the per-domain managed states (`HotkeyState`, `NativeCaptureState`, `LiveTranscriptionState`, `TtsState`, `MeetingState`) (`src-tauri/src/lib.rs:294`).
    - Build the `AppState` itself: fresh `LocalEngine`s for whisper and parakeet, the open `SqlitePool`, a fresh `LlmEngine` (`src-tauri/src/lib.rs:302`).
    - Emit runtime warnings (CPU baseline, Vulkan loader) via `commands::models::emit_runtime_warnings` (`src-tauri/src/lib.rs:312`).
    - Setup the system tray on desktop (`src-tauri/src/lib.rs:314`).
7. **Command registration.** `tauri::generate_handler![...]` lists 71 commands (`src-tauri/src/lib.rs:321`). The order in the macro is grouped by domain (preferences, models, LLM, transcription, audio, tasks, feedback, TTS, rituals, nudges, intentions, profiles, transcripts, diagnostics, live, windows, clipboard, fs, paste, meeting, hardware, hotkey, updater).
8. **Run.** `.run(tauri::generate_context!())` blocks the main thread until the app exits. Panics are wrapped with `expect("error while running Lumotia")`.

## Data flow

- **Frontend bootstrap:** the webview is built per `tauri.conf.json` window config. Tauri's `eval()` hook fires the preferences script before the SvelteKit bundle parses, so the user never sees a flash of unstyled content.
- **Database:** opened once, owned by `AppState`, cloned by `Arc` semantics into every `state.db` borrow.
- **Engine state:** the two transcription `LocalEngine`s and the `LlmEngine` are shared `Arc`s; commands clone them and run inference inside `tokio::task::spawn_blocking` so the async runtime stays responsive.
- **Per-domain state:** `HotkeyState`, `NativeCaptureState`, `LiveTranscriptionState`, `TtsState`, `MeetingState` are all stashed via `app.manage(...)` and retrieved by their command files via `tauri::State<'_, T>`.

## Watch-outs

- `tauri::async_runtime::block_on` inside `setup` blocks startup. The DB init and prefs read are explicitly timed and logged so regressions show up. Adding more synchronous async work here directly pushes the time-to-first-paint up.
- The Linux media-permission wire-up is non-fatal: if `with_webview` fails the app still boots, but `getUserMedia` will be silently denied or fall back to a prompt the user cannot answer (no UI). The error path logs a `lumotia_startup` warning.
- Linux rendering env vars are a launcher contract, not a runtime mutation. In development, use `npm run dev:tauri` / `./run.sh`; packaged Linux builds need an equivalent wrapper or `.desktop` `Exec=env` policy. `WEBKIT_DISABLE_DMABUF_RENDERER=0` remains the user opt-out for the DMA-BUF workaround.
- Close-to-tray works only on desktop (the `cfg!(not(target_os = "android"))` block). On Android, closing the activity terminates the process, which is the expected platform behaviour.
- The `prewarm_default_model` call is *not* wired here. `commands::models::prewarm_default_model` exists, but `setup` does not invoke it. The frontend invokes the matching `prewarm_default_model_cmd` command after the main page mounts. If you ever want to shift pre-warm into setup, watch the spawn_blocking ordering against the engine `Arc` clones.

## See also

- [Commands index](commands/README.md) — every command registered by `lib.rs::run`.
- [System tray](system-tray.md) — what `tray::setup(app)` builds.
- [Tauri config](tauri-config.md) — the window config that drives the `get_webview_window("main")` retrieval.
- [Capabilities and ACL](capabilities-and-acl.md) — the permission set that decides which commands each window can call.
- [Cargo and features](cargo-and-features.md) — the dependency block that determines which plugins compile in.
