Development vs Production Builds
Understand the differences between development and production modes in a Tauri v2 app, how each mode changes the environment and behavior, and how to configure your React + Vite frontend for both.
Development Mode in Tauri v2
When you run npm run tauri dev (or cargo tauri dev), Tauri orchestrates a set of steps that give you a fast, interactive coding experience. The goal of development mode is to make every change visible almost instantly, without requiring a full rebuild of the Rust backend on each UI tweak. Contrast this with production via tauri build.
Under the hood, this is what happens:
- Tauri reads your
tauri.conf.jsonand executes the command listed inbuild.beforeDevCommand. For a React + Vite project, that usually starts the Vite development server. - The Rust code is compiled with debug assertions enabled. This is the default for the
devprofile and activates things likecfg!(debug_assertions). - Once the Vite server is ready, Tauri opens a native WebView window. Instead of loading static files, the WebView navigates to
build.devUrl— the URL where Vite serves your app, typicallyhttp://localhost:1420.
The frontend is served from Vite’s development server, so Hot Module Replacement (HMR) works as expected. You edit a React component, save the file, and the change appears in the WebView almost immediately. Rust changes, on the other hand, trigger a recompilation and application restart.
A minimal development configuration looks like this:
{
"build": {
"beforeDevCommand": "pnpm run dev",
"devUrl": "http://localhost:1420",
"frontendDist": "../dist"
}
}
The beforeDevCommand keeps the Vite dev server running as long as Tauri is active. Tauri manages the lifecycle: it starts the command, waits for it to be ready, and kills it when you stop the dev process.
No Dev Server Overhead in the Final App:
The development server and its dependencies never ship with the production build. They exist only to speed up your local development loop.
Production Mode: What Changes
When a user launches your built application — by double‑clicking the .app on macOS, the .exe on Windows, or the binary on Linux — the environment is fundamentally different from your development terminal. Production mode strips away every convenience that was added for development.
Here are the key differences you must account for:
beforeDevCommanddoes not run. There is no dev server started automatically. The WebView loads static files directly from thefrontendDistdirectory, which was populated during the build.- The system PATH is minimal. Finder‑launched apps on macOS, for example, see only
/usr/bin:/bin:/usr/sbin:/sbin. Tools likepnpm, Node.js installed via nvm, or Homebrew binaries simply do not exist in that environment. If your Rust code relies on executing external commands, you cannot assume they will be found. cfg!(debug_assertions)evaluates tofalse. The Rust compiler eliminates all code insideif cfg!(debug_assertions)blocks, so any debug‑only logic vanishes from the binary.- Hot reload stops existing. The WebView loads a snapshot of your built frontend. Any changes to the source code are not reflected until you rebuild and redistribute the application.
The Most Common Production Crash:
If your app works perfectly with tauri dev but shows a blank screen or crashes when launched from Finder, the cause is almost always a missing tool or a command that relied on your development shell’s PATH. Never assume that binaries like node, pnpm, or git are available in the production environment unless you bundle them explicitly.
A typical Tauri build command (tauri build) runs beforeBuildCommand to generate the static frontend assets (e.g., vite build). After that, it compiles the Rust code in release mode and bundles everything together. The resulting application does not contact any dev server; it is entirely self‑contained if configured correctly.
Detecting the Current Mode in Rust
At compile time, Rust already knows whether it is building a development or release binary. You can use that information to branch your code without any runtime cost.
The compile‑time constant cfg!(debug_assertions) returns true only during debug builds. Because it is evaluated at compile time, the compiler will remove the branch that is not taken. This gives you a zero‑cost way to conditionally execute code:
// Prevents additional console window on Windows in release
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
const IS_DEV: bool = cfg!(debug_assertions);
fn main() {
tauri::Builder::default()
.setup(|app| {
if IS_DEV {
// Dev mode: the frontend dev server is already running,
// so we might only need to log a message or skip sidecar spawning.
println!("Running in development mode");
} else {
// Production mode: any external process that the app needs
// (like a local API server) must be started manually here,
// using absolute paths or bundled binaries.
start_production_services(app.handle());
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Think of IS_DEV as a toggle that lets you write two versions of the same startup logic — one for your comfortable terminal, one for the bare‑bones production environment. Because it is a compile‑time constant, you can even use it to conditionally compile entire modules or change the behavior of commands.
Configuration That Differs Between Modes
The build section of tauri.conf.json contains several fields whose roles change depending on whether you are developing or producing a final build.
| Field | Role in Development (tauri dev) | Role in Production (tauri build/runtime) |
|---|---|---|
beforeDevCommand | Runs automatically, starts the Vite dev server | Does not run; has no effect at runtime |
devUrl | WebView loads from this URL (your live dev server) | Ignored; WebView loads from frontendDist instead |
frontendDist | Not used; UI is served by the dev server | Directory that holds the built static files |
beforeBuildCommand | Not used during development | Runs once before bundling, typically executes the frontend build (e.g., vite build) |
A practical consequence of this split is that your beforeBuildCommand must be correct and produce a complete static build, because that is the only version your users will ever see. If your dev server works but vite build fails, the packaged application will be broken.
Build Workflow from Development to Release
Moving from a development prototype to a shippable application is a sequence of steps where each depends on the previous one. The following procedure ensures you catch production‑only issues early.
Step 1: Develop in dev mode
Run npm run tauri dev during active development. This gives you hot reload on frontend changes and debug assertions on the Rust side. Use this mode for all feature work and UI iteration.
Step 2: Verify the frontend build independently
Before relying on Tauri’s build pipeline, run your frontend build command directly (e.g., pnpm run build) and confirm it completes without errors. If your Vite build fails or produces warnings about missing assets, fix them now. The production WebView will load exactly this output.
Step 3: Run a release build
Execute npm run tauri build. Tauri will first run beforeBuildCommand, then compile the Rust backend with optimizations and debug assertions disabled, and finally bundle everything into a platform‑specific application (.dmg, .msi, .AppImage, etc.). This step validates that the whole pipeline works end to end.
Step 4: Test the production artifact
Install or launch the generated application bundle as a real user would — from Finder, the Start Menu, or the file manager. Do not test by running cargo run or a development binary. This is the moment you’ll discover PATH issues, missing sidecars, or broken relative paths.
Step 5: Debug and repeat
If the production app fails, return to Step 1 with a better understanding of what the production environment lacks. Add conditional logic using cfg!(debug_assertions), bundle required binaries as sidecars, or adjust your file‑loading strategy. Repeat until the production app behaves identically to the dev experience (except for intentional debug‑only features).
A Healthy Production Build:
If you can launch the final .app or .exe from Finder/Explorer and see your entire UI load correctly — without any terminal window spitting out errors — your production configuration is solid.
Common Pitfalls When Testing Production Builds
Moving from tauri dev to a packaged application catches many developers off guard. Here are the mistakes that surface most often and how to avoid them.
- Assuming
beforeDevCommandruns in production. The dev server is a development convenience only. If your app needs a server process at runtime in production (for example, a sidecar Express server), you must spawn it explicitly from your Rust code using an absolute path or a bundled binary. - Relying on tools in the shell PATH. Production apps launched from the desktop inherit a tiny PATH. Commands like
node,pnpm,python, orgitwill not be found unless you install them globally and reference absolute paths, or bundle them as Tauri sidecars. Hard‑code fallback paths like/opt/homebrew/bin/pnpmif you must rely on system‑installed tools, but bundling is the safer choice. - Incorrect or missing
frontendDist. IfbeforeBuildCommandfails silently or outputs files to the wrong directory, the WebView will load a blank page or stale content. Always verify thatfrontendDistcontainsindex.htmland all your static assets after running the build command manually. - Leaving debug‑only features active. Logging statements, mock data, or developer tools exposed in the UI should be gated behind
cfg!(debug_assertions)or a compile‑time flag so they disappear from the release binary. Accidentally shipping a debug panel can be a security concern. - Mishandling the WebView origin. In dev mode, the origin is typically
http://localhost:1420. In production, the origin is something likehttps://tauri.localhost(on some platforms) or a custom scheme. If you performfetchrequests or use absolute URLs, ensure they work under the production origin. Use Tauri’s HTTP API or a Rust proxy to avoid CORS issues that don’t exist during dev.
Test Production Early and Often:
The quickest way to uncover these issues is to do a production build within the first few days of development and test it. Waiting until release week to discover that your app needs a bundled Node.js binary is a painful time sink.
The Development Lifecycle
A Tauri project follows a loop that oscillates between the fast‑iteration world of development and the realistic, hardened world of production builds.
Active development: You run tauri dev, make changes to React components or Rust commands, and see results immediately. The dev server, debug assertions, and console output make this phase productive. You should periodically run the frontend build (vite build) to catch any build‑time errors that HMR might hide.
Intermittent production checks: Every few features, perform a full tauri build and launch the resulting artifact. This step confirms that your production configuration, static assets, and Rust logic all hold up outside the development bubble. Catch PATH problems, missing assets, and configuration drift here rather than at release time.
Pre‑release hardening: When you are feature‑complete, freeze the code and do a final production build. Test on clean machines (or fresh virtual machines) that have no development tooling installed. This simulates exactly what a new user will experience. Fix any last‑minute production‑only bugs and then build the final distributable.
The entire cycle reduces to a simple rule: develop in dev mode, verify in production mode, and never confuse the two environments. Keeping the separation clear in your mind (and in your tauri.conf.json) prevents the majority of Tauri‑specific bugs.
Summary
The distinction between development and production modes in Tauri is not just about debug versus release builds — it’s about the entire environment surrounding your application. Development mode wraps your app in a comfortable shell with a live dev server, full PATH access, and debug tooling. Production mode strips all of that away and runs your app in a minimal, locked‑down environment that mirrors what users actually get.
The single most impactful habit you can adopt is testing the production build regularly, not just at the end. A tauri build and a double‑click from Finder or Explorer tells you more about the health of your application than a thousand tauri dev sessions.