Skip to main content
LiquidLauncher provides a flexible system for selecting Minecraft versions through branches and builds.

Branch System

Builds are organized into branches, similar to Git branches:

Fetching Branches

src-tauri/src/app/client_api.rs
/// Request all available branches
pub async fn branches(&self) -> Result<Branches> {
    self.request_from_endpoint(API_V1, "version/branches").await
}
The API returns:
src-tauri/src/app/client_api.rs
#[derive(Serialize, Deserialize)]
pub struct Branches {
    #[serde(rename = "defaultBranch")]
    pub default_branch: String,
    pub branches: Vec<String>,
}
{
  "defaultBranch": "nextgen",
  "branches": ["nextgen", "legacy", "experimental"]
}

Branch Command

The frontend requests branches with retry logic:
src-tauri/src/app/gui/commands/client.rs
#[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)
}
Exponential backoff ensures the launcher remains functional even with temporary network issues.

Build Selection

Once a branch is selected, you can fetch available builds.

Fetching Builds

1

Select Branch

Choose a branch from the available branches
2

Choose Release Filter

Decide whether to show only release builds or all builds
3

Request Builds

Fetch builds using the API:
src-tauri/src/app/client_api.rs
pub async fn builds_by_branch(
    &self, 
    branch: &str, 
    release: bool
) -> Result<Vec<Build>> {
    self.request_from_endpoint(API_V1, &if release {
        format!("version/builds/{}/release", branch)
    } else {
        format!("version/builds/{}", branch)
    })
    .await
}

Build Structure

Each build contains comprehensive metadata:
src-tauri/src/app/client_api.rs
#[derive(Serialize, Deserialize, Clone)]
pub struct Build {
    #[serde(rename(serialize = "buildId"))]
    pub build_id: u32,
    #[serde(rename(serialize = "commitId"))]
    pub commit_id: String,
    pub branch: String,
    pub subsystem: String,
    #[serde(rename(serialize = "lbVersion"))]
    pub lb_version: String,
    #[serde(rename(serialize = "mcVersion"))]
    pub mc_version: String,
    pub release: bool,
    pub date: DateTime<Utc>,
    pub message: String,
    pub url: String,
    #[serde(rename(serialize = "jreDistribution"), default)]
    pub jre_distribution: JavaDistribution,
    #[serde(rename(serialize = "jreVersion"))]
    pub jre_version: u32,
    #[serde(flatten)]
    pub subsystem_specific_data: SubsystemSpecificData,
}
  • build_id: Unique identifier for the build
  • commit_id: Git commit hash
  • branch: Branch name (e.g., “nextgen”)
  • subsystem: Mod loader type (“fabric” or “forge”)
  • lb_version: LiquidBounce version
  • mc_version: Minecraft version
  • release: Whether this is a stable release
  • date: Build timestamp
  • message: Commit message or release notes
  • url: Repository URL
  • jre_distribution: Required Java distribution
  • jre_version: Required Java version
  • subsystem_specific_data: Loader-specific metadata

Subsystem Data

Builds include subsystem-specific information:
src-tauri/src/app/client_api.rs
#[derive(Serialize, Deserialize, Clone)]
pub struct SubsystemSpecificData {
    #[serde(rename(serialize = "fabricApiVersion"))]
    pub fabric_api_version: String,
    #[serde(rename(serialize = "fabricLoaderVersion"))]
    pub fabric_loader_version: String,
    #[serde(rename(serialize = "kotlinVersion"))]
    pub kotlin_version: String,
    #[serde(rename(serialize = "kotlinModVersion"))]
    pub kotlin_mod_version: String,
}
This ensures compatibility with the correct Fabric API, loader, and Kotlin versions.

Requesting Builds Command

The frontend command includes retry logic and error handling:
src-tauri/src/app/gui/commands/client.rs
#[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())
        .notify(|err, dur| {
            warn!("Failed to request builds. Retrying in {:?}. Error: {}", dur, err);
        })
        .await
        .map_err(|e| format!("unable to request builds: {:?}", e))?;

    Ok(builds)
}

Changelog

Each build has an associated changelog:

Fetching Changelog

src-tauri/src/app/client_api.rs
/// Request changelog of specified build
pub async fn fetch_changelog(&self, build_id: u32) -> Result<Changelog> {
    self.request_from_endpoint(API_V1, &format!("version/changelog/{}", build_id))
        .await
}

Changelog Structure

src-tauri/src/app/client_api.rs
#[derive(Serialize, Deserialize)]
pub struct Changelog {
    pub build: Build,
    pub changelog: String,
}

Changelog Command

src-tauri/src/app/gui/commands/client.rs
#[tauri::command]
pub(crate) async fn fetch_changelog(
    client: Client, 
    build_id: u32
) -> Result<Changelog, String> {
    (|| async { client.fetch_changelog(build_id).await })
        .retry(ExponentialBuilder::default())
        .notify(|err, dur| {
            warn!("Failed to fetch changelog. Retrying in {:?}. Error: {}", dur, err);
        })
        .await
        .map_err(|e| format!("unable to fetch changelog: {:?}", e))
}
Changelogs are fetched on-demand to reduce initial loading time.

Launch Manifest

Before launching, the launcher fetches a complete launch manifest:
src-tauri/src/app/client_api.rs
/// Request launch manifest of specific build
pub async fn fetch_launch_manifest(&self, build_id: u32) -> Result<LaunchManifest> {
    self.request_from_endpoint(API_V1, &format!("version/launch/{}", build_id))
        .await
}

Manifest Structure

src-tauri/src/app/client_api.rs
#[derive(Deserialize)]
pub struct LaunchManifest {
    pub build: Build,
    pub subsystem: LoaderSubsystem,
    pub mods: Vec<LoaderMod>,
    pub repositories: BTreeMap<String, String>,
}
  • build: Complete build information
  • subsystem: Fabric or Forge configuration
  • mods: Required and optional mods
  • repositories: Maven repositories for mod downloads

Subsystem Configuration

src-tauri/src/app/client_api.rs
#[derive(Deserialize)]
#[serde(tag = "name")]
pub enum LoaderSubsystem {
    #[serde(rename = "fabric")]
    Fabric {
        manifest: String,
        mod_directory: String,
    },
    #[serde(rename = "forge")]
    Forge {
        manifest: String,
        mod_directory: String,
    },
}

API Endpoints

All version-related requests go through the API client:
src-tauri/src/app/client_api.rs
pub const API_V1: &str = "api/v1";

pub async fn request_from_endpoint<T: DeserializeOwned>(
    &self, 
    api_version: &str, 
    endpoint: &str
) -> Result<T> {
    Ok(HTTP_CLIENT
        .get(format!("{}/{}/{}", self.url, api_version, endpoint))
        .header("X-Session-Token", &self.session_token)
        .send()
        .await?
        .error_for_status()?
        .json::<T>()
        .await?)
}
All requests include a session token for authentication and rate limiting.

Usage Flow

1

Fetch Branches

Get list of available branches and the default branch
2

Select Branch

User chooses a branch (or uses default)
3

Fetch Builds

Load all builds for the selected branch
4

Filter Builds

Optionally filter to show only release builds
5

View Changelog

User can view changelog for any build
6

Select Build

User chooses a specific build to launch
7

Fetch Launch Manifest

Launcher retrieves complete launch configuration
8

Launch

Game is launched with the selected build

Blog Posts

The API also provides blog posts for news and updates:
src-tauri/src/app/client_api.rs
/// Request all blog posts
pub async fn blog_posts(&self, page: u32) -> Result<PaginatedResponse<BlogPost>> {
    self.request_from_endpoint(API_V3, &format!("blog?page={}", page)).await
}

#[derive(Serialize, Deserialize)]
pub struct BlogPost {
    #[serde(rename(serialize = "postId"))]
    pub post_id: u32,
    #[serde(rename(serialize = "postUid"))]
    pub post_uid: String,
    pub author: String,
    pub title: String,
    pub description: String,
    pub date: NaiveDateTime,
    #[serde(rename(serialize = "bannerText"))]
    pub banner_text: String,
    #[serde(rename(serialize = "bannerImageUrl"))]
    pub banner_image_url: String,
}

Auto-Updates

Builds are automatically updated from the API

Java Management

Each build specifies required Java version

Custom Mods

Install additional mods for any build

Build docs developers (and LLMs) love