Key Files and Directories Explained

Understand the purpose and role of every file and directory in a Tauri project's src-tauri folder.

Every Tauri project has two distinct halves: a frontend (JavaScript, TypeScript, or Rust‑based UI) and a Rust backend that lives inside src-tauri/. This directory holds everything Tauri needs to turn your web code into a desktop or mobile application. It contains configuration, security declarations, source code, and assets.

Understanding each file and folder in src-tauri/ is the difference between tweaking a template and genuinely owning your project. When something does not work — a command fails, a window title does not change, or an icon is missing — knowing which file to open saves you from guesswork. The sections below walk through the six key items you will encounter in every Tauri v2 project.

tauri.conf.json

This is the main configuration file for Tauri. The CLI reads it to know how to build and run your app, and the runtime reads it to set up windows, bundles, and plugins. It is always located at src-tauri/tauri.conf.json, and its presence marks the Rust project root for the CLI. The dedicated tauri.conf.json page covers the same file in isolation; the Configuration chapter goes deeper into product, build, and application settings.

The file is written in JSON by default, but Tauri also supports JSON5 and TOML if you enable the corresponding feature flags in Cargo.toml. The structure is the same regardless of format, so a setting like the app identifier works the same way whether you write "identifier": "com.example.app" in JSON or identifier = "com.example.app" in TOML.

Platform‑specific overrides:

Tauri can merge platform‑specific configuration files — like tauri.windows.conf.json or tauri.macos.conf.json — on top of the base tauri.conf.json. This lets you define different icons, window sizes, or permissions per operating system without duplicating the entire config.

What the file actually does

At a high level, tauri.conf.json groups settings into a few major sections:

  • build – where the frontend code lives during development and after it is compiled. The devUrl tells Tauri which local server to connect to during tauri dev, and frontendDist points to the folder that contains your built static files (the output of your frontend build tool).
  • app – runtime behavior: the list of windows, their titles, sizes, and whether they have decorations. Security settings like the Content Security Policy also live here.
  • bundle – packaging and distribution: the app’s icon files, supported installer formats, and platform‑specific signing identities.
  • plugins – configuration for official and community plugins such as the updater, deep‑link handler, or CLI.

The value of build.beforeDevCommand and build.beforeBuildCommand are shell commands that Tauri runs automatically. For a React project, beforeDevCommand is typically "npm run dev" and beforeBuildCommand is "npm run build". Tauri waits for the dev command to start the local server before opening the app window.

Invalid JSON breaks everything:

A single misplaced comma or missing quote in tauri.conf.json will prevent Tauri from starting — often with a confusing error message. Always validate your JSON after editing, especially if you are hand‑editing arrays like "icon" or "targets".

A minimal working config

Here is the smallest tauri.conf.json that defines a working Tauri v2 app. The identifier must be unique; use reverse‑domain notation like com.yourname.yourapp.

src-tauri/tauri.conf.json
{
  "$schema": "https://schema.tauri.app/config/2",
  "productName": "MyApp",
  "version": "0.1.0",
  "identifier": "com.myorg.myapp",
  "build": {
    "beforeDevCommand": "npm run dev",
    "devUrl": "http://localhost:5173",
    "beforeBuildCommand": "npm run build",
    "frontendDist": "../dist"
  },
  "app": {
    "windows": [
      {
        "title": "My App",
        "width": 800,
        "height": 600
      }
    ]
  }
}

The $schema line is optional but activates autocompletion and validation in editors like VS Code. The build.devUrl must match the port your frontend dev server actually uses. For a Vite project that is typically 5173; if you use another framework, check its default port.

Your config is valid if...:

Running tauri dev starts your app without configuration errors. If the window opens and shows your frontend, the essential settings are correct.

Common mistakes

  • Forgetting the identifier field. Without it, builds will fail because every platform requires a unique app ID.
  • Pointing frontendDist to the wrong folder. This path is relative to src-tauri, so ../dist means “go up one level from src-tauri and then into dist”. If your build tool outputs to build or out, update this path accordingly.
  • Using the same port for devUrl as another running app. Tauri’s built‑in static file server (used when there is no dev command) defaults to port 1430; your frontend dev server should use a different port.

capabilities/ Directory

Inside src-tauri/capabilities/ you will find at least one file, usually named default.json. This directory is where Tauri v2’s capability‑based permission system lives. The capabilities Directory page expands on file layout; Understanding Capabilities covers the security model.

Every command or API that your frontend wants to call — whether a built‑in Tauri API like dialog or a custom Rust command you write — must be listed in a capability file. If a permission is missing, the call will simply fail, often without a helpful error message in the running app.

Why Tauri v2 uses capabilities

Tauri v1 used an allow‑list: you would set "fs": { "scope": ["..."] } in the config, and everything inside that scope was available. The capability model replaces that with explicit, named permissions. This gives you much finer control: you can allow “read‑only access to the Documents directory” without accidentally also granting write access to the whole filesystem.

The capability system also makes it impossible for a third‑party plugin to silently escalate privileges. A plugin can request permissions, but your app’s capability files are the only place those permissions can be granted.

A missing permission causes silent failures:

When the frontend calls a command that has not been granted, the Promise rejects with a generic error. Tauri does not warn you at build time. If an API call suddenly stops working after you add a new plugin or command, the first place to check is whether the corresponding permission is listed in a capability file.

The structure of a capability file

A capability file is a JSON document with three main fields:

  • identifier – a unique name for this capability set (commonly "default").
  • description – a human‑readable explanation of what the permissions are for.
  • windows – which app windows these permissions apply to. ["*"] means all windows.
  • permissions – an array of permission identifiers. These can be built‑in (like "core:default") or custom (like "my-plugin:allow-read-file").

Here is the default.json file generated by create-tauri-app:

src-tauri/capabilities/default.json
{
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "shell:allow-open"
  ]
}
  • core:default enables a set of fundamental Tauri APIs that almost every app needs: window manipulation, event listening, and basic app metadata.
  • shell:allow-open lets the frontend open URLs in the system browser via the shell plugin.

When you add a new plugin or create a custom command, you will extend this list. For example, adding "dialog:allow-open" would let the frontend show a file open dialog.

How it connects to code

Suppose you write a Rust command:

src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

Before the frontend can invoke greet, you need to add a permission for it. You can either create a new capability file or modify default.json. The permission identifier for a custom command follows the pattern "app-name:default" unless you explicitly set a different identifier.

You know capabilities are set up correctly if...:

After adding a permission and rebuilding, calling the command from the frontend returns the expected result instead of a rejected Promise. The Tauri devtools console can also show which permissions are active at runtime.

icons/ Directory

The icons/ directory inside src-tauri is the default output folder for the tauri icon command. It is where platform‑specific icon files land after you generate them from a source image. See the icons Directory page and Application Icons for bundle-side configuration.

Every operating system expects a different icon format:

PlatformRequired Format
Windows.ico
macOS.icns
Linux.png

Tauri’s bundle configuration references these files through the bundle.icon array:

src-tauri/tauri.conf.json
{
  "bundle": {
    "icon": [
      "icons/32x32.png",
      "icons/128x128.png",
      "icons/128x128@2x.png",
      "icons/icon.icns",
      "icons/icon.ico"
    ]
  }
}

The tauri icon command reads a high‑resolution source image (recommended 1024×1024 or larger) and automatically generates all required sizes and formats. After running it, the output files land in icons/, and the icon array in tauri.conf.json points to them.

Regenerate after a design change:

If you update your app’s logo, running tauri icon again will overwrite the old icons. Make sure your source image has a transparent background if your platform guidelines recommend it (macOS does, for example).

A low‑resolution source produces blurry icons:

If you feed a 256×256 image to tauri icon, the generated icons will look pixelated on high‑DPI screens. Always start with at least 1024×1024 pixels to allow downscaling without quality loss.

build.rs

The build.rs file at the root of src-tauri is a Rust build script. Cargo (the Rust build system) executes it before compiling your crate. In a Tauri project, its sole job is to invoke tauri_build::build(). The build.rs page is the focused reference.

src-tauri/build.rs
fn main() {
    tauri_build::build()
}

This single function call triggers several critical compile‑time steps:

  1. It reads tauri.conf.json and validates the schema.
  2. It embeds the configuration into the compiled binary so the runtime can access it without reading a file on disk.
  3. It generates code for things like the app’s version string, which the frontend can retrieve through the API.

Without this build script, the Tauri runtime would not know basic information about your app, and compilation would fail with missing constants.

Do not remove build.rs:

Deleting or emptying build.rs will cause compilation errors because the Tauri runtime expects certain generated code to exist. The only reason to modify it is to enable a different config format, such as adding features = ["config-json5"] to tauri-build in Cargo.toml.

If you enable JSON5 or TOML config support, you do not change build.rs itself. Instead, you modify Cargo.toml:

src-tauri/Cargo.toml
[build-dependencies]
tauri-build = { version = "2", features = ["config-json5"] }

The build script stays exactly as it is — the feature flag tells tauri_build::build() to handle the rest.

src/lib.rs

src/lib.rs is the main Rust library file for your Tauri app. It is the single most important Rust file you will edit, because all your backend logic lives here. See src/lib.rs and Creating Your First Rust Command.

Tauri compiles your Rust code as a library (not just a binary) so that both desktop and mobile targets can reuse it. On desktop, main.rs calls a function from this library. On mobile, the platform’s native framework loads the library directly. This architecture means you write your app logic once in lib.rs, and it works everywhere.

The anatomy of lib.rs

At minimum, lib.rs contains:

  • The app builder setup, which registers commands, plugins, and state.
  • The mobile entry point function, annotated with #[cfg_attr(mobile, tauri::mobile_entry_point)].
  • Any custom commands you want to expose to the frontend.

Here is a bare‑bones lib.rs that registers a single command:

src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

The tauri::generate_handler![] macro registers all commands so the frontend can call them. The tauri::generate_context!() macro reads the embedded configuration at runtime.

Good practice: keep main.rs thin:

Because lib.rs is the shared entry point for both desktop and mobile, any code you place there is automatically cross‑platform. Putting application setup in main.rs instead means it will never run on iOS or Android.

Common mistakes

  • Writing logic in main.rs. If you start the builder or register commands in the binary entry point, those commands will be unavailable on mobile. Always do app setup in lib.rs.
  • Forgetting #[cfg_attr(mobile, tauri::mobile_entry_point)]. This attribute ensures the function is compiled as the mobile entry point only when targeting mobile platforms. Without it, your app will not start on iOS or Android.
  • Not registering a command. The frontend will receive an error that looks like a missing permission, but the real problem is that the command was never added to invoke_handler.

src/main.rs

src/main.rs is the desktop entry point. It is a tiny binary crate that calls into the library you wrote in lib.rs. Its entire purpose is to be an executable that the operating system can launch. The src/main.rs page covers the Windows subsystem attribute and why this file stays thin.

src-tauri/src/main.rs
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
    app_lib::run()
}

Two things happen here:

  1. The windows_subsystem attribute hides the terminal window on Windows when the app is compiled in release mode. Removing this line causes an empty console window to appear behind your app.
  2. app_lib::run() is a call to the run function defined in lib.rs. The name app_lib comes from the [lib] section in Cargo.toml, where name = "app_lib" is set by the project template.

Do not add application logic here:

Anything you add to main.rs will only execute on desktop. If you later decide to ship a mobile version, that logic will silently vanish. Keep the desktop entry point as a thin launcher.

The relationship between main.rs and lib.rs is one of those design decisions that only makes sense once you understand the mobile compilation story. On desktop, you need an executable binary. On mobile, the OS expects a library. By keeping the real app inside a library, Tauri gives you both from one codebase. main.rs is just the small piece that turns the library into something a desktop OS can double‑click.


What you now know

These six items form a stack that goes from configuration to execution:

  • tauri.conf.json defines what your app is and how it should be built.
  • capabilities/ controls what your frontend is allowed to do.
  • icons/ provides the visual identity that platforms display.
  • build.rs prepares compile‑time data so the runtime has everything it needs.
  • src/lib.rs is where you write your app’s behavior, shared across all platforms.
  • src/main.rs is the thin desktop launcher that turns the library into an executable.

Each file has a narrow, non‑overlapping responsibility. When something breaks, the fix is almost always in exactly one of these files — and now you know which one to open first.

tauri.conf.json - The Core Configuration File

A complete reference for the Tauri v2 configuration file covering its structure, every major section, platform-specific overrides, CLI extension, and common pitfalls beginners hit.

Capabilities Directory

Understand the capabilities directory in Tauri v2 projects and how to control which Rust commands your frontend can call

The icons Directory

How the src-tauri/icons directory stores application icons for all platforms and how to generate custom icons with tauri icon

build.rs

Understand the role of the build script in a Tauri project, how it integrates with Cargo, and how to customize it safely.

src-lib.rs - The Rust library entry point for Tauri apps

Understand why Tauri uses a library crate entry point src-lib.rs how it enables desktop and mobile builds and how to register commands plugins and setup logic there

Understanding src/main.rs in Tauri v2

Deep dive into the src/main.rs file — the desktop entry point of a Tauri v2 project. Learn what it does, why it exists, how it connects to lib.rs and Cargo.toml, and why you should rarely touch it.