Skip to main content

Mobile App Architecture

This document covers the bf_mobile Tauri application architecture, native integrations, and Clerk authentication.

Key Files

  • crates/bf_mobile/src/lib.rs - Main app entry point and Tauri commands
  • crates/bf_mobile/src/clerk.rs - Clerk SDK integration
  • crates/bf_mobile/src/push.rs - Push notification handling
  • crates/bf_mobile/src/env.rs - Environment management
  • crates/bf_mobile/src/deeplink.rs - Deep link handling

Overview

The mobile app is built with:

  • Tauri - Rust-based framework for cross-platform apps
  • Lynx Plugin - Native iOS/Android bridge
  • Clerk SDK - Authentication (email/OTP, Apple, Google, passkeys)
  • FCM - Firebase Cloud Messaging for push notifications
  • OTA System - A/B versioned over-the-air updates
┌─────────────────────────────────────────────────────────────────────┐
│ Mobile App Architecture │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Frontend (WebView) │ │
│ │ │ │
│ │ React + TypeScript + TanStack Router │ │
│ │ Generated TypeScript bindings via specta │ │
│ └──────────────────────────┬───────────────────────────────────┘ │
│ │ │
│ Tauri Commands │
│ │ │
│ ┌──────────────────────────┴───────────────────────────────────┐ │
│ │ Tauri Runtime (Rust) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ OTA │ │ Clerk │ │ Push │ │ Env │ │ │
│ │ │ System │ │ Auth │ │ Notif │ │ Mgmt │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ └───────┼──────────────┼────────────┼─────────────┼───────────┘ │
│ │ │ │ │ │
│ └──────────────┴────────────┴─────────────┘ │
│ │ │
│ Lynx Native Bridge │
│ │ │
│ ┌──────────────────────────┴───────────────────────────────────┐ │
│ │ Native Layer (iOS/Android) │ │
│ │ │ │
│ │ Keychain/Keystore │ Firebase │ Clerk SDK │ Sign-In Services │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

Tauri Commands

The app exposes 40+ commands to the frontend via #[tauri::command]:

Device & Splash

#[tauri::command]
fn show_splash_screen() { lynx::show_splash_screen(); }

#[tauri::command]
fn close_splash_screen(app_handle: AppHandle) {
CLOSED_SPLASH_SCREEN.store(true, Ordering::Relaxed);
lynx::close_splash_screen();
push::set_push_app_handle(app_handle);
push::register_push_callbacks();
}

#[tauri::command]
fn get_device_info() -> DeviceInfo {
DeviceInfo {
os_version: lynx::os_version(),
model: lynx::device_model(),
device_id: lynx::get_device_unique_id(),
}
}

#[tauri::command]
fn get_connectivity_status() -> u8 {
// 0 = connected, 1 = cellular only, 2 = no connection
lynx::get_connectivity_error()
}

Credential Storage

Native secure storage (iOS Keychain / Android Keystore):

#[tauri::command]
fn store_credential(key: String, value: String) -> Result<(), String> {
lynx::store_credential(&key, &value)
}

#[tauri::command]
fn get_credential(key: String) -> Result<Option<String>, String> {
lynx::get_credential(&key)
}

#[tauri::command]
fn delete_credential(key: String) -> Result<(), String> {
lynx::delete_credential(&key)
}

#[tauri::command]
fn has_credential(key: String) -> bool {
lynx::has_credential(&key)
}

Push Notifications

#[tauri::command]
fn push_request_permission() -> Result<String, String> {
// Returns: "granted", "denied", "not_determined"
lynx::push_request_permission()
}

#[tauri::command]
fn push_get_permission_status() -> String {
// Returns: "granted", "denied", "not_determined", "provisional"
lynx::push_get_permission_status()
}

#[tauri::command]
async fn push_get_fcm_token() -> Result<String, String> {
lynx::push_get_fcm_token().await
}

#[tauri::command]
async fn push_delete_fcm_token() -> bool {
lynx::push_delete_fcm_token().await
}

Clerk Authentication

Native Clerk SDK integration for multiple auth methods.

Email/OTP Flow

// crates/bf_mobile/src/clerk.rs

#[tauri::command]
pub async fn clerk_sign_in_email(email: String) -> Result<EmailSignInResult, String> {
lynx::clerk_sign_in_email(&email).await
}

#[tauri::command]
pub async fn clerk_verify_otp(code: String) -> Result<ClerkUser, String> {
lynx::clerk_verify_otp(&code).await
}

Social Sign-In

#[tauri::command]
pub async fn clerk_sign_in_with_apple() -> Result<ClerkUser, String> {
lynx::clerk_sign_in_with_apple().await
}

#[tauri::command]
pub async fn clerk_sign_in_with_google() -> Result<ClerkUser, String> {
lynx::clerk_sign_in_with_google().await
}

Passkey Support

#[tauri::command]
pub fn clerk_passkey_is_supported() -> bool {
lynx::clerk_passkey_is_supported()
}

#[tauri::command]
pub async fn clerk_has_passkey() -> bool {
lynx::clerk_has_passkey().await
}

#[tauri::command]
pub async fn clerk_create_passkey() -> Result<(), String> {
lynx::clerk_create_passkey().await
}

#[tauri::command]
pub async fn clerk_sign_in_with_passkey() -> Result<ClerkUser, String> {
lynx::clerk_sign_in_with_passkey().await
}

Session Management

#[tauri::command]
pub async fn clerk_get_user() -> Option<ClerkUser> {
lynx::clerk_get_user().await
}

#[tauri::command]
pub async fn clerk_get_token() -> Option<String> {
lynx::clerk_get_token().await
}

#[tauri::command]
pub async fn clerk_sign_out() -> Result<(), String> {
lynx::clerk_sign_out().await
}

#[tauri::command]
pub async fn clerk_restore_session() -> Result<ClerkUser, String> {
lynx::clerk_restore_session().await
}

Environment Management

Runtime backend switching without app reinstall:

// crates/bf_mobile/src/env.rs

#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)]
pub struct EnvironmentInfo {
pub name: String,
pub backend_url: String,
pub clerk_publishable_key: String,
}

#[tauri::command]
pub fn get_environment(app_handle: AppHandle) -> EnvironmentInfo {
// Read from data_dir/env.json or fall back to compile-time default
}

#[tauri::command]
pub fn set_environment(app_handle: AppHandle, env_name: String) -> Result<(), String> {
// Write selection to data_dir/env.json
// Clerk SDK must reinitialize - typically requires app restart
}

Environment Configuration

Compile-time JSON array of environments:

VITE_BF_MOBILE_BACKEND_URL=[["production","https://app.broadfordlife.com"],["staging","https://staging.broadfordlife.com"]]

App Events

Events emitted to the frontend via Tauri's event system:

#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum AppEvent {
ReloadWebview,

PushNotificationReceived {
notification_id: String,
notification_type: String,
property_id: String,
title: String,
body: String,
action_url: Option<String>,
},

PushNotificationTapped {
notification_id: String,
action_url: Option<String>,
},

Toast {
level: String, // "success", "error", "info"
message: String,
},
}

Deep Linking

URL scheme handling for external redirects:

// crates/bf_mobile/src/deeplink.rs

pub fn handle_deep_link(app_handle: &AppHandle, urls: Vec<Url>) {
for url in urls {
// Handle broadfordlife:// scheme
// Route to appropriate app section
}
}

Scheme Registration

  • iOS: Info.plist URL types
  • Android: Intent filters in AndroidManifest.xml

App Lifecycle

Initialization Sequence

pub fn run() {
// 1. Register OTA callback
lynx::register_ota_callback(new_version_callback);

// 2. Start network monitoring
lynx::start_network_monitoring();

// 3. Configure OTA assets wrapper
let ota_assets = GlobalOtaAssets::new(app_context.assets);
app_context.assets = Box::new(ota_assets);

// 4. Build Tauri app with plugins
builder
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_haptics::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_websocket::init())
.plugin(tauri_plugin_geolocation::init()) // mobile only
.setup(|app| {
// 5. Create data directory
// 6. Set OTA app handle
// 7. Configure Firebase
// 8. Show splash screen
// 9. Register deep link handler
// 10. Initialize Clerk SDK
// 11. Start OTA update check loop
});
}

Environment Switch

#[tauri::command]
fn exit_app(app_handle: AppHandle) {
// Exit needed for Clerk SDK reinitialization
lynx::log("[app] Exiting app for environment switch");
app_handle.exit(0);
}

TypeScript Bindings

Commands generate TypeScript types via specta macro:

#[tauri::command]
#[specta::specta] // Generates TypeScript definitions
fn get_device_info() -> DeviceInfo { ... }

Generated output in gen/ directory enables type-safe frontend calls.

Platform-Specific Code

iOS-Only

#[cfg(target_os = "ios")]
{
lynx::start_network_monitoring();
lynx::push_register_notification_tapped();
}

Android-Only

#[cfg(target_os = "android")]
{
lynx::register_android_plugin(app_handle.clone())?;
lynx::start_network_monitoring();
}

See Also