How to Add a New Tauri Command in Meetily's Rust Backend

To add a new Tauri command in Meetily, implement an async Rust function annotated with #[tauri::command] in a module like frontend/src-tauri/src/api/commands.rs, then register it in the tauri::generate_handler! macro inside frontend/src-tauri/src/lib.rs (around line 526), and finally invoke it from the TypeScript frontend using invoke('command_name').

Meetily is an open-source meeting assistant built with Tauri, combining a Rust backend with a web-based frontend. Adding custom backend functionality requires exposing new commands through Tauri's IPC system that bridge the frontend and Rust layers. This guide walks through the exact implementation pattern used in the Zackriya-Solutions/meetily repository, referencing specific source files and line numbers.

Understanding Meetily's Tauri Command Architecture

Tauri commands in Meetily are regular Rust async functions marked with the #[tauri::command] attribute. They reside in domain-specific modules such as frontend/src-tauri/src/api/commands.rs for generic operations or frontend/src-tauri/src/audio/recording_commands.rs for audio-specific functionality.

The global registration happens in frontend/src-tauri/src/lib.rs via the tauri::generate_handler! macro (found around lines 526-540). This macro collects all exposed function pointers and builds the IPC dispatcher that routes invoke calls from the JavaScript frontend to the appropriate Rust handler. Existing commands like start_recording, stop_recording, and is_recording follow this exact pattern, making them ideal reference implementations.

Step-by-Step Implementation Guide

1. Implement the Command Function

Create or open a logical module under frontend/src-tauri/src/. For generic API utilities, use api/commands.rs; for feature-specific logic, follow the pattern in audio/recording_commands.rs.

Define a pub async fn with the #[tauri::command] attribute. Accept AppHandle<R> (where R: Runtime) if you need access to Tauri state or event emitters. Return Result<T, String> to allow proper error handling in the frontend.

use tauri::{AppHandle, Runtime};

#[tauri::command]
pub async fn ping<R: Runtime>(_app: AppHandle<R>) -> Result<String, String> {
    Ok("pong".to_string())
}

2. Register the Command in lib.rs

Open frontend/src-tauri/src/lib.rs and locate the invoke_handler block around line 526. Add your function identifier to the tauri::generate_handler! list, maintaining the existing comma separation.

.invoke_handler(tauri::generate_handler![
    start_recording,
    stop_recording,
    is_recording,
    // ... existing commands ...
    ping,  // newly added command
])

The generate_handler! macro expands this list into the routing table used by the Tauri runtime to dispatch IPC calls.

3. Invoke from the Frontend

Call the command from your TypeScript code using the invoke function from @tauri-apps/api/tauri. Pass the command name as the first argument and an object containing any arguments as the second.

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

async function testBackend() {
  try {
    const response = await invoke<string>('ping');
    console.log(response); // Outputs: "pong"
  } catch (error) {
    console.error('Command failed:', error);
  }
}

Complete Working Example: The Ping Command

Here is the full implementation of a health-check command that returns "pong", following the patterns found in the Meetily codebase.

File: frontend/src-tauri/src/api/commands.rs

use tauri::{AppHandle, Runtime};

/// Simple health-check endpoint demonstrating the command pattern.
/// Mirrors the structure of start_recording in lib.rs (lines 83-108).
#[tauri::command]
pub async fn ping<R: Runtime>(_app: AppHandle<R>) -> Result<String, String> {
    // In production commands, you might access state or emit events here
    Ok("pong".to_string())
}

Update in: frontend/src-tauri/src/lib.rs (line ~526)

    .invoke_handler(tauri::generate_handler![
        start_recording,
        stop_recording,
        is_recording,
        ping,  // Add this line
    ])

Frontend usage: Any TypeScript component

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

const checkBackendHealth = async () => {
  const reply = await invoke<string>('ping');
  return reply === 'pong';
};

Advanced Command Patterns

Handling Command Arguments

Commands can accept serializable arguments from the frontend. Define parameters after the AppHandle (if present). Meetily's set_meeting_name pattern demonstrates storing frontend data in backend state.

#[tauri::command]
pub async fn set_meeting_name<R: Runtime>(
    app: AppHandle<R>, 
    name: String,
    meeting_id: u64
) -> Result<(), String> {
    // Access shared state or database via app.state::<YourState>()
    Ok(())
}

Call with:

await invoke('set_meeting_name', { name: 'Team Sync', meetingId: 123 });

Emitting Events to the Frontend

Use app.emit() to send real-time updates without waiting for a return value. This pattern appears in Meetily's recording commands for progress updates.

#[tauri::command]
pub async fn process_data<R: Runtime>(app: AppHandle<R>) -> Result<(), String> {
    // Perform work...
    
    app.emit("processing-complete", serde_json::json!({ 
        "status": "success",
        "timestamp": chrono::Utc::now().to_rfc3339()
    }))
    .map_err(|e| e.to_string())
}

Listen in TypeScript:

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

listen('processing-complete', (event) => {
  console.log('Backend event:', event.payload);
});

Accessing Shared State

Access the application state defined in frontend/src-tauri/src/state.rs using app.state::<T>(). This allows commands to interact with global resources like the recording session or database connections.

use crate::state::MeetingState;

#[tauri::command]
pub async fn get_current_meeting<R: Runtime>(
    app: AppHandle<R>
) -> Result<String, String> {
    let state = app.state::<MeetingState>();
    state.get_active_meeting()
        .map_err(|e| e.to_string())
}

Summary

  • Implement a pub async fn with the #[tauri::command] attribute in the appropriate module under frontend/src-tauri/src/, using AppHandle<R> for state access and returning Result<T, String>.
  • Register the function name in the tauri::generate_handler! macro inside frontend/src-tauri/src/lib.rs (around line 526) to expose it to the frontend.
  • Invoke the command from TypeScript using invoke('command_name', { args }) from @tauri-apps/api/tauri, handling the returned Promise for results or errors.

Frequently Asked Questions

Where should I place new Tauri commands in the Meetily codebase?

Place commands in domain-specific modules under frontend/src-tauri/src/. Use api/commands.rs for general utilities, create audio/recording_commands.rs for recording features, or add to existing feature modules. Ensure the module is declared in lib.rs if it's new, and that functions are marked pub so they're visible to the generate_handler! macro.

How do I access application state from within a command?

Import AppHandle<R> from tauri as the first parameter to your command function, then call app.state::<YourStateType>() where YourStateType is defined in state.rs. For example, app.state::<MeetingState>() gives you access to shared meeting data. This follows the same pattern used by start_recording in lib.rs lines 119-131.

Can I return custom error types instead of String?

Yes, any type implementing std::fmt::Display or serde::Serialize works, but String errors serialize cleanly to JavaScript Error objects. For custom error types, implement From<YourError> for String or use map_err(|e| e.to_string()) before returning. The frontend receives these as rejected Promise values.

Why is my new command returning "command not found" in the frontend?

This occurs when the function isn't registered in the generate_handler! list in lib.rs. Verify the function name appears in the macro invocation (around line 526), that it's marked pub, and that you've rebuilt the application with ./clean_run.sh or cargo build. Changes to Rust code require recompilation before the frontend can discover new commands.

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 →