---
title: Writing Plugin Permissions
sidebar:
  order: 11
i18nReady: true
---

import { Steps } from '@astrojs/starlight/components';
import ShowSolution from '@components/ShowSolution.astro'
import Cta from '@fragments/cta.mdx';

The goal of this exercise is to get a better understanding on how
plugin permissions can be created when writing your own plugin.

At the end you will have the ability to create simple permissions for
your plugins.
You will have an example Tauri plugin where permissions are partially autogenerated
and hand crafted.

<Steps>

1. ### Create a Tauri Plugin

    In our example we will facilitate the Tauri [`cli`](/reference/cli/)
    to bootstrap a Tauri plugin source code structure.
    Make sure you have installed all [Prerequisites](/start/prerequisites/)
    and verify you have the Tauri CLI in the correct version
    by running `cargo tauri info`.

    The output should indicate the `tauri-cli` version is `2.x`.
    We will proceed in this step-by-step explanation with `pnpm` but you can choose another
    package manager and replace it in the commands accordingly.

    Once you have a recent version installed you can go
    ahead and create the plugin using the Tauri CLI.

    <ShowSolution>
    ```sh
    mkdir -p tauri-learning
    cd tauri-learning
    cargo tauri plugin new test
    cd tauri-plugin-test
    pnpm install
    pnpm build
    cargo build
    ```
    </ShowSolution>

2. ### Create a New Command

    To showcase something practical and simple let us assume
    our command writes user input to a file in our temporary folder while
    adding some custom header to the file.

    Let's name our command `write_custom_file`, implement it in `src/commands.rs`
    and add it to our plugin builder to be exposed to the frontend.

    Tauri's core utils will autogenerate `allow` and `deny` permissions for this
    command, so we do not need to care about this.

    <ShowSolution>

    The command implementation:

    ```rust title="src/commands.rs" ins={15-22} ins=", Manager"
    use tauri::{AppHandle, command, Runtime};

    use crate::models::*;
    use crate::Result;
    use crate::TestExt;

    #[command]
    pub(crate) async fn ping<R: Runtime>(
        app: AppHandle<R>,
        payload: PingRequest,
    ) -> Result<PingResponse> {
        app.test1().ping(payload)
    }

    #[command]
    pub(crate) async fn write_custom_file<R: Runtime>(
        user_input: String,
        app: AppHandle<R>,
    ) -> Result<String> {
        std::fs::write(app.path().temp_dir().unwrap(), user_input)?;
        Ok("success".to_string())
    }
   
    ```

    Auto-Generate inbuilt permissions for your new command:

    ```rust title="src/build.rs" ins="\"write_custom_file\""
    const COMMANDS: &[&str] = &["ping", "write_custom_file"];
    ```

    These inbuilt permissions will be automatically generated by the Tauri build
    system and will be visible in the `permissions/autogenerated/commands` folder.
    By default an `enable-<command>` and `deny-<command>` permission will
    be created.

    </ShowSolution>
3. ### Expose the New Command

    The previous step was to write the actual command implementation.
    Next we want to expose it to the frontend so it can be consumed.

    <ShowSolution>
 
    Configure the Tauri builder to generate the invoke handler to pass frontend
    IPC requests to the newly implemented command:

    ```rust title="src/lib.rs"  ins="commands::write_custom_file,"
    pub fn init<R: Runtime>() -> TauriPlugin<R> {
    Builder::new("test")
        .invoke_handler(tauri::generate_handler![
            commands::ping,
            commands::write_custom_file,
        ])
        .setup(|app, api| {
            #[cfg(mobile)]
            let test = mobile::init(app, api)?;
            #[cfg(desktop)]
            let test = desktop::init(app, api)?;
            app.manage(test);

            // manage state so it is accessible by the commands
            app.manage(MyState::default());
            Ok(())
        })
        .build()
    }
    ```

    Expose the new command in the frontend module.

    This step is essential for the example application to successfully
    import the frontend module. This is for convenience and has
    no security impact, as the command handler is already generated
    and the command can be manually invoked from the frontend.

    ```ts title="guest-js/index.ts" ins={11-13}
    import { invoke } from '@tauri-apps/api/core'

    export async function ping(value: string): Promise<string | null> {
      return await invoke<{value?: string}>('plugin:test|ping', {
        payload: {
          value,
        },
      }).then((r) => (r.value ? r.value : null));
    }

    export async function writeCustomFile(user_input: string): Promise<string> {
     return await invoke('plugin:test|write_custom_file',{userInput: user_input});
    }
    ```

    :::tip
    The invoke parameter needs to be CamelCase. In this example it is `userInput` instead of `user_input`. 
    :::

    Make sure your package is built:

    ```
    pnpm build
    ```

    </ShowSolution>

4. ### Define Default Plugin Permissions

    As our plugin should expose the `write_custom_file` command by default
    we should add this to our `default.toml` permission.

    <ShowSolution>
    Add this to our default permission set to allow the new command
    we just exposed.

    ```toml title="permissions/default.toml" ins=", \"allow-write-custom-file\""
    "$schema" = "schemas/schema.json"
    [default]
    description = "Default permissions for the plugin"
    permissions = ["allow-ping", "allow-write-custom-file"]
    ```
    </ShowSolution>

5. ### Invoke Test Command from Example Application
    
    The created plugin directory structure contains an `examples/tauri-app` folder,
    which has a ready to use Tauri application to test out the plugin.

    Since we added a new command we need to slightly modify the frontend to
    invoke our new command instead.

    <ShowSolution>
    ```svelte title="src/App.svelte" del={11-13,42-45} ins={14-16,45-49}
    <script>
      import Greet from './lib/Greet.svelte'
      import { ping, writeCustomFile } from 'tauri-plugin-test-api'

      let response = ''

      function updateResponse(returnValue) {
        response += `[${new Date().toLocaleTimeString()}]` + (typeof returnValue === 'string' ? returnValue : JSON.stringify(returnValue)) + '<br>'
      }

      function _ping() {
        ping("Pong!").then(updateResponse).catch(updateResponse)
      }
      function _writeCustomFile() {
        writeCustomFile("HELLO FROM TAURI PLUGIN").then(updateResponse).catch(updateResponse)
      }
    </script>

    <main class="container">
      <h1>Welcome to Tauri!</h1>

      <div class="row">
        <a href="https://vitejs.dev" target="_blank">
          <img src="/vite.svg" class="logo vite" alt="Vite Logo" />
        </a>
        <a href="https://tauri.app" target="_blank">
          <img src="/tauri.svg" class="logo tauri" alt="Tauri Logo" />
        </a>
        <a href="https://svelte.dev" target="_blank">
          <img src="/svelte.svg" class="logo svelte" alt="Svelte Logo" />
        </a>
      </div>

      <p>
        Click on the Tauri, Vite, and Svelte logos to learn more.
      </p>

      <div class="row">
        <Greet />
      </div>

      <div>
        <button on:click="{_ping}">Ping</button>
        <div>{@html response}</div>
      </div>
      <div>
        <button on:click="{_writeCustomFile}">Write</button>
        <div>{@html response}</div>
      </div>


    </main>

    <style>
      .logo.vite:hover {
        filter: drop-shadow(0 0 2em #747bff);
      }

      .logo.svelte:hover {
        filter: drop-shadow(0 0 2em #ff3e00);
      }
    </style>
    ```

    Running this and pressing the "Write" button you should be greeted with this:

    ```
    success
    ```

    And you should find a `test.txt` file in your temporary folder containing a message
    from our new implemented plugin command. 
    🥳

    </ShowSolution>

 </Steps>   
