Starter Shape

Start with the smallest app that proves the boundary: one web UI, one Rust backend, one capability file, one typed IPC wrapper, and one release smoke path.

Folder Shape

my-app/
  package.json
  vite.config.ts
  src/
    app.tsx
    tauri-api.ts
    tauri-api.test.ts
  src-tauri/
    Cargo.toml
    tauri.conf.json
    capabilities/
      default.json
    src/
      lib.rs
      commands.rs
      state.rs

Keep the frontend and native boundary obvious. Do not scatter raw invoke calls across the UI.

Frontend Wrapper

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

export type OpenProjectRequest = {
  path: string;
};

export type OpenProjectResponse = {
  name: string;
  itemCount: number;
};

export function openProject(input: OpenProjectRequest) {
  return invoke<OpenProjectResponse>("open_project", input);
}

This does not make IPC type-safe by itself. It gives you one place to test names, payloads, validation assumptions, and frontend error handling.

Rust Command

use serde::Serialize;

#[derive(Serialize)]
struct OpenProjectResponse {
    name: String,
    item_count: usize,
}

#[tauri::command]
fn open_project(path: String) -> Result<OpenProjectResponse, String> {
    if path.trim().is_empty() {
        return Err("path is required".into());
    }

    Ok(OpenProjectResponse {
        name: "example".into(),
        item_count: 0,
    })
}

pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![open_project])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Validate before touching the filesystem, shell, network, or user data. The command boundary is not a formality.

Capability File

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Main window permissions",
  "windows": ["main"],
  "permissions": [
    "core:default"
  ]
}

Add plugin permissions only when a feature needs them. Prefer narrow scoped permissions over broad defaults.

Add Complexity In This Order

StepAddStop if
1One validated Rust commandThe UI needs broad shell/filesystem authority
2Typed IPC wrapper and frontend mock testsRaw command strings spread across screens
3App state through Builder::manage and State<'_, T>State needs async locking and ownership is unclear
4Official pluginThe permission file grants more than the feature needs
5SidecarYou cannot define args, logs, ports, cleanup, and release packaging
6UpdaterSigning keys, endpoints, and artifact generation are not planned
7Mobile smokeRequired plugins do not document Android/iOS support

What To Exclude From v1

  • Custom plugin system unless commands are already too large or reusable.
  • Sidecars for work that can be done safely in Rust commands.
  • Dynamic shell arguments without validators.
  • Remote API access from the webview unless the threat model is explicit.
  • Mobile support claims before physical device or emulator smoke tests.
  • Auto-update until you know your signing, endpoints, rollback, and key storage story.