Desktop Bridge Layer
The Desktop Bridge Layer connects Apollo’s React frontend to native OS capabilities through Tauri 2.9, a Rust-based desktop runtime. It provides type-safe IPC communication, backend lifecycle management, and native integrations while maintaining a small memory footprint.
Bridge Architecture
Apollo uses a hybrid dual-protocol architecture for optimal performance:
+-------------------------------------------------------------+
| React 19 Frontend |
| (WebView2/WebKit) |
+-------------+-----------------------+-----------------------+
| |
| Tauri IPC | Direct HTTP
| (Native Features) | (Data Endpoints)
| |
+-------------v-----------------------v-----------------------+
| Tauri 2.9 Bridge (Rust Native Binary) |
| * IPC Command Handlers |
| * Backend Health Monitoring |
| * HTTP Proxy to FastAPI |
| * Native OS Integration (dialogs, shell) |
+-----------------------------+-------------------------------+
|
| HTTP/REST (reqwest)
| localhost:8000
|
+-----------------------------v-------------------------------+
| FastAPI Backend |
| (Docker Containers) |
+-------------------------------------------------------------+Why Dual Protocol?
- Tauri IPC: Native features (health checks, model switching, file dialogs, notifications)
- Direct HTTP: High-throughput data operations (queries, document upload, SSE streaming)
This hybrid approach optimizes for both performance and developer ergonomics.
IPC Communication Pattern
Type-Safe Boundaries
Tauri uses serde (Rust) and JSON to provide type-safe bidirectional communication:
// src-tauri/src/commands.rs
use serde::{Deserialize, Serialize};
use tauri::State;
/// Health status response (auto-serialized to JSON)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthStatus {
pub status: String,
pub backend: String,
pub version: String,
pub models: ModelsStatus,
pub cache: CacheStatus,
}
/// Tauri command - exposed to frontend via invoke()
#[tauri::command]
pub async fn check_atlas_health(
state: State<'_, Arc<Mutex<AppState>>>
) -> Result<HealthStatus, String> {
let state = state.lock().await;
let url = format!("{}/api/health", state.backend_url);
let response = state.http_client
.get(&url)
.send()
.await
.map_err(|e| format!("Backend unreachable: {}", e))?;
let health: HealthStatus = response
.json()
.await
.map_err(|e| format!("Invalid response format: {}", e))?;
Ok(health)
}Frontend Integration
The frontend calls Rust commands using the invoke() API:
// src/services/backendService.ts
import { invoke } from '@tauri-apps/api/core';
interface HealthStatus {
status: 'healthy' | 'degraded' | 'down';
backend: string;
version: string;
models: {
llm: string;
embeddings: string;
vector_db: string;
};
cache: {
redis_connected: boolean;
hit_rate: number;
};
}
export const checkHealth = async (): Promise<HealthStatus> => {
// Tauri IPC call - serialized via serde/JSON
return await invoke<HealthStatus>('check_atlas_health');
};Type Generation: Tauri’s generate_handler! macro automatically generates TypeScript types from Rust structs, ensuring compile-time type safety across the IPC boundary.
Invoke vs Emit
Tauri provides two IPC patterns:
| Pattern | Direction | Use Case | Example |
|---|---|---|---|
| invoke() | Frontend → Backend | Request-response operations | Health checks, model switching |
| emit() | Backend → Frontend | Event notifications | Progress updates, status changes |
// src-tauri/src/sidecar.rs
use tauri::Manager;
// Emit event to frontend
app.emit_all("backend-status-changed", BackendStatus {
running: true,
healthy: true,
last_checked: chrono::Utc::now(),
});// src/hooks/useBackendStatus.ts
import { listen } from '@tauri-apps/api/event';
useEffect(() => {
const unlisten = listen('backend-status-changed', (event) => {
setStatus(event.payload as BackendStatus);
});
return () => {
unlisten.then(fn => fn());
};
}, []);Security Model
Capabilities-Based Permissions
Tauri 2.x uses a deny-by-default security model with explicit permission grants:
// src-tauri/capabilities/default.json
{
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"core:path:default",
"core:event:default",
"core:webview:default",
"core:window:default",
"shell:allow-open", // Opens URLs only, no arbitrary commands
"dialog:default" // File picker dialogs
]
}Security Boundaries:
- Frontend cannot execute arbitrary Rust code
- Only registered commands in
generate_handler!are exposed - HTTP client isolated to backend (not exposed to frontend)
- Shell access restricted to
open::that()only (no command injection) - File system access scoped via
tauri-plugin-fs
CSP Configuration
// src-tauri/tauri.conf.json
{
"security": {
"csp": null, // DEVELOPMENT ONLY
// PRODUCTION: "default-src 'self'; connect-src 'self' http://localhost:8000"
"assetProtocol": {
"enabled": true,
"scope": ["**"]
}
}
}Production Recommendation: Enable CSP with script-src 'self' and connect-src limited to backend URL. Current null value is for development only.
Native OS Capabilities
File System Access
Tauri’s fs plugin provides scoped file system operations:
// src-tauri/src/lib.rs
.plugin(tauri_plugin_fs::init())// Frontend: Scoped file operations
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
// Read file (requires permission)
const contents = await readTextFile('documents/readme.txt');
// Write file (requires permission)
await writeTextFile('logs/output.log', 'Log data');File paths are scoped to allowed directories defined in capabilities. Attempts to access paths outside the scope will fail with a permission error.
Native File Dialogs
// src/components/Documents/DocumentUpload.tsx
import { open } from '@tauri-apps/plugin-dialog';
const handleFileSelect = async () => {
const file = await open({
multiple: false,
filters: [{
name: 'Documents',
extensions: ['pdf', 'txt', 'docx', 'doc', 'md']
}]
});
if (file) {
// Process file...
}
};Shell Integration
// src-tauri/src/commands.rs
use open;
#[tauri::command]
pub fn open_url(url: String) -> Result<(), String> {
// Opens URL in default browser
open::that(&url)
.map_err(|e| format!("Failed to open URL: {}", e))
}Shell access is restricted to shell:allow-open permission only. This prevents command injection attacks by limiting operations to opening URLs/files in default applications.
File Upload Pipeline
Native → Backend Flow
+---------------------------------------------------------------+
| 1. User selects file via native dialog (tauri-plugin-dialog) |
+-----------------------+---------------------------------------+
|
v
+-----------------------+---------------------------------------+
| 2. Frontend validates (size, type) and prepares FormData |
+-----------------------+---------------------------------------+
|
v
+-----------------------+---------------------------------------+
| 3. Direct HTTP POST to /api/documents/upload |
| (Bypasses Tauri for performance) |
+-----------------------+---------------------------------------+
|
v
+-----------------------+---------------------------------------+
| 4. Backend: Sanitize, compute SHA256, save to documents/ |
+-----------------------+---------------------------------------+
|
v
+-----------------------+---------------------------------------+
| 5. User triggers reindexing (Tauri IPC or HTTP) |
+---------------------------------------------------------------+Why Direct HTTP?
Large file uploads bypass Tauri IPC for performance:
- IPC: Limited to ~5MB practical payload size
- Direct HTTP: Supports 50MB files with streaming
// src/services/api.ts
export const uploadDocument = async (file: File): Promise<UploadResponse> => {
const formData = new FormData();
formData.append('file', file);
// Direct HTTP, not Tauri IPC
const response = await fetch('http://localhost:8000/api/documents/upload', {
method: 'POST',
body: formData
});
return await response.json();
};Backend Lifecycle Management
Sidecar Pattern
The Rust bridge manages the FastAPI backend as a sidecar process:
// src-tauri/src/sidecar.rs
pub struct BackendSidecar {
status: Arc<Mutex<BackendStatus>>,
health_monitor_task: Option<JoinHandle<()>>,
}
impl BackendSidecar {
pub fn new(app_handle: AppHandle) -> Self {
Self {
status: Arc::new(Mutex::new(BackendStatus::default())),
health_monitor_task: None,
}
}
pub fn start_health_monitor(&mut self, backend_url: String) {
let status = Arc::clone(&self.status);
// Background tokio task for health monitoring
self.health_monitor_task = Some(tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(10)).await;
let health_check = reqwest::get(format!("{}/api/health", backend_url))
.await;
let mut status_lock = status.lock().unwrap();
match health_check {
Ok(response) if response.status().is_success() => {
status_lock.running = true;
status_lock.healthy = true;
status_lock.error = None;
},
Ok(response) => {
status_lock.running = true;
status_lock.healthy = false;
status_lock.error = Some(format!("HTTP {}", response.status()));
},
Err(e) => {
status_lock.running = false;
status_lock.healthy = false;
status_lock.error = Some(e.to_string());
}
}
status_lock.last_checked = Some(chrono::Utc::now());
}
}));
}
}Health Monitoring
Background health checks run every 10 seconds:
// src-tauri/src/commands.rs
#[tauri::command]
pub async fn get_backend_status(
state: State<'_, Arc<Mutex<Option<BackendSidecar>>>>
) -> Result<BackendStatus, String> {
let sidecar = state.lock().unwrap();
match sidecar.as_ref() {
Some(s) => Ok(s.get_status()),
None => Err("Backend sidecar not initialized".to_string())
}
}Window Management
Configuration
// src-tauri/tauri.conf.json
{
"windows": [
{
"title": "Tactical RAG - Document Intelligence",
"width": 1400,
"height": 900,
"minWidth": 1024,
"minHeight": 768,
"resizable": true,
"fullscreen": false,
"decorations": true,
"alwaysOnTop": false,
"center": true
}
]
}Programmatic Control
// src-tauri/src/commands.rs
use tauri::{Manager, Window};
#[tauri::command]
pub async fn set_fullscreen(window: Window, fullscreen: bool) -> Result<(), String> {
window.set_fullscreen(fullscreen)
.map_err(|e| format!("Failed to set fullscreen: {}", e))
}
#[tauri::command]
pub async fn minimize_window(window: Window) -> Result<(), String> {
window.minimize()
.map_err(|e| format!("Failed to minimize: {}", e))
}State Management
Arc/Mutex Pattern
Tauri uses Arc<Mutex<T>> for thread-safe shared state:
// src-tauri/src/lib.rs
pub struct AppState {
pub backend_url: String,
pub http_client: reqwest::Client,
}
impl AppState {
pub fn new(backend_url: String) -> Self {
Self {
backend_url,
http_client: reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("Failed to create HTTP client"),
}
}
}
// Register state
let app_state = Arc::new(tokio::sync::Mutex::new(
commands::AppState::new(backend_url)
));
app.manage(app_state);State Injection
Commands receive state via the State<'_, T> parameter:
#[tauri::command]
pub async fn check_backend_connected(
state: State<'_, Arc<Mutex<AppState>>>
) -> Result<bool, String> {
let state = state.lock().await;
// Access state.backend_url, state.http_client
}Async Mutex: Tauri uses tokio::sync::Mutex for async operations (HTTP requests, file I/O) and std::sync::Mutex for sync operations (state updates).
Rust Backend Commands
Command Registration
// src-tauri/src/lib.rs
.invoke_handler(tauri::generate_handler![
// Backend lifecycle
sidecar::start_backend,
sidecar::stop_backend,
sidecar::get_backend_status,
// ATLAS protocol
commands::check_atlas_health,
commands::check_backend_connected,
commands::get_cache_stats,
commands::get_available_models,
// Model hotswap
commands::get_models_list,
commands::get_current_model,
commands::switch_model,
// System integration
commands::open_url,
commands::show_notification,
// Ollama integration
ollama::get_ollama_status,
ollama::pull_qwen_model,
ollama::verify_qwen,
])Model Hotswap Command
// src-tauri/src/commands.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelSwitchResponse {
pub success: bool,
pub message: String,
pub previous_model: Option<String>,
pub current_model: Option<String>,
}
#[tauri::command]
pub async fn switch_model(
model_id: String,
state: State<'_, Arc<Mutex<AppState>>>
) -> Result<ModelSwitchResponse, String> {
let state = state.lock().await;
let url = format!("{}/api/models/switch", state.backend_url);
#[derive(Serialize)]
struct SwitchRequest {
model_id: String,
}
let response = state.http_client
.post(&url)
.json(&SwitchRequest { model_id })
.send()
.await
.map_err(|e| format!("Failed to switch model: {}", e))?;
let switch_response: ModelSwitchResponse = response
.json()
.await
.map_err(|e| format!("Invalid response: {}", e))?;
Ok(switch_response)
}Performance Considerations
Async Runtime
All Tauri commands use tokio async runtime:
// Async command (non-blocking)
#[tauri::command]
pub async fn check_health(state: State<'_, T>) -> Result<HealthStatus, String> {
// tokio::spawn, await, etc.
}
// Sync command (blocks Tauri thread pool)
#[tauri::command]
pub fn get_config() -> String {
"config".to_string()
}Avoid Sync Commands for I/O: Use async fn for HTTP requests, file I/O, or any operation that might block. Sync commands block the Tauri thread pool.
HTTP Client Reuse
The reqwest::Client is reused for all requests:
pub struct AppState {
pub http_client: reqwest::Client, // Connection pooling
}This enables HTTP/2 multiplexing and connection pooling for faster requests.
Memory Footprint
| Component | Memory Usage |
|---|---|
| Rust binary | ~5-10 MB |
| WebView2 (Windows) | ~50-80 MB |
| Frontend assets | ~20-30 MB |
| Total Desktop App | ~75-120 MB |
Compare to Electron (~200-300 MB baseline).
Code Examples
Complete IPC Handler
// src-tauri/src/commands.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheStats {
pub hit_rate: f64,
pub total_requests: u64,
pub cache_hits: u64,
pub cache_misses: u64,
pub avg_latency_ms: f64,
}
#[tauri::command]
pub async fn get_cache_stats(
state: State<'_, Arc<Mutex<AppState>>>
) -> Result<CacheStats, String> {
let state = state.lock().await;
let url = format!("{}/api/cache/metrics", state.backend_url);
let response = state.http_client
.get(&url)
.send()
.await
.map_err(|e| format!("Failed to fetch cache stats: {}", e))?;
let stats: CacheStats = response
.json()
.await
.map_err(|e| format!("Invalid response: {}", e))?;
Ok(stats)
}Frontend Hook
// src/hooks/useCacheStats.ts
import { invoke } from '@tauri-apps/api/core';
import { useEffect, useState } from 'react';
interface CacheStats {
hit_rate: number;
total_requests: number;
cache_hits: number;
cache_misses: number;
avg_latency_ms: number;
}
export const useCacheStats = () => {
const [stats, setStats] = useState<CacheStats | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchStats = async () => {
try {
setLoading(true);
const data = await invoke<CacheStats>('get_cache_stats');
setStats(data);
setError(null);
} catch (err) {
setError(err as string);
} finally {
setLoading(false);
}
};
fetchStats();
// Refresh every 5 seconds
const interval = setInterval(fetchStats, 5000);
return () => clearInterval(interval);
}, []);
return { stats, loading, error };
};Build Configuration
Multi-Stage Build
// src-tauri/tauri.conf.json
{
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"bundle": {
"active": true,
"targets": ["nsis"],
"windows": {
"webviewInstallMode": {
"type": "embedBootstrapper"
}
}
}
}Build Process
- npm run build → Vite builds React app to
dist/ - cargo build —release → Compiles Rust binary
- tauri-build generates context and resources
- NSIS packages binary, frontend, and icons
- Output:
Tactical-RAG-Desktop_4.0.0_x64-setup.exe(~20-30MB)
Next: Integration Patterns
Continue to Integration Layer to see how the bridge layer orchestrates communication between frontend, backend, and native OS.
Key Files:
src-tauri/src/main.rs- Entry point (minimal)src-tauri/src/lib.rs- Application logic and Tauri buildersrc-tauri/src/commands.rs- ATLAS backend integrationsrc-tauri/src/sidecar.rs- Backend process lifecyclesrc-tauri/capabilities/default.json- Permission definitions