How Tauri Commands and Events Enable Frontend-to-Rust Backend Communication in Meetily

Tauri commands and events provide bidirectional communication: commands let the JavaScript frontend call Rust functions and await results, while events let the Rust backend push real-time data to the frontend without polling.

Meetily, an AI-powered meeting transcriber built with Tauri, uses these two mechanisms to bridge its Next.js TypeScript UI and native Rust audio processing core. Understanding how Tauri commands and events work together is essential for building responsive desktop applications with complex backend operations.

Tauri Commands: Frontend Invokes Rust Functions

Commands form the request-response backbone of Meetily's frontend-to-backend communication. They allow TypeScript code to call Rust functions as if they were local async functions.

Defining a Command in Rust

In frontend/src-tauri/src/lib.rs, commands are defined using the #[tauri::command] attribute macro. Here's the start_recording command:

#[tauri::command]
async fn start_recording<R: Runtime>(
    app: AppHandle<R>,
    mic_device_name: Option<String>,
    system_device_name: Option<String>,
    meeting_name: Option<String>,
) -> Result<(), String> {
    // Recording initialization logic...
}

The AppHandle<R> parameter provides access to Tauri application state and event emission capabilities. The Result<(), String> return type gives consistent error handling—failures propagate to the frontend as rejected promises.

Registering Commands at Startup

Commands must be registered when building the Tauri application. In Meetily's lib.rs, this happens via tauri::generate_handler!:

fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            start_recording,
            stop_recording,
            pause_recording,
            resume_recording,
            list_audio_devices,
            // ... additional commands
        ])
        // ... additional configuration
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

The invoke_handler accepts only registered commands—unregistered functions are inaccessible from the frontend, providing a security boundary.

Invoking Commands from TypeScript

The frontend calls Rust commands through @tauri-apps/api/core. From frontend/src/components/RecordingControls.tsx:

import { invoke } from '@tauri-apps/api/core';

async function beginRecording() {
  const result = await invoke('start_recording', {
    mic_device_name: selectedMicrophone,
    system_device_name: selectedSystemAudio,
    meeting_name: currentMeetingName,
  });
  // Returns Promise<void> on success, throws on error
}

Arguments are serialized to JSON, marshalled across the process boundary, deserialized in Rust, and validated against the function signature. Return values follow the reverse path.

Additional Command Locations

Meetily organizes commands by domain:

Tauri Events: Rust Pushes Data to Frontend

Events provide unidirectional, server-push communication from Rust to JavaScript. Unlike commands, events carry no expectation of response—they're ideal for streaming data and status updates.

Emitting Events from Rust

Any code holding an AppHandle can emit events. In frontend/src-tauri/src/lib_old_complex.rs, the transcription pipeline emits progress updates:

if let Err(e) = app_handle.emit("transcript-update", &update) {
    log::error!("Failed to emit transcript update: {}", e);
}

The emit method takes an event name string and any JSON-serializable payload. Meetily's Whisper engine in whisper_engine/commands.rs uses the same pattern for download progress:

let progress_info = DownloadProgress {
    model_name: model.name.clone(),
    percent: bytes_downloaded as f32 / total_bytes as f32 * 100.0,
};
if let Err(e) = app_handle.emit("whisper-download-progress", progress_info) {
    log::error!("Failed to emit download progress: {}", e);
}

Tray interactions in frontend/src-tauri/src/tray.rs also emit events:

app_handle.emit("recording-stop-complete", stopped_meeting_id)?;

Listening for Events in TypeScript

The frontend registers listeners using listen from @tauri-apps/api/event. A typical pattern in Meetily creates listeners in React context providers:

import { listen } from '@tauri-apps/api/event';

// In a context provider or useEffect
await listen<TranscriptUpdate>('transcript-update', (event) => {
  setTranscripts((prev) => [...prev, event.payload]);
});

await listen<DownloadProgress>('whisper-download-progress', (event) => {
  setDownloadPercent(event.payload.percent);
});

The generic type parameter TranscriptUpdate or DownloadProgress provides TypeScript type safety—payload structure is enforced on both sides of the boundary.

Commands vs. Events: When to Use Each

Pattern Best For Meetily Example
Commands One-shot operations requiring confirmation Starting/stopping recording, fetching device lists
Events Continuous data streams, async notifications Live transcription chunks, download progress, recording status changes

Commands suit operations where the UI needs to know success or failure before proceeding. The await invoke('start_recording') pattern blocks the caller until the Rust audio pipeline confirms initialization.

Events decouple the backend's timeline from the frontend. The Whisper model downloads in the background, emitting progress events; the UI doesn't block and can display a progress bar that updates independently.

Security and Type Safety Considerations

Meetily's implementation demonstrates several best practices:

  • Command validation: Rust's type system validates arguments at the boundary—Option<String> handles missing parameters gracefully
  • Explicit registration: Only functions in generate_handler! are exposed; internal Rust functions remain inaccessible
  • Payload contracts: TypeScript interfaces mirror Rust structs; both sides agree on TranscriptUpdate, DownloadProgress, and other event shapes
  • Error propagation: Rust Result::Err(String) becomes JavaScript rejected promises with descriptive messages

Summary

  • Tauri commands provide typed, request-response RPC from TypeScript to Rust—define with #[tauri::command], register in generate_handler!, and call via invoke()
  • Tauri events enable push-based communication from Rust to TypeScript—emit with app_handle.emit() and consume with listen()
  • Meetily uses commands for control operations (start/stop recording, device queries) and events for streaming data (transcripts, download progress, status updates)
  • Key implementation files: lib.rs for command registration, recording_commands.rs and whisper_engine/commands.rs for domain logic, and various .tsx components for frontend integration

Frequently Asked Questions

How do I pass complex arguments from TypeScript to a Rust command?

Tauri automatically serializes TypeScript objects to JSON and deserializes them into Rust structs. Define matching types on both sides—use serde::Deserialize on the Rust struct and a TypeScript interface with identical field names. Meetily's start_recording command uses Option<String> parameters that map directly to TypeScript string | undefined.

Can Rust emit events before the frontend has registered a listener?

Events are fire-and-forget; if no listener is active, the event is dropped. Meetily avoids this race condition by setting up listeners in React context providers during component mount, before triggering commands that spawn background work. For critical events, consider buffering in Rust or having the frontend request missed state on reconnect.

What's the performance cost of Tauri commands versus events?

Commands cross the process boundary twice (request and response) with JSON serialization overhead—typically sub-millisecond for small payloads. Events cross once from Rust to JavaScript. Both are far more efficient than HTTP alternatives; Meetily streams live transcription with no perceptible latency using the event channel.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →