Multi-Config App Variants
Building multiple Tauri app variants from shared code using overlaid config files
Multi-Config App Variants
A single Tauri codebase can produce multiple app variants — different names, identifiers, and icons — by using overlaid configuration files. The base tauri.conf.json contains the full configuration, and variant configs override only the fields that differ.
The Problem
You have a text editor app called “zudotext”. Now you want to create a second app called “ztoffice” with:
- A different name and icon
- A different macOS bundle identifier
- The same Rust code and frontend
You could copy the entire project, but that means maintaining two copies of everything. Instead, use config overlays.
How Config Overlay Works
Tauri’s --config flag accepts an additional JSON file that is merged on top of the base tauri.conf.json. The overlay only needs to contain the fields you want to override.
# Build the base app
cargo tauri build
# Build the variant app
cargo tauri build --config tauri.conf.ztoffice.json
Real Example
Base Config: tauri.conf.json
The full configuration with all fields:
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "zudotext",
"version": "0.1.0",
"identifier": "com.takazudo.zudotext",
"build": {
"beforeDevCommand": "pnpm exec vite --config vite.config.ts",
"beforeBuildCommand": "pnpm exec vite build --config vite.config.ts",
"devUrl": "http://localhost:37461",
"frontendDist": "./dist-renderer"
},
"app": {
"macOSPrivateApi": true,
"windows": [],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"category": "DeveloperTool",
"macOS": {
"minimumSystemVersion": "10.15"
}
}
}
Variant Config: tauri.conf.ztoffice.json
Only the fields that differ:
{
"productName": "ztoffice",
"identifier": "com.takazudo.ztoffice"
}
That is the entire file — just two fields. When you run cargo tauri build --config tauri.conf.ztoffice.json, Tauri:
- Reads the base
tauri.conf.json - Deep-merges
tauri.conf.ztoffice.jsonon top - Builds with
productName: "ztoffice"andidentifier: "com.takazudo.ztoffice" - Everything else (build commands, bundle config, etc.) comes from the base
The output is:
target/release/bundle/macos/ztoffice.app
What You Can Override
Any field in tauri.conf.json can be overridden in the variant config. Common overrides:
| Field | Purpose |
|---|---|
productName | The app name (displayed in title bar, dock) |
identifier | macOS bundle identifier (must be unique per app) |
bundle.icon | App icon |
build.beforeDevCommand | Different dev command for the variant |
build.frontendDist | Different frontend assets |
Per-Variant Workspace Resolution
The multi-config pattern becomes particularly powerful when your Rust code adapts behavior based on the app name. Consider the project root resolution from the Text Editor App:
// In production, derive app name from the .app bundle path
// /Applications/ztoffice.app/Contents/MacOS/zudotext
// ^^^^^^^^^^ this is the app_name
let app_name = std::env::current_exe()
.ok()
.and_then(|exe| {
exe.ancestors()
.find(|p| p.extension().map(|ext| ext == "app").unwrap_or(false))
.and_then(|app_dir| {
app_dir.file_stem()
.map(|s| s.to_string_lossy().to_string())
})
})
.unwrap_or_else(|| "default".to_string());
This means:
zudotext.appgets workspace at~/Documents/zudo-text/zudotext/ztoffice.appgets workspace at~/Documents/zudo-text/ztoffice/
Each variant has its own isolated workspace, configured via:
~/.config/zudotext/
zudotext/
config.json # workspace for the base app
ztoffice/
config.json # workspace for the variant
💡 Tip
Notice that the config directory uses the binary name (zudotext), while the subdirectory uses the app bundle name. This means all variants of the same binary share a config namespace, which is intentional — they are variants of the same app.
Build Scripts
For convenience, create build scripts for each variant:
#!/bin/bash
# scripts/build-zudotext.sh
set -e
cargo clean -p zudotext
cargo tauri build
killall zudotext 2>/dev/null || true
sleep 1
rm -rf /Applications/zudotext.app
cp -r target/release/bundle/macos/zudotext.app /Applications/
xattr -cr /Applications/zudotext.app
echo "Installed zudotext.app"
#!/bin/bash
# scripts/build-ztoffice.sh
set -e
cargo clean -p zudotext
cargo tauri build --config tauri.conf.ztoffice.json
killall ztoffice 2>/dev/null || true
sleep 1
rm -rf /Applications/ztoffice.app
cp -r target/release/bundle/macos/ztoffice.app /Applications/
xattr -cr /Applications/ztoffice.app
echo "Installed ztoffice.app"
📝 Note
Both scripts use cargo clean -p zudotext (the crate name, not the product name). The crate name does not change between variants — only the product name does.
Dev Mode with Variants
For development, you can also use --config:
cargo tauri dev --config tauri.conf.ztoffice.json
This runs the variant in dev mode with the overridden product name and identifier.
Variant-Specific Frontend Config
If your variants need different frontend behavior, you can pass information from the config through to the frontend. One approach is to use Vite environment variables:
# In tauri.conf.ztoffice.json
{
"productName": "ztoffice",
"identifier": "com.takazudo.ztoffice",
"build": {
"beforeBuildCommand": "VITE_APP_VARIANT=ztoffice pnpm exec vite build --config vite.config.ts"
}
}
Then in your frontend:
const appVariant = import.meta.env.VITE_APP_VARIANT || 'zudotext';
Alternatively, your Rust code can expose the app name via an IPC command, which the frontend queries at startup.
Limitations
- Same Rust binary — all variants compile to the same Rust binary. You cannot have variant-specific Rust code through config alone (use feature flags for that)
- Same bundle resources — unless you override
bundle.iconorfrontendDist, all variants share the same resources - Same Cargo.toml — the crate name in
Cargo.tomldoes not change, only the Tauri product name