Organizing Configuration

Learn how to structure, split, and maintain your Tauri application configuration files for long-term maintainability.

A Tauri project has a surprising number of configuration files. There is the main tauri.conf.json, capability definitions, the Rust project manifest, frontend tooling configs, environment files, and platform-specific overrides. If you add them all without a plan, you will eventually have a folder where no one remembers which file controls which behavior. That is the problem organisation solves.

This document covers the concrete structures and habits that keep your Tauri configuration predictable, searchable, and easy to change months later.

Tauri Configuration Files Overview

Before organising, you need to know what you are organising. Here are the files that directly influence how a Tauri app behaves:

File / DirectoryPurpose
src-tauri/tauri.conf.json (or .json5 / Tauri.toml)Central app, build, bundle, and window configuration
src-tauri/tauri.*.conf.jsonPlatform‑specific overrides (merged on top of the main config)
src-tauri/capabilities/*.jsonPermission declarations for windows and plugins
src-tauri/Cargo.tomlRust dependencies, crate metadata, and build profiles
Root package.jsonFrontend scripts and dependencies (Vite, React, TypeScript)
vite.config.tsVite dev server and build settings
.env / .env.productionEnvironment variables consumed by the frontend or Tauri CLI
src-tauri/tauri.propertiesAndroid version metadata (if you commit it)

All of these are text files that you will edit by hand. The layout on disk matters because Tauri expects certain names and relative paths. When you rearrange things without understanding those expectations, the build breaks with an error that rarely explains the real cause.

Do Not Rename Config Files Arbitrarily:

Tauri looks for tauri.conf.json (or Tauri.toml) in the src-tauri directory by convention. Renaming the file or moving it to a subdirectory requires changing the tauri CLI invocation, which is not straightforward. Keep the top‑level configuration file exactly where the scaffolding placed it.

Folder Structure for a Clean Configuration

A typical Tauri project with React and Vite starts with this layout:

my-app/
├── src/                     # React frontend source
├── public/                  # Static assets for Vite
├── src-tauri/
│   ├── tauri.conf.json
│   ├── Cargo.toml
│   ├── capabilities/
│   │   └── default.json
│   ├── icons/               # App icons
│   └── src/
│       ├── main.rs
│       └── lib.rs
├── package.json
├── vite.config.ts
└── tsconfig.json

That is the scaffold. The moment your app grows, the single tauri.conf.json swells with dozens of windows, bundle customisations, and plugin settings. Organising configuration means two things: keeping related settings physically close on disk, and reducing the cognitive load of a single enormous file.

Adding a Configuration Directory

Create a config/ folder inside src-tauri to hold supplementary configuration that is not directly consumed by the Tauri CLI but drives your Rust backend or build scripts. For example:

src-tauri/
├── config/
│   ├── logging.toml
│   ├── feature-flags.json
│   └── migrations/
├── tauri.conf.json
├── capabilities/
├── ...

This folder is purely for your own Rust code to read at runtime via std::fs or through a resource. It does not interfere with Tauri’s boot process.

Resources versus Config:

If you want to ship a configuration file inside the final bundle, use the Tauri resource system instead. Add the file to the bundle > resources array in tauri.conf.json and access it through the resourceDir path. Files in a plain config/ folder are not automatically bundled unless you tell Tauri to include them.

Capabilities Directory

Tauri v2 introduced capability files that live in src-tauri/capabilities/. Every JSON file in that directory is automatically discovered and loaded. This means you can split permissions across multiple files — one per window, or one per feature group — without a single monolithic permissions list.

Splitting Configuration Across Files

The main configuration file can be split horizontally (platform overrides) and vertically (capabilities, Cargo features, environment variables). The key rule is: a setting should exist in exactly one place, and its provenance should be obvious to anyone reading the codebase.

Platform‑Specific Overrides

Tauri merges tauri.{platform}.conf.json on top of the base configuration. For example, if you need a different window size on macOS, you create tauri.macos.conf.json that contains only the app > windows section. The rest of the configuration remains in tauri.conf.json.

// src-tauri/tauri.conf.json (base)
{
  "productName": "my-app",
  "version": "1.0.0",
  "app": {
    "windows": [
      {
        "title": "My App",
        "width": 800,
        "height": 600
      }
    ]
  }
}
// src-tauri/tauri.macos.conf.json (macOS override)
{
  "app": {
    "windows": [
      {
        "width": 900,
        "height": 700,
        "titleBarStyle": "Overlay"
      }
    ]
  }
}

On macOS the effective width becomes 900. On Windows and Linux the width stays 800. The merge is deep, so you only write the keys you want to change.

Be Careful with Arrays in Merges:

Tauri merges objects recursively but replaces arrays wholesale. If your base config has "windows": [{...}] and your platform override also has "windows": [{...}], the base window definition is completely discarded. You must repeat the entire window object in the override, not just the fields you want to change. Forgetting this leads to missing properties that were present in the base config.

Choosing a File Format

Tauri supports JSON, JSON5, and TOML for the main configuration. The format affects how easy it is to comment, how collisions with Cargo manifest syntax are handled, and how your team edits it. JSON5 and TOML both allow comments; plain JSON does not.

// Comments are not valid in standard JSON.
{
  "productName": "my-app",
  "version": "1.0.0",
  "build": {
    "frontendDist": "../dist"
  },
  "app": {
    "windows": [
      { "title": "My App", "width": 800, "height": 600 }
    ]
  }
}

The choice is mostly about team preference. If you need to share snippets with developers who rarely touch Rust, JSON5 with comments (file name tauri.conf.json or tauri.conf.json5) is often the safest middle ground.

Capability Segmentation

Instead of one default.json with every permission your app might ever need, group capabilities by window or by feature. A settings window should not have filesystem write access if it never writes to disk.

capabilities/
├── main-window.json
├── settings-window.json
└── updater.json
// capabilities/main-window.json
{
  "identifier": "main-window-capability",
  "description": "Permissions for the primary app window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "shell:allow-open"
  ]
}
// capabilities/settings-window.json
{
  "identifier": "settings-window-capability",
  "description": "Permissions for the settings window",
  "windows": ["settings"],
  "permissions": [
    "core:default",
    "fs:scope-app-recursive"
  ]
}

Each capability file references a specific window label. That label must match the window definitions in tauri.conf.json under app > windows. When the window opens, Tauri enforces only the permissions declared in capabilities that list it.

Small Capabilities Are Easier to Audit:

When you split capabilities by window, a security reviewer can look at one file and immediately know what that window is allowed to do. A single monolithic file forces them to trace every permission back to a window label, which is slower and more error‑prone.

Keeping Configuration Maintainable

Configuration deteriorates when settings accumulate without clear ownership. The practices below prevent that.

Always Set the Schema Reference

The $schema key at the top of a capability file gives your editor autocompletion and validation. It is the single cheapest way to catch typos before they become runtime errors.

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

The path assumes the file is inside src-tauri/capabilities/. If you move capability files, update the relative path. If your editor suddenly stops giving hints, the schema path is the first thing to check.

Avoid Redundant Version Sources

Tauri can read the version number from tauri.conf.json, Cargo.toml, or a separate package.json field. Pick one canonical source. Letting them drift apart causes the installer to show one version number while the about dialog shows another.

// tauri.conf.json — single source of truth
{
  "version": "1.2.3"
}

If you must derive the version from the frontend’s package.json, set it explicitly:

{
  "version": "../package.json"
}

Then remove the version field from Cargo.toml (or keep it in sync manually). The Tauri docs recommend managing versioning in the Tauri config and letting the bundler propagate it to platform‑specific manifests.

Use Environment Variables for Environment‑Specific Settings

Values that change between development and production — API endpoints, feature flags, logging levels — should live outside the configuration files. Tauri’s build scripts have access to environment variables, and you can read them in tauri.conf.json using the {{ ENV_VAR }} syntax if you enable the config-json5 feature (which supports variables). For TOML, you would read them in a build script and write them to a generated configuration fragment.

A cleaner pattern is to keep the Tauri configuration static and let your Rust backend read environment variables at startup:

// src-tauri/src/main.rs
fn main() {
    let api_url = std::env::var("API_URL").unwrap_or_else(|_| "http://localhost:3000".into());
    tauri::Builder::default()
        .manage(AppState { api_url })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

This keeps the Tauri config files free of secrets and environment‑specific noise.

Commit Platform‑Specific Config Files Even If They Are Small

A tauri.macos.conf.json that only changes the window title bar style is still worth committing. It documents the deliberate platform divergence. Without it, the next developer has to guess whether the behavior they see on macOS is a bug or an intentional override.

Step‑by‑Step: Setting Up a Well‑Organised Configuration

The following steps take a standard scaffold and reshape it into a maintainable structure. They assume you already have a Tauri project with React and Vite.

1

Step 1: Establish the base configuration with comments

If you are using JSON, switch to JSON5 by enabling the config-json5 feature in Cargo.toml and renaming your file to tauri.conf.json5 (or keep the .json extension). Add a top‑level comment block that explains the purpose of each major section.

// tauri.conf.json5 — main configuration for my-app
{
  // Product metadata (displayed in installers and about dialogs)
  "productName": "my-app",
  "version": "1.0.0",
  // Build settings (paths relative to src-tauri/)
  "build": {
    "frontendDist": "../dist",
    "devUrl": "http://localhost:1420"
  },
  // Application behaviour
  "app": {
    "windows": [
      { "title": "My App", "width": 800, "height": 600 }
    ]
  }
}

This takes two minutes and saves hours of head‑scratching when someone opens the file for the first time in six months.

2

Step 2: Create a dedicated capabilities folder with scoped files

Delete the generic default.json that the scaffold created. Replace it with at least two files: one for the main window and one for any secondary windows you plan to add.

src-tauri/capabilities/
├── main-window.json
└── settings-window.json

Each file references only the permissions its window actually needs. This exercise forces you to think about the principle of least privilege early, when it is cheap.

3

Step 3: Extract platform‑specific overrides into dedicated files

Identify any setting that differs between operating systems — window dimensions, menu styles, file associations. Move those differences into tauri.macos.conf.json, tauri.windows.conf.json, and tauri.linux.conf.json.

// tauri.macos.conf.json — only macOS overrides
{
  "app": {
    "windows": [
      {
        "titleBarStyle": "Overlay",
        "hiddenTitle": true
      }
    ]
  }
}

After the extraction, tauri.conf.json should contain only the settings that are identical across all platforms. It becomes noticeably shorter and easier to scan.

4

Step 4: Move environment‑dependent values out of config files

Identify any hardcoded URLs, secrets, or feature toggles in the configuration. Extract them to environment variables or to a separate Rust‑side config struct loaded at startup.

// src-tauri/src/config.rs
pub struct AppConfig {
    pub api_base_url: String,
}
impl AppConfig {
    pub fn from_env() -> Self {
        Self {
            api_base_url: std::env::var("API_BASE_URL")
                .unwrap_or_else(|_| "https://api.example.com".into()),
        }
    }
}

Do not store secrets in tauri.conf.json. That file is committed to version control and visible in the final bundle.

5

Step 5: Validate the configuration with a dry‑run build

Run npm run tauri build -- --debug and check that no warnings appear. Then inspect the generated bundle to confirm that platform‑specific overrides were applied correctly. On macOS, the .app bundle’s Info.plist should reflect the merged configuration. On Windows, the installer metadata should show the correct product name and version.

If the build succeeds but the app behaves unexpectedly, temporarily add a window that prints tauri.conf.json diagnostics. The tauri::generate_context!() macro embeds the final merged config, and you can log parts of it from your Rust setup code.

Summary

Organising Tauri configuration is about two principles. First, one setting, one source — never duplicate the same value across files without a clear merge strategy. Second, scope permissions to the window that needs them — split capabilities by window label so the security boundary is visible on disk.

If your configuration is scattered across a dozen files and you cannot explain which file controls the window size, the fastest reset is to consolidate everything into tauri.conf.json, verify it works, then incrementally split it back out. That process alone often reveals accidental overrides and dead settings.

Many performance tuning changes — like reducing binary size through LTO settings or optimising the bundle — start as modifications to the files you just organised.