How the Meetily Frontend Invokes Tauri Commands and Listens for Rust Events

Meetily's Next.js frontend communicates with its Rust backend using Tauri's invoke API to execute commands and the listen API to subscribe to events, enabling real-time bidirectional data flow between the React UI and native audio processing code.

The Meetily desktop application bridges a Next.js React interface with a high-performance Rust core using Tauri's inter-process communication layer. Understanding how the frontend invokes Tauri commands and listens for events from Rust reveals the architecture that keeps the UI responsive while handling compute-intensive tasks like audio capture, VAD processing, and Whisper transcription.

Invoking Tauri Commands from TypeScript

The frontend centralizes all Rust command calls inside service modules. In frontend/src/services/recordingService.ts, the application imports ** invoke ** from @tauri-apps/api/core and exposes thin async wrappers that forward arguments to the Rust side.

Every command registered in the Tauri handler can be triggered using the string identifier defined in Rust. The recording service implements standard CRUD-style operations for the recorder:

// frontend/src/services/recordingService.ts
import { invoke } from '@tauri-apps/api/core';

export async function startRecording() {
  return invoke('start_recording');
}

export async function stopRecording() {
  return invoke('stop_recording');
}

export async function pauseRecording() {
  return invoke('pause_recording');
}

export async function resumeRecording() {
  return invoke('resume_recording');
}

Additional commands manage AI model lifecycles and analytics. The frontend invokes whisper_load_model to initialize transcription engines, builtin_ai_download_model for on-device LLMs, and track_event for telemetry, all following the same pattern of passing a command name and optional payload to invoke.

Listening for Rust Events in React

While commands follow a request-response pattern, events enable the Rust backend to push updates to the frontend asynchronously. The application imports ** listen ** from @tauri-apps/api/event to subscribe to channels that the Rust code emits via app.emit().

Recording Lifecycle Events

The recording service exposes functions that return ** UnlistenFn ** callbacks, allowing React components to subscribe to state changes:

// frontend/src/services/recordingService.ts
import { listen } from '@tauri-apps/api/event';

export async function onRecordingStarted(callback: () => void) {
  return listen('recording-started', callback);
}

export async function onRecordingStopped(callback: () => void) {
  return listen('recording-stopped', callback);
}

export async function onRecordingPaused(callback: () => void) {
  return listen('recording-paused', callback);
}

export async function onRecordingResumed(callback: () => void) {
  return listen('recording-resumed', callback);
}

Transcription Streaming Events

Real-time transcription updates flow through frontend/src/services/transcriptService.ts, which listens for chunk-level updates and completion signals:

// frontend/src/services/transcriptService.ts
import { listen } from '@tauri-apps/api/event';

export interface TranscriptPayload {
  text: string;
  isFinal: boolean;
}

export async function onTranscriptUpdate(callback: (event: { payload: TranscriptPayload }) => void) {
  return listen('transcript-update', callback);
}

export async function onTranscriptionComplete(callback: (event: { payload: { text: string } }) => void) {
  return listen('transcription-complete', callback);
}

export async function onTranscriptionError(callback: (event: { payload: { error: string } }) => void) {
  return listen('transcription-error', callback);
}

Managing Listener Lifecycles in React Hooks

To prevent memory leaks, hooks like useRecordingStop (found in frontend/src/hooks/useRecordingStop.ts) store the unlisten functions returned by the service layer and invoke them during component unmount.

// frontend/src/hooks/useRecordingStop.ts
import { useEffect, useRef } from 'react';
import { onRecordingStopped } from '@/services/recordingService';

export function useRecordingStop(onStop: () => void) {
  const unlistenRef = useRef<(() => void) | null>(null);

  useEffect(() => {
    const setupListener = async () => {
      const unlisten = await onRecordingStopped(() => {
        onStop();
      });
      unlistenRef.current = unlisten;
    };

    setupListener();

    return () => {
      if (unlistenRef.current) {
        unlistenRef.current();
      }
    };
  }, [onStop]);
}

This pattern ensures that Rust events do not attempt to update unmounted React components, which is critical in a desktop environment where users frequently switch views during long-running recordings.

Rust Implementation: Command Registration and Event Emission

On the Rust side, commands are defined in frontend/src-tauri/src/lib.rs using the #[tauri::command] attribute macro and registered via tauri::generate_handler!.

// frontend/src-tauri/src/lib.rs
#[tauri::command]
async fn start_recording(
    state: State<'_, RecordingState>,
    device_names: Vec<String>,
    meeting_name: String,
) -> Result<String, String> {
    // Audio pipeline initialization
    Ok("recording_started".to_string())
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            start_recording,
            stop_recording,
            pause_recording,
            resume_recording,
            whisper_load_model,
            track_event,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

During execution, Rust emits events through the Tauri application handle. For example, when recording actually begins, the backend emits the recording-started event that the frontend is already awaiting:

// frontend/src-tauri/src/audio/recordingManager.rs (conceptual)
app.emit("recording-started", Payload { timestamp: now });

Complete Data Flow Example

The full communication cycle illustrates the separation of concerns:

  1. User Action: React component calls startRecording() service function
  2. Invocation: Service executes invoke('start_recording', { device_names, meeting_name })
  3. Rust Execution: Command handler in lib.rs receives arguments and initializes the audio pipeline
  4. Event Emission: When hardware initialization succeeds, Rust calls app.emit("recording-started", ...)
  5. Frontend Update: The onRecordingStarted listener in the React hook receives the payload and updates component state
  6. Cleanup: Component unmount triggers the unlisten function, removing the subscription

This architecture ensures that computationally expensive work stays in Rust while the React UI remains responsive to user input.

Summary

  • Command Invocation: The frontend uses invoke from @tauri-apps/api/core in frontend/src/services/recordingService.ts to trigger Rust functions like start_recording and whisper_load_model.
  • Event Subscription: The listen function from @tauri-apps/api/event creates subscriptions to Rust-emitted events including recording-started, transcript-update, and transcription-complete.
  • Memory Management: React hooks store UnlistenFn callbacks returned by listeners and clean them up on unmount to prevent leaks.
  • Rust Registration: Commands are defined with #[tauri::command] in frontend/src-tauri/src/lib.rs and emitted via app.emit() from managers like recordingManager.rs.

Frequently Asked Questions

How does Meetily pass arguments when invoking Tauri commands?

Meetily passes arguments as the second parameter to invoke. For example, when starting a recording, the frontend calls invoke('start_recording', { device_names, meeting_name }), where the object matches the parameter signature defined in the Rust #[tauri::command] handler. The Tauri framework automatically serializes the JavaScript object into Rust types.

What events does the Meetily Rust backend emit to the frontend?

The backend emits recording lifecycle events (recording-started, recording-stopped, recording-paused, recording-resumed) through recordingService.ts, and transcription-specific events (transcript-update, transcription-complete, transcription-error) through transcriptService.ts. These events carry typed payloads that TypeScript interfaces define for type safety.

How does Meetily prevent memory leaks from event listeners?

Each call to listen returns an UnlistenFn that removes the subscription. React hooks like useRecordingStop store this function in a ref and invoke it inside the effect cleanup function when the component unmounts, ensuring that Rust cannot emit events to destroyed React components.

Where are Tauri commands registered in the Meetily codebase?

Commands are registered in frontend/src-tauri/src/lib.rs using the tauri::generate_handler! macro, which takes a list of functions marked with #[tauri::command]. This registration occurs during the Tauri application build phase within the main() function before tauri::Builder::default().run() executes.

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 →