Configuration Best Practices
Practical guidelines for organizing, securing, and optimizing your Tauri v2 configuration files to build maintainable and high-performance desktop applications
The Tauri configuration system gives you precise control over your app’s behavior, build process, security posture, and final bundle. Because the configuration touches everything from the window title to the Content Security Policy, small mistakes can cause silent failures, security holes, or bloated binaries. This page collects the patterns that keep your configuration organized, safe, and fast across development and production.
Organizing Configuration
A tauri.conf.json file that grows organically over months becomes hard to audit. Keeping the file structured from day one saves time when you later need to add a second window, switch to a new bundler target, or debug a permission error.
Keep one source of truth for the version
Your app version appears in tauri.conf.json (version), in Cargo.toml (package.version), and possibly in your frontend’s package.json. Letting them drift apart creates confusion during release management.
The cleanest approach is to set the version in tauri.conf.json and remove it from Cargo.toml. Tauri will fall back to the Cargo version if the config version is missing, but the config is the recommended single source.
// src-tauri/tauri.conf.json (excerpt)
{
"productName": "my-app",
"version": "1.2.0"
}
# src-tauri/Cargo.toml (excerpt)
[package]
name = "my-app"
# version removed – Tauri reads it from tauri.conf.json
Semver in the version field:
The version string can be a semver number or a path to a package.json file that contains a version field. Using the path is handy when your frontend already owns the version, but the direct value is usually simpler to audit.
Split platform-specific overrides into separate files
When your app needs slightly different window sizes on Linux, extra permissions on macOS, or a different installer configuration on Windows, avoid cluttering the main config with conditional blocks. Tauri reads platform-specific files and merges them with the base configuration automatically.
Supported platform files (alongside tauri.conf.json):
tauri.linux.conf.jsontauri.windows.conf.jsontauri.macos.conf.jsontauri.android.conf.jsontauri.ios.conf.json
These files only need to contain the keys that differ from the base. Tauri performs a deep merge, so you do not repeat the entire configuration.
Example: a macOS-specific transparency override that does not affect other platforms.
// src-tauri/tauri.macos.conf.json
{
"app": {
"windows": [
{
"transparent": true,
"titleBarStyle": "Overlay"
}
]
}
}
The base tauri.conf.json keeps the shared window settings — title, default size, resizable — and the platform file adds only what macOS needs.
Platform-specific files reduce merge conflicts:
When multiple developers work on different platforms, keeping platform quirks in dedicated files prevents accidental cross-platform breakage and makes code review safer.
Manage permissions with capability files, not inline everything
Tauri v2 uses a capability-based security model. Each plugin and each custom command must be explicitly granted through capability files inside src-tauri/capabilities/. A common mistake is dumping every permission into one monolithic file.
Instead, group capabilities by purpose:
desktop-default.json— permissions that every window needs (core window APIs, event system)file-access.json— read/write paths for a specific feature (e.g., saving user documents)updater.json— permissions for the auto-updater plugin
This makes it obvious which features depend on which permissions, and it simplifies security audits when you later decide to remove a feature.
// src-tauri/capabilities/desktop-default.json
{
"identifier": "desktop-default",
"description": "Default capabilities for all windows",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open"
]
}
// src-tauri/capabilities/file-access.json
{
"identifier": "file-access",
"description": "Read and write user documents",
"windows": ["main"],
"permissions": [
"fs:allow-read-text-file",
"fs:allow-write-text-file"
]
}
Unused permissions are an attack surface:
Every permission you grant is a door you leave open. If a plugin is no longer used, delete its capability file entirely. A future refactor that inadvertently calls a still-granted API is a vulnerability that static analysis will not catch.
Keep secrets out of the config file
The tauri.conf.json is bundled with your application and distributed to every user. Any value placed there — an API key, a client secret, a license token — is trivially extractable. Use environment variables for compile-time secrets and the OS keyring for runtime secrets.
At build time, you can inject values through your frontend’s environment variable system (Vite’s import.meta.env) and keep the sensitive values in a .env file that never enters version control.
Performance Recommendations
A desktop app’s perceived speed is shaped by how quickly it starts, how small the download is, and whether the UI thread stays responsive. The Tauri config directly influences all three. See Performance Recommendations for the full checklist.
Set the correct frontendDist for production
During development, Tauri connects to your Vite dev server via the devUrl. For a production build, it reads static files from the path set in build.frontendDist. Forgetting to set this correctly — or leaving it pointing to a development output — results in a blank window or a build failure.
For a Vite + React project, the default output directory is ../dist relative to src-tauri. The config should reflect this:
// src-tauri/tauri.conf.json (excerpt)
{
"build": {
"devUrl": "http://localhost:5173",
"frontendDist": "../dist"
}
}
The frontendDist path is relative to the src-tauri directory. Double-check that your frontend build script (vite build) actually writes to that location.
Missing frontendDist causes a blank screen:
If frontendDist points to a nonexistent or empty directory, tauri build may succeed without warning, but the resulting app will show an empty window. Always test a production build locally before distributing.
Remove unused commands from the Rust binary
Every Tauri command you define — through #[tauri::command] — adds code to the final binary. If you register a command that never gets called from the frontend, the dead code may still survive the linker. Tauri offers a removeUnusedCommands option that prunes unreferenced commands automatically.
{
"build": {
"removeUnusedCommands": true
}
}
This setting is false by default for backward compatibility. Enable it in any new project. The pruning relies on static analysis of your frontend’s invoke calls, so if you dynamically construct command names, the setting might remove commands you actually use — test thoroughly.
Bundle the Visual C++ runtime statically on Windows
By default, Tauri links the Visual C++ runtime dynamically, which requires the MSVC redistributable to be installed on the user’s machine. For a smoother Windows installation experience, enable the static runtime option.
{
"build": {
"windows": {
"staticVCRuntime": true
}
}
}
The trade-off is a slightly larger binary (a few hundred kilobytes) in exchange for eliminating a common installation support ticket. For most applications, the trade-off is worth it.
Keep resources and sidecars lean
Resources you add through the bundle.resources array and external binaries you define as sidecars all increase the final bundle size. Before adding a resource, ask whether it could instead be fetched on first launch or generated at runtime. Large assets (videos, machine learning models, offline maps) are better downloaded post-install than shipped inside the installer.
Similarly, use bundle.targets to build only the formats you actually distribute. Building all targets (“all”) is convenient during development but wasteful in CI.
{
"bundle": {
"targets": ["deb", "appimage"]
}
}
Enable only the plugins you need
Every Tauri plugin you add pulls in Rust crates, builds native code, and increases compile times. Before adding a plugin, check whether the functionality can be achieved with the core APIs or a lighter dependency. After removing a feature, delete the plugin from Cargo.toml, tauri.conf.json (under plugins), and its capability file.
Security Recommendations
A desktop application runs with the user’s full operating system permissions. The Tauri configuration is your primary tool for restricting what the webview — and therefore any JavaScript running inside it — can access. The security recommendations page expands each of these rules.
Enforce a strict Content Security Policy
The Content Security Policy (CSP) limits where the webview can load scripts, styles, images, and connections from. An absent or overly permissive CSP is the most common security misconfiguration in Tauri apps.
For a production app that does not load external resources, set a CSP that allows only the local assets and the Tauri IPC protocol:
{
"app": {
"security": {
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ipc: http://ipc.localhost"
}
}
}
connect-src must include ipc: http://ipc.localhost to allow the frontend to communicate with the Rust backend. If you load images or fonts from a CDN, add the domains explicitly — never use wildcards.
'unsafe-inline' in style-src is often unavoidable:
Many CSS-in-JS libraries and UI frameworks inject inline styles at runtime. Removing 'unsafe-inline' from style-src may break your UI. If you must keep it, ensure that all other directives are as restrictive as possible.
Do not disable CSP modification unless you have a specific reason
Tauri normally injects its own CSP rules to ensure the IPC layer functions correctly. The setting dangerousDisableAssetCspModification prevents this injection.
{
"app": {
"security": {
"dangerousDisableAssetCspModification": false
}
}
}
Set this to true only if you fully understand the CSP implications and are prepared to manually include every directive Tauri requires. In almost every case, leave it at the default false.
Scope the asset protocol tightly
The asset protocol allows the webview to load local files from specific directories. By default it is disabled. If you enable it, define the smallest possible scope.
{
"app": {
"security": {
"assetProtocol": {
"enable": true,
"scope": ["$APPDATA/user-files/*"]
}
}
}
}
Scoping to $APPDATA/user-files/* means only files inside that specific subdirectory are accessible. A scope of ** would expose the entire filesystem to any script running in the webview — treat that as equivalent to disabling security entirely.
Freeze the prototype chain
JavaScript allows modifying built-in object prototypes at runtime — a technique used by some malicious scripts to intercept method calls. Tauri can freeze Object.prototype and other built-ins at startup to prevent this.
{
"app": {
"security": {
"freezePrototype": true
}
}
}
This is safe to enable unless your frontend or a dependency relies on prototype patching (rare in modern frameworks). It adds a layer of defense with no runtime cost.
Disable withGlobalTauri unless you explicitly need it
When withGlobalTauri is true, Tauri exposes its API as window.__TAURI__ globally — even in production. This makes the IPC surface available to any script that runs in the webview, including third-party scripts loaded from a CDN or an injected advertisement.
{
"app": {
"withGlobalTauri": false
}
}
Instead, import Tauri APIs from the @tauri-apps/api package using ES modules. This keeps the IPC surface scoped to your own code.
Global Tauri in production is a security risk:
If a compromised dependency or a cross-site scripting vulnerability can execute JavaScript in your webview, withGlobalTauri: true hands it direct access to the filesystem, shell, and any other APIs you have granted. This is the single highest-impact config switch you can get wrong.
Common Configuration Mistakes
Even experienced developers trip over these. Each one has caused at least one real-world production issue. The common mistakes page lists the same traps with more detail.
Forgetting to update the identifier for a new project
The identifier field must be a unique reverse-domain string (e.g., com.yourcompany.yourapp). Leaving it as the default com.tauri.dev or copying it from another project causes conflicts — two apps with the same identifier share the same webview data directory and bundle ID, leading to data corruption and installation collisions on macOS and Linux.
{
"identifier": "com.yourcompany.yourapp"
}
Change this before the first build. Changing it later requires migrating user data manually.
Leaving devUrl set in production config
If your tauri.conf.json still points build.devUrl to http://localhost:5173 but you run tauri build, Tauri will ignore it and use frontendDist. The build succeeds, but developers who later test a debug build on a machine without a dev server running see a connection-refused error. Use environment-specific config files or CI overrides to keep development-only settings out of the main config.
Misunderstanding platform-specific overrides
A platform-specific file like tauri.macos.conf.json merges with the base config, not replaces it. Adding a windows array in the platform file does not remove the windows from the base config — it adds new entries. If you want different window configurations per platform, define all windows in the platform files and remove them from the base tauri.conf.json, or use the label field to merge intelligently.
Not configuring permissions for plugins
Adding a plugin to Cargo.toml and tauri.conf.json does not grant its permissions. Every plugin also needs at least one capability entry. A missing capability manifests as a runtime error like Permission denied that does not appear during compilation. Always check that each plugin’s required permissions are present in at least one capability file.
Setting the CSP too late
Developers sometimes add a strict CSP only when preparing for production, only to discover that their app breaks because a dependency loads a script from an unpredicted source. Define a strict CSP early in development and use the browser developer tools (via tauri dev) to catch violations immediately. Adding a domain later is trivial; discovering missing domains the night before a release is stressful.
Enabling the asset protocol with a wildcard scope
A scope of ** or / grants every script in the webview full filesystem read access. This occasionally appears in tutorials as a quick way to make file loading work, but it should never appear in a shipped application. Always scope to the narrowest directory your feature actually needs.
Summary
Configuration best practices in Tauri come down to three principles: keep the structure maintainable so you can reason about what each setting does, restrict every permission and protocol to the minimum the app actually requires, and remove everything you do not use — commands, plugins, build targets, and global API exposure. The default values are safe and sensible; every change away from them should be intentional and documented.
Organizing Configuration
Learn how to structure, split, and maintain your Tauri application configuration files for long-term maintainability.
Performance Recommendations
Configuration practices to reduce bundle size, improve startup time, and optimize binary performance in Tauri v2 with React and Vite
Security Recommendations
Best practices for hardening your Tauri v2 application configuration to protect users and data
Common Configuration Mistakes
How to identify and fix frequent configuration errors in Tauri v2 projects using React and Vite