All playbooksNative Utility

Add a native command

Expose a quick Rust operation to the UI through app-api, a Tauri command with its own permission and the native client, with generated types and tests on both sides.

When to use it

The UI needs an answer from the Rust core that comes back at once, for example the size of the local database or the details of a chosen file. The steps use a storage_info command as the example. Work that can take more than a moment (reading a folder tree, network, devices) is a job: see the playbook "Add a job handler".

Rules to keep

  • The path is always UI → packages/native-client → Tauri command → crates/app-api → core crates. The command only calls one AppApi method; no logic in apps/desktop/src-tauri.
  • Every command needs its own permission in apps/desktop/src-tauri/capabilities/main-window.json. No global or wildcard permissions.
  • Types cross the boundary as DTOs from crates/app-api/src/dto.rs; the TypeScript versions are generated, never written by hand.
  • Errors are ApiError values with a stable RW_* code, never sentences. The UI translates the code.
  • A path that comes from the UI goes through validate_path before it is used.

Steps

  1. Add the DTO to crates/app-api/src/dto.rs with the same derives and attributes as the others (Serialize, Deserialize, the ts-export cfg_attr and rename_all = "camelCase"). Mark u64 fields with ts(type = "number") as JobDto does:

    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
    #[cfg_attr(feature = "ts-export", derive(TS), ts(export))]
    #[serde(rename_all = "camelCase")]
    pub struct StorageInfoDto {
        pub data_dir: String,
        #[cfg_attr(feature = "ts-export", ts(type = "number"))]
        pub database_bytes: u64,
    }
    
  2. Add the method to AppApi in crates/app-api/src/api.rs, returning Result<StorageInfoDto, ApiError>. Build failures with ApiError::new("RW_...", category, recoverable) and check disabled modules as the existing methods do. Test it in crates/app-api/tests/api.rs with the api(config()) helper, including the error case.

  3. Run pnpm types:generate, export the new type from packages/shared-types/src/index.ts and commit the files in packages/shared-types/src/generated. pnpm types:check fails if they are out of date.

  4. Add the command to the matching file in apps/desktop/src-tauri/src/commands/ (here filesystem.rs), following validate_path: it takes State<'_> and the arguments, and returns state.api.storage_info(). Register it in generate_handler! in src-tauri/src/main.rs, declare "storage_info" in build.rs and grant "allow-storage-info" in capabilities/main-window.json.

  5. Add the method to the NativeClient interface in packages/native-client/src/client.ts, to TauriNativeClient in tauri.ts (call<StorageInfoDto>("storage_info"); JavaScript arguments are camelCase and Tauri maps them to snake_case) and to MockNativeClient in mock.ts with realistic data and the error scenarios. Test the mock in mock.test.ts.

  6. Read it in the UI with a hook under apps/desktop/src/features/<feature>/hooks/, like useFeatures: useQuery with a stable key and useNativeClient(). Show errors with i18n.error.

  7. Add every new RW_* code to packages/i18n/locales/*/errors.json and every UI text to common.json, in de-DE, en and it-IT. Check the layout in German.

  8. List the command in docs/tauri-bridge.md.

Done when

  • the verification commands pass, and pnpm types:check reports no difference;
  • the command has exactly one allow-<name> entry in the capability and is registered in main.rs and build.rs;
  • the mock implements the method, so pnpm dev works without the Rust core;
  • every new code and text exists in the three locales.