Skip to main content

Overview

The Modrinth App is a desktop application for managing Minecraft mods, modpacks, and instances. Built with Tauri 2.x, it combines a Rust backend (Theseus) with a Vue 3 frontend. Platforms: Windows, macOS, Linux
Framework: Tauri 2.x
Backend: Rust (Theseus library)
Frontend: Vue 3 + Tailwind CSS

Architecture

Tauri Application Model

┌───────────────────────────────────────────────┐
│          Tauri Application Window            │
├───────────────────────────────────────────────┤
│                                               │
│        WebView (Vue 3 Frontend)               │
│         apps/app-frontend/                    │
│                                               │
│    ┌──────────────────────────────────┐      │
│    │  Vue Components, Router, etc.    │      │
│    └──────────────────────────────────┘      │
│                    │                          │
│                    │ IPC (Tauri Commands)     │
│                    ↓                          │
│    ┌──────────────────────────────────┐      │
│    │   Rust Backend (Theseus)         │      │
│    │   packages/app-lib/              │      │
│    │                                  │      │
│    │  • Profile Management            │      │
│    │  • Mod Installation              │      │
│    │  • Game Launching                │      │
│    │  • File System Access            │      │
│    │  • SQLite Database               │      │
│    └──────────────────────────────────┘      │
│                                               │
└───────────────────────────────────────────────┘
          │                    │
          ↓                    ↓
   Native System APIs    Modrinth APIs
   (FS, Process, etc.)   (Labrinth, etc.)

Component Separation

  1. Tauri Shell (apps/app/) - Desktop framework, native integrations
  2. Frontend (apps/app-frontend/) - Vue 3 UI rendered in WebView
  3. Backend Library (packages/app-lib/) - Theseus Rust library for core logic

Directory Structure

App Shell (apps/app/)

apps/app/
├── src/
│   ├── main.rs              # Tauri app entry point
│   └── lib.rs               # Tauri command exports
├── src-tauri/
│   └── tauri.conf.json      # Tauri configuration
├── Cargo.toml               # Rust dependencies
└── package.json             # Frontend build integration

Frontend (apps/app-frontend/)

apps/app-frontend/
├── src/
│   ├── App.vue              # Root component
│   ├── main.ts              # Vue app entry
│   ├── router.ts            # Vue Router
│   ├── pages/               # Page components
│   │   ├── Browse.vue       # Browse mods/modpacks
│   │   ├── Library.vue      # User's instances
│   │   ├── Instance.vue     # Instance detail view
│   │   └── Settings.vue     # App settings
│   ├── components/          # App-specific components
│   └── helpers/             # Utilities
├── index.html
├── vite.config.ts
└── package.json

Backend Library (packages/app-lib/)

packages/app-lib/  (Theseus)
├── src/
│   ├── lib.rs               # Library entry point
│   ├── api/                 # Tauri command handlers
│   │   ├── profile.rs       # Profile management
│   │   ├── pack.rs          # Modpack operations
│   │   ├── metadata.rs      # Minecraft metadata
│   │   └── ...
│   ├── profile/             # Profile & instance logic
│   ├── launcher/            # Game launching
│   ├── pack/                # Modpack import/export
│   ├── state/               # Application state
│   ├── util/                # Utilities
│   └── event/               # Event emitters
├── migrations/              # SQLite migrations
├── java/                    # Java runtime detection
├── Cargo.toml
└── .env.local               # Environment template

Tauri Commands (IPC)

The frontend communicates with the backend via Tauri commands.

Defining Commands (Rust)

packages/app-lib/src/api/profile.rs
use tauri::State;
use crate::state::AppState;

#[tauri::command]
pub async fn get_all_profiles(
    state: State<'_, AppState>,
) -> Result<Vec<Profile>, String> {
    let profiles = state.profiles.get_all().await
        .map_err(|e| e.to_string())?;
    Ok(profiles)
}

#[tauri::command]
pub async fn create_profile(
    name: String,
    game_version: String,
    mod_loader: String,
    state: State<'_, AppState>,
) -> Result<Profile, String> {
    let profile = state.profiles.create(
        &name,
        &game_version,
        &mod_loader,
    ).await.map_err(|e| e.to_string())?;
    
    Ok(profile)
}

Registering Commands

apps/app/src/main.rs
use tauri::Builder;

fn main() {
    Builder::default()
        .invoke_handler(tauri::generate_handler![
            get_all_profiles,
            create_profile,
            install_mod,
            launch_instance,
            // ... more commands
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Calling from Frontend (TypeScript)

apps/app-frontend/src/helpers/profile.ts
import { invoke } from '@tauri-apps/api/core'

export interface Profile {
	id: string
	name: string
	gameVersion: string
	modLoader: string
	// ...
}

export async function getAllProfiles(): Promise<Profile[]> {
	return await invoke<Profile[]>('get_all_profiles')
}

export async function createProfile(
	name: string,
	gameVersion: string,
	modLoader: string,
): Promise<Profile> {
	return await invoke<Profile>('create_profile', {
		name,
		gameVersion,
		modLoader,
	})
}

Usage in Components

src/pages/Library.vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getAllProfiles, createProfile } from '@/helpers/profile'

const profiles = ref<Profile[]>([])
const loading = ref(true)

onMounted(async () => {
	profiles.value = await getAllProfiles()
	loading.value = false
})

async function handleCreateProfile() {
	const newProfile = await createProfile('My Modpack', '1.20.1', 'fabric')
	profiles.value.push(newProfile)
}
</script>

<template>
	<div>
		<h1>My Instances</h1>
		<div v-if="loading">Loading...</div>
		<div v-else>
			<ProfileCard
				v-for="profile in profiles"
				:key="profile.id"
				:profile="profile"
			/>
		</div>
		<button @click="handleCreateProfile">Create New Instance</button>
	</div>
</template>

Theseus (Backend Library)

Theseus (packages/app-lib/) is the Rust library powering the app.

Core Features

Profile Management

Profiles represent Minecraft instances (game version + mods + config).
use theseus::profile::Profile;

// Create profile
let profile = Profile::create(
    "My Modpack",
    "1.20.1",
    "fabric",
).await?;

// Add mod
profile.add_project(
    "sodium",
    "version_id_123",
).await?;

// Launch game
let process = profile.launch().await?;

Mod Installation

use theseus::pack::install_mod;

// Install from Modrinth
install_mod(
    profile_id,
    "sodium",      // Project ID or slug
    Some("latest"), // Version (or None for latest compatible)
).await?;

// Install from file
install_mod_from_file(
    profile_id,
    PathBuf::from("/path/to/mod.jar"),
).await?;

Minecraft Version Management

use theseus::metadata::Metadata;

// Get available versions
let versions = Metadata::get_minecraft_versions().await?;

// Get loader versions (Forge, Fabric, Quilt)
let fabric_versions = Metadata::get_fabric_versions("1.20.1").await?;

Java Runtime Detection

use theseus::launcher::JavaVersion;

// Auto-detect Java installations
let java_installs = JavaVersion::find_all().await?;

// Get or download Java for version
let java = JavaVersion::get_for_minecraft("1.20.1").await?;

Game Launching

use theseus::launcher::launch;

// Launch instance
let process = launch(
    profile_id,
    Some(java_path),
    Some(memory_mb),
).await?;

// Monitor process
process.on_stdout(|line| {
    println!("Game: {}", line);
});

let exit_code = process.wait().await?;

State Management

Theseus maintains application state in memory and SQLite.
src/state/mod.rs
use parking_lot::RwLock;
use std::sync::Arc;

pub struct AppState {
    pub profiles: ProfileManager,
    pub metadata: MetadataCache,
    pub settings: Arc<RwLock<Settings>>,
    pub db: SqlitePool,
}

impl AppState {
    pub async fn init() -> Result<Self> {
        let db = SqlitePool::connect("modrinth.db").await?;
        
        Ok(Self {
            profiles: ProfileManager::new(&db).await?,
            metadata: MetadataCache::new().await?,
            settings: Arc::new(RwLock::new(Settings::load()?)),
            db,
        })
    }
}

Event System

Theseus emits events for long-running operations:
use theseus::event::{emit, EventPayload};

// Emit progress event
emit(EventPayload::DownloadProgress {
    id: "mod_download",
    current: 1024,
    total: 5120,
});

// Emit completion
emit(EventPayload::DownloadComplete {
    id: "mod_download",
});
Frontend subscribes:
import { listen } from '@tauri-apps/api/event'

listen<{ id: string; current: number; total: number }>(
	'download-progress',
	(event) => {
		const { current, total } = event.payload
		progress.value = (current / total) * 100
	}
)

Database (SQLite)

Local data is stored in SQLite:
migrations/001_create_profiles.sql
CREATE TABLE profiles (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    game_version TEXT NOT NULL,
    mod_loader TEXT NOT NULL,
    created_at INTEGER NOT NULL,
    last_played INTEGER
);

CREATE TABLE installed_mods (
    profile_id TEXT NOT NULL,
    project_id TEXT NOT NULL,
    version_id TEXT NOT NULL,
    file_name TEXT NOT NULL,
    PRIMARY KEY (profile_id, project_id),
    FOREIGN KEY (profile_id) REFERENCES profiles(id)
);
Queries:
use sqlx::{SqlitePool, query_as};

pub async fn get_profile(id: &str, pool: &SqlitePool) -> Result<Profile> {
    query_as!(
        Profile,
        "SELECT * FROM profiles WHERE id = ?",
        id
    )
    .fetch_one(pool)
    .await
    .map_err(Into::into)
}

Frontend (Vue 3)

The frontend is a Vue 3 SPA rendered in Tauri’s WebView.

API Client Integration

Use @modrinth/api-client with TauriModrinthClient:
src/App.vue
import { TauriModrinthClient, AuthFeature } from '@modrinth/api-client'
import { provideModrinthClient } from '@modrinth/ui'
import { getVersion } from '@tauri-apps/api/app'

const version = await getVersion()
const client = new TauriModrinthClient({
	userAgent: `modrinth/theseus/${version} ([email protected])`,
	features: [
		new AuthFeature({
			token: async () => auth.value.token,
		}),
	],
})

provideModrinthClient(client)

Shared Components

The app reuses components from @modrinth/ui:
<script setup>
import { Button, Modal, ProjectCard } from '@modrinth/ui'
</script>

<template>
	<div>
		<ProjectCard :project="project" />
		<Button @click="install">Install</Button>
	</div>
</template>
See the cross-platform-pages skill for shared page components.

Routing

Vue Router handles navigation:
src/router.ts
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
	{ path: '/', component: () => import('./pages/Browse.vue') },
	{ path: '/library', component: () => import('./pages/Library.vue') },
	{ path: '/instance/:id', component: () => import('./pages/Instance.vue') },
	{ path: '/settings', component: () => import('./pages/Settings.vue') },
]

export const router = createRouter({
	history: createWebHistory(),
	routes,
})

Native Features

File System Access

import { open } from '@tauri-apps/plugin-dialog'
import { readTextFile } from '@tauri-apps/plugin-fs'

// File picker
const selected = await open({
	multiple: false,
	filters: [{ name: 'Modpack', extensions: ['mrpack'] }],
})

// Read file
const content = await readTextFile(selected.path)
Handle modrinth:// URLs:
src/main.rs
use tauri_plugin_deep_link::DeepLink;

Builder::default()
    .plugin(DeepLink::new())
    .setup(|app| {
        app.listen_deep_link(|url| {
            // Handle modrinth://install/sodium
            if url.starts_with("modrinth://install/") {
                let project_id = url.strip_prefix("modrinth://install/").unwrap();
                // Trigger install
            }
        });
        Ok(())
    })

System Tray

use tauri::SystemTray;

let tray = SystemTray::new().with_menu(/* menu */);

Builder::default()
    .system_tray(tray)
    .on_system_tray_event(|app, event| {
        // Handle tray clicks
    })

Auto-Updates

use tauri_plugin_updater::UpdaterExt;

let updater = app.updater();
if let Some(update) = updater.check().await? {
    update.download_and_install().await?;
    app.restart();
}

Cross-Platform Builds

Build Targets

# macOS (x64 + ARM64 universal binary)
cargo tauri build --target universal-apple-darwin

# Windows
cargo tauri build --target x86_64-pc-windows-msvc

# Linux
cargo tauri build --target x86_64-unknown-linux-gnu

CI/CD (GitHub Actions)

The theseus-build.yml workflow builds for all platforms:
jobs:
  build:
    strategy:
      matrix:
        platform: [macos-latest, windows-latest, ubuntu-latest]
    runs-on: ${{ matrix.platform }}
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: pnpm install
      - run: pnpm app:build
      - uses: actions/upload-artifact@v4
        with:
          name: app-${{ matrix.platform }}
          path: apps/app/src-tauri/target/release/bundle/

Development

Running Locally

# Install dependencies
pnpm install

# Copy environment file
cd packages/app-lib
cp .env.local .env

# Start development (from root)
pnpm app:dev
This launches the app with:
  • Frontend hot-reloading (Vite HMR)
  • Rust recompilation on code changes (requires manual restart)

Environment Variables

packages/app-lib/.env.local
# API endpoints (use staging for development)
LABRINTH_URL=https://staging-api.modrinth.com

# Analytics
ANALYTICS_ENABLED=false

Debugging

Open DevTools:
  • macOS: Cmd+Option+I
  • Windows/Linux: Ctrl+Shift+I
Rust logs:
use tracing::info;

info!("Profile created: {}", profile.id);
Logs appear in the terminal running pnpm app:dev.

Testing

Rust Tests

# Run all tests
cargo test -p theseus

# Run specific test
cargo test -p theseus test_profile_creation

Frontend Tests

pnpm --filter @modrinth/app-frontend run test

Integration Tests

tests/integration_test.rs
#[tokio::test]
async fn test_install_and_launch() {
    let state = AppState::init().await.unwrap();
    
    let profile = create_profile("Test", "1.20.1", "fabric", &state).await.unwrap();
    install_mod(&profile.id, "sodium", None, &state).await.unwrap();
    
    let process = launch(&profile.id, None, None, &state).await.unwrap();
    assert!(process.is_running());
}

Pre-PR Checks

# From root directory
pnpm prepr:frontend:app

# Rust checks
cargo clippy -p theseus --all-targets
cargo fmt --check

Distribution

Installers

Tauri creates platform-specific installers:
  • macOS: .dmg (disk image) + .app bundle
  • Windows: .msi (installer) + .exe (portable)
  • Linux: .deb, .AppImage

Download

Users download from modrinth.com/app.

Auto-Updates

The app checks for updates on launch and downloads/installs automatically.

Next Steps

Packages

Learn about shared packages (Theseus, UI, etc.)

Testing

Testing strategies for Rust and Vue

Deployment

CI/CD and release process

Frontend (Web)

Compare with the web frontend architecture