Skip to main content
The LiquidLauncher backend is written in Rust and built on the Tauri framework, providing system-level operations, file management, and Minecraft launching capabilities.

Module Structure

The backend is organized into four primary modules:
src-tauri/src/
├── main.rs              # Application entry point
├── error.rs             # Error types
├── app/                 # Application & GUI layer
│   ├── mod.rs
│   ├── gui/             # Tauri integration
│   │   ├── mod.rs       # AppState and gui_main
│   │   └── commands/    # Command handlers
│   │       ├── auth.rs  # Authentication commands
│   │       ├── client.rs # Client/launch commands
│   │       ├── data.rs  # Data management
│   │       └── system.rs # System utilities
│   ├── client_api.rs    # LiquidBounce API
│   ├── options.rs       # Configuration
│   └── webview.rs       # WebView helpers
├── minecraft/           # Minecraft-specific logic
│   ├── mod.rs
│   ├── launcher/        # Core launcher
│   ├── version.rs       # Version profiles
│   ├── auth.rs          # MC authentication
│   ├── prelauncher.rs   # Pre-launch setup
│   ├── progress.rs      # Progress tracking
│   ├── rule_interpreter.rs
│   └── java/            # Java runtime
├── auth/                # Authentication module
│   └── mod.rs
└── utils/               # Shared utilities
    ├── checksum.rs
    ├── download.rs
    ├── extract.rs
    ├── hosts.rs
    ├── macros.rs
    ├── maven.rs
    └── sys.rs

Application State

The backend maintains application state using Tauri’s state management:
src-tauri/src/app/gui/mod.rs:31-41
pub struct AppState {
    pub runner_instance: Arc<Mutex<Option<RunnerInstance>>>,
}

pub struct RunnerInstance {
    pub terminator: tokio::sync::oneshot::Sender<()>,
}

impl AppState {
    pub fn new() -> Self {
        Self {
            runner_instance: Arc::new(Mutex::new(None)),
        }
    }
}
The AppState tracks the running game instance and provides a termination channel for stopping the game process.

Tauri Commands

Tauri commands are the primary interface between frontend and backend. They are registered in gui_main():
src-tauri/src/app/gui/mod.rs:46-82
pub fn gui_main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_updater::Builder::new().build())
        .plugin(tauri_plugin_process::init())
        .plugin(tauri_plugin_opener::init())
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_clipboard_manager::init())
        .manage(AppState::new())
        .invoke_handler(tauri::generate_handler![
            setup_client,
            check_system,
            sys_memory,
            get_options,
            store_options,
            request_branches,
            request_builds,
            request_mods,
            run_client,
            login_offline,
            login_microsoft,
            client_account_authenticate,
            client_account_update,
            logout,
            refresh,
            fetch_blog_posts,
            fetch_changelog,
            clear_data,
            default_data_folder_path,
            terminate,
            get_launcher_version,
            get_custom_mods,
            install_custom_mod,
            delete_custom_mod
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Command Categories

request_branches

Fetches available branches (stable, beta, etc.) from the LiquidBounce API:
src-tauri/src/app/gui/commands/client.rs:43-52
#[tauri::command]
pub(crate) async fn request_branches(client: Client) -> Result<Branches, String> {
    let branches = (|| async { client.branches().await })
        .retry(ExponentialBuilder::default())
        .notify(|err, dur| {
            warn!("Failed to request branches. Retrying in {:?}. Error: {}", dur, err);
        })
        .await
        .map_err(|e| format!("unable to request branches: {:?}", e))?;
    Ok(branches)
}

request_builds

Fetches builds for a specific branch:
src-tauri/src/app/gui/commands/client.rs:56-65
#[tauri::command]
pub(crate) async fn request_builds(
    client: Client, 
    branch: &str, 
    release: bool
) -> Result<Vec<Build>, String> {
    let builds = (|| async { client.builds_by_branch(branch, release).await })
        .retry(ExponentialBuilder::default())
        .await
        .map_err(|e| format!("unable to request builds: {:?}", e))?;
    Ok(builds)
}

run_client

The most complex command - launches the Minecraft client. See Launcher Core for details.

terminate

Stops the running game process:
src-tauri/src/app/gui/commands/client.rs:405-415
#[tauri::command]
pub(crate) async fn terminate(app_state: tauri::State<'_, AppState>) -> Result<(), String> {
    let mut lck = app_state.runner_instance.lock()
        .map_err(|e| format!("unable to lock runner instance: {:?}", e))?;
    
    if let Some(inst) = lck.take() {
        info!("Sending sigterm");
        inst.terminator.send(()).unwrap();
    }
    Ok(())
}
Handles Minecraft account authentication:
  • login_offline - Creates offline account
  • login_microsoft - Initiates Microsoft OAuth flow
  • logout - Removes stored account
  • refresh - Refreshes Microsoft account tokens
  • client_account_authenticate - Authenticates with LiquidBounce API
  • client_account_update - Updates account information
Manages application data and configuration:
  • get_options - Loads user options from disk
  • store_options - Saves user options to disk
  • clear_data - Clears launcher data
  • default_data_folder_path - Returns default data directory
  • get_custom_mods - Lists custom mods
  • install_custom_mod - Installs a custom mod file
  • delete_custom_mod - Removes a custom mod
System information and checks:
  • check_system - Validates system configuration
  • sys_memory - Returns available system memory
  • get_launcher_version - Returns launcher version string

Error Handling

LiquidLauncher uses Rust’s Result type with custom error enums:
src-tauri/src/error.rs:22-28
#[derive(Error, Debug)]
pub enum LauncherError {
    #[error("Invalid version profile: {0}")]
    InvalidVersionProfile(String),
    #[error("Unknown template parameter: {0}")]
    UnknownTemplateParameter(String),
}

Error Propagation

1
1. Error Occurs
2
Rust functions return Result<T, E>:
3
fn download_file(url: &str) -> Result<Vec<u8>> {
    // May fail with network error
}
4
2. Error Propagated with ?
5
The ? operator propagates errors up the call stack:
6
pub async fn setup_assets() -> Result<()> {
    let data = download_file(url)?; // Propagates error
    Ok(())
}
7
3. Error Converted at Boundary
8
Tauri commands convert Result to String for JavaScript:
9
#[tauri::command]
pub async fn my_command() -> Result<Data, String> {
    let result = fallible_operation()
        .map_err(|e| format!("Operation failed: {:?}", e))?;
    Ok(result)
}
10
4. Error Handled in Frontend
11
try {
    await invoke("my_command");
} catch (error) {
    console.error("Command failed:", error);
}

Connection Error Helper

For network errors, a helper provides user-friendly messages:
src-tauri/src/error.rs:30-35
pub fn map_into_connection_error(e: Error) -> Error {
    anyhow!(
        "Failed to download file. This might have been caused by connection issues. \
        Please try using a VPN such as Cloudflare Warp.\n\nError: {}",
        e
    )
}

Event System

For asynchronous updates (progress, logs), the backend emits events:
src-tauri/src/app/gui/commands/client.rs:234-247
fn handle_progress(
    window: &ShareableWindow,
    progress_update: ProgressUpdate,
) -> anyhow::Result<()> {
    window
        .lock()
        .map_err(|_| anyhow!("Window lock is poisoned"))?
        .emit("progress-update", &progress_update)?;
    
    if let ProgressUpdate::SetLabel(label) = progress_update {
        handle_log(window, &label)?;
    }
    Ok(())
}

Event Types

  • progress-update - Download/installation progress
  • process-output - Game stdout/stderr
  • client-error - Launch errors
  • client-exited - Game process terminated

HTTP Client

A shared HTTP client is used throughout the application:
src-tauri/src/main.rs:52-62
static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));

static HTTP_CLIENT: Lazy<Client> = Lazy::new(|| {
    let client = reqwest::ClientBuilder::new()
        .user_agent(APP_USER_AGENT)
        .build()
        .unwrap_or_else(|_| Client::new());
    client
});
The HTTP client includes retry logic using the backon crate with exponential backoff for reliability.

Async Runtime

Tauri commands run in Tokio’s async runtime:
src-tauri/src/app/gui/commands/client.rs:355-361
thread::spawn(move || {
    tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .unwrap()
        .block_on(async {
            // Async operations here
        });
});

Logging

The backend uses tracing for structured logging:
src-tauri/src/main.rs:67-86
let logs = LAUNCHER_DIRECTORY.data_dir().join("logs");
utils::clean_directory(&logs, 7)?; // Keep 7 days

let file_appender = tracing_appender::rolling::daily(logs, "launcher.log");

let subscriber = tracing_subscriber::registry()
    .with(EnvFilter::from("liquidlauncher=debug"))
    .with(
        fmt::Layer::new()
            .with_ansi(true)
            .with_writer(io::stdout),  // Console output
    )
    .with(
        fmt::Layer::new()
            .with_ansi(false)
            .with_writer(file_appender),  // File output
    );
tracing::subscriber::set_global_default(subscriber)
    .expect("Unable to set a global subscriber");
Logs are written to both console (with ANSI colors) and rotating daily files, kept for 7 days.

Utilities Module

The utils module provides shared functionality:

download.rs

File downloading with progress callbacks:
pub async fn download_file<F>(url: &str, progress_fn: F) -> Result<Vec<u8>>
where
    F: Fn(u64, u64),

checksum.rs

SHA1 checksum verification:
pub fn sha1sum(path: impl AsRef<Path>) -> Result<String>

extract.rs

Archive extraction (ZIP, TAR, etc.)

maven.rs

Maven artifact path resolution:
pub fn get_maven_artifact_path(artifact: &str) -> Result<String>

sys.rs

System detection:
pub static OS: &str = /* "linux", "windows", "macos" */;
pub static ARCHITECTURE: &str = /* "x86_64", "aarch64", etc. */;

macros.rs

Convenience macros:
macro_rules! mkdir {
    ($path:expr) => {
        std::fs::create_dir_all($path)?;
    };
}

Best Practices

Use ? for error propagation - Don’t use .unwrap() or .expect() in production code paths
Prefer async operations - Use tokio::fs instead of std::fs for file I/O to avoid blocking
Emit events for updates - Use the event system for progress updates rather than returning large data structures
Log appropriately - Use trace!, debug!, info!, warn!, error! at appropriate levels

Next Steps

Frontend Architecture

Learn about Svelte components and UI structure

Launcher Core

Understand the game launch process in depth