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 commandscrates/bf_mobile/src/clerk.rs- Clerk SDK integrationcrates/bf_mobile/src/push.rs- Push notification handlingcrates/bf_mobile/src/env.rs- Environment managementcrates/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.plistURL 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
- OTA Updates - Over-the-air update system
- Client Caching - Asset caching strategies
- Notification System - Push delivery