zudo-tauri

Type to search...

to open search from anywhere

Multi-Config App Variants

CreatedMar 29, 2026UpdatedMar 29, 2026Takeshi Takatsudo

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:

  1. Reads the base tauri.conf.json
  2. Deep-merges tauri.conf.ztoffice.json on top
  3. Builds with productName: "ztoffice" and identifier: "com.takazudo.ztoffice"
  4. 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:

FieldPurpose
productNameThe app name (displayed in title bar, dock)
identifiermacOS bundle identifier (must be unique per app)
bundle.iconApp icon
build.beforeDevCommandDifferent dev command for the variant
build.frontendDistDifferent 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.app gets workspace at ~/Documents/zudo-text/zudotext/
  • ztoffice.app gets 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.icon or frontendDist, all variants share the same resources
  • Same Cargo.toml — the crate name in Cargo.toml does not change, only the Tauri product name