New Fluxzy v2 just shipped. Electron is out, Tauri is in. gRPC ready, 3x smaller install. Learn more

Five months after switching Fluxzy from Electron to Tauri

Quick context. We make Fluxzy, an HTTPS debugging proxy in the Fiddler and Charles family. It runs offline, no account, no cloud. The capture engine is fluxzy.core, an open source .NET MITM library (https://github.com/haga-rak/fluxzy.core) that handles HTTP/1.1, HTTP/2, gRPC, and WebSocket. The desktop app sits on top of that engine and adds the things we actually wanted when debugging traffic: native PCAPNG capture with embedded TLS keys so Wireshark can decrypt the session, JA4 fingerprinting, decoded multipart forms and JWTs, Monaco-based editors for inspecting and tweaking payloads, and a few things aimed at the new mess of LLM API traffic that nobody's tooling really handles yet.

Fluxzy Desktop on Tauri capturing LLM API calls to Anthropic

The desktop app shipped in February 2023 on Electron. In December 2025 we pushed v2, which moved the shell to Tauri. The Angular frontend stayed put, and the .NET sidecar that runs the engine stayed put too.

Five months in. Here's what actually happened.

Why I left Electron in the first place

Electron was a great first run. Genuinely. The docs are excellent, the community is huge, and getting a multi-platform desktop app out the door in 2023 was basically a solved problem because of it. I have nothing bad to say about the people who maintain it. They do hard work.

But a few things piled up.

Bundle size and memory. The Windows installer dropped from around 190mb to 55mb. On macOS and Linux the memory footprint dropped noticeably too, because we're now running on WKWebView and WebKitGTK respectively instead of a bundled Chromium per app. On Windows, WebView2 is shared system-wide so there's no per-app Chromium being copied around. Same story.

I expected visual regressions going from Chromium to three different webviews. Got essentially none. Angular's build pipeline already handles polyfills and prefixing, which did most of the work for me. I am not going to pretend I was clever here. I was lucky that the frontend was already conservative.

Electron has a perception problem. This is uncomfortable to write but it's real. Among developers and network folks, the audience Fluxzy is aimed at, "Electron app" carries weight. Some of it deserved, some not. When a competitor in the proxy space is on Electron and you're not, that becomes part of the pitch whether you like it or not. I was naive about marketing for a long time. I thought good software sold itself. It doesn't. Stack matters to the people you want to reach.

Opening archives needs to be fast. A lot of Fluxzy users open many archive files in a session, debugging across captures. Cold start and file-open latency on Tauri is dramatically better than Electron. I don't have a clean benchmark to drop here so I won't pretend I do, but every user who tried both noticed without being prompted.

The Tauri capability model is genuinely nice. Coming from Electron, where the renderer either has access to everything you wire up or nothing, Tauri's capabilities feel like the right default. Permissions are scoped, declarative, and you opt in to what each window can call. For Fluxzy I never had to write a custom capability. The defaults plus a few standard plugin permissions covered everything: window controls, file system access on the archive paths, dialog, shell open for external links.

The whole surface lives in one JSON file and reads exactly like what it is:

{
  "identifier": "default",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "core:window:allow-set-title",
    "core:window:allow-start-dragging",
    "core:window:allow-minimize",
    "core:window:allow-maximize",
    "core:window:allow-close",
    "shell:allow-open",
    "dialog:default",
    "dialog:allow-open",
    "dialog:allow-save",
    "clipboard-manager:allow-write-text",
    "clipboard-manager:allow-read-text",
    "process:allow-exit",
    "process:allow-restart",
    {
      "identifier": "fs:allow-read-text-file",
      "allow": [{ "path": "**" }]
    }
  ]
}

That is a small thing on paper but it matters when your app is in the security/network space and you actually care about what the frontend is allowed to do. With Electron I had a hand-rolled IPC surface that I had to be paranoid about. With Tauri the framework is paranoid for me.

The shell got cleaner. The architecture was always Angular renderer plus Electron main plus a .NET sidecar doing the proxy work. The Node main process was glue, nothing more. Replacing that glue with Rust trimmed code I never enjoyed maintaining anyway. Rust forces a discipline that Node main processes do not. You do not get to leave a half-typed callback chain lying around for two years.

Pain points, because there are always pain points

IPC. Electron's IPC and Tauri's command/event model are different enough that this could have been a nightmare. It wasn't, because the Electron version already had a clean split between renderer and main, and the heavy work was in the .NET sidecar anyway. If your Electron app has business logic smeared into the renderer, this part will hurt. Mine didn't, so it didn't.

The pattern that replaced most of my Electron ipcMain.handle calls is just #[tauri::command] with strongly typed arguments and return values, plus an Emitter for the async stream coming back from the .NET process:

#[tauri::command]
fn get_backend_state(state: tauri::State<AppState>) -> BackendState {
    state.backend.lock().unwrap().clone()
}

// On the spawn side, we tail fluxzyd's stdout and forward
// a "ready" event the moment the engine is listening.
if line.contains("FLUXZY_LISTENING") {
    let mut backend = state.backend.lock().unwrap();
    backend.ready = true;
    let payload = serde_json::json!({
        "port": backend.port,
        "openFile": backend.open_file.clone(),
    });
    let _ = app_handle.emit("fluxzyd-ready", payload);
}

On the Angular side that turns into invoke('get_backend_state') and listen('fluxzyd-ready', ...). Compared to the Electron preload + contextBridge + ipcRenderer.invoke ceremony, it is much less code and the types survive the round trip. Worth thinking about before you commit, but if your split is already clean you'll probably enjoy this part.

Tauri's dialog plugin does Yes/No. It does Ok/Cancel. It does not do Yes/No/Cancel. The third option is on someone's list somewhere. I needed it for the canonical "save changes before closing?" prompt and I had it in Electron, so the Tauri build couldn't ship without it. Pulled in the rfd Rust crate and exposed a command:

#[tauri::command]
fn show_yes_no_cancel_dialog(title: String, message: String) -> i32 {
    let result = MessageDialog::new()
        .set_title(&title)
        .set_description(&message)
        .set_buttons(MessageButtons::YesNoCancel)
        .show();
    match result {
        MessageDialogResult::Yes => 0,
        MessageDialogResult::No => 1,
        _ => 2,
    }
}

Twenty lines of Rust and one extra crate. Annoying because it's the kind of gap you only notice halfway through porting your existing dialogs, when the third button you wrote in 2023 suddenly has nowhere to land.

Auto-updater discontinuity. This is the one that bit me. Electron's updater and Tauri's updater don't talk to each other (obviously !). I had to ship an intermediate Electron release whose only job was to tell users "go download v2 manually." Months later I still have users sitting on v1 because they never opened the app during the transition window. If you do this migration, plan the bridge release before you start, not after, maybe it's feasible to put an intermediate bridge to make possible the migration from electron to tauri updater.

For the record, the Tauri side itself is fine. The whole updater config is roughly six lines of JSON pointing at an endpoint that returns a signed manifest:

"plugins": {
  "updater": {
    "pubkey": "<minisign public key>",
    "endpoints": [
      "https://updater.fluxzy.io/api/update?target={{target}}&arch={{arch}}"
    ]
  }
}

The pain isn't writing this. The pain is that v1 users on Electron's autoUpdater will never see it.

Code signing on Windows is two-pass. Tauri's updater verifies a minisign signature over the bundle. Authenticode then rewrites the binary to embed its own signature, which invalidates the minisign one. Sign in the wrong order and the .sig no longer matches what users actually download, the updater rejects every release, and your auto-update silently breaks. The fix is obvious once you've stared at it long enough: Authenticode first, then regenerate every .sig with tauri signer after. The Windows build script does it in a Node wrapper whose only job is to make two signing systems take turns:

1. tauri build --target x86_64-pc-windows-msvc
2. dotnet sign (Authenticode on .exe, .msi, .nsis output)
3. regenerateUpdaterSignatures()      // re-sign with TAURI_SIGNING_PRIVATE_KEY
4. zip the standalone bundle

The Tauri docs, last I looked, do not walk through this combination. Worth getting right the first time.

Self-hosted Windows runner, toolchain drift. This one took me longer than I'd like to admit. Cold release builds on the Windows runner started failing with dlltool.exe not found. Nothing in the project had changed. What had changed was that something on the runner had set rustup's default host to the GNU triple between runs, and the next Tauri build tried to link host-side build scripts against binutils that weren't installed. One-line fix:

- run: rustup default 1.92.0-x86_64-pc-windows-msvc

On GitHub-hosted runners you'll never see this. On a long-lived self-hosted runner you will, eventually. Pin the host explicitly.

Wayland on Linux. I'm an Nvidia/Wayland user so I got to live this one personally. WebKitGTK has known issues there. I disable hardware acceleration via env vars and the app doesn't need GPU anyway, so functionally fine. The Linux startup also has to nudge GTK into dark mode and turn off the global menu proxy before the webview spins up, otherwise you get a bright Adwaita header and a phantom Ubuntu app menu glued to the top of the window:

#[cfg(target_os = "linux")]
fn setup_linux_dark_mode() {
    std::env::set_var("GTK_THEME", "Adwaita:dark");
    std::env::set_var("ADW_DISABLE_PORTAL", "1");
    std::env::set_var("UBUNTU_MENUPROXY", "0");
    std::env::set_var("GTK_MODULES", "");
    std::env::set_var("APPMENU_DISPLAY_BOTH", "0");
}

Calling that before tauri::Builder::default() is the difference between a window that looks like the rest of the app and a window that looks like 2014. On GNOME/Wayland I still cannot get the native window decorations to disappear without losing the context menu on the custom header bar, so the Linux build looks slightly different from macOS and Windows. My brain adapted in a week. Yours will too.

The boring upside

Code signing and notarization got faster. The macOS notarization step in particular shaved real minutes off the pipeline because there is much less binary to scan. Windows SmartScreen also seems to clear faster on the smaller, signed installer, though that one is a vibe, not a measurement.

I'm a heavy daily user of my own tool. Five months in, I have no urge to go back.

Why not Avalonia or MAUI

Seems obvious because the sidecar is already in .NET.

Rewriting the Angular frontend was a non-starter. That's many nights of UX work, and the Fluxzy users I respect most are productive in the current UI.

Last but not least, Monaco Editor, which has no equivalent outside the JS world.

Takeaway

Fluxzy had a clean split between the Angular frontend, the Electron main process, and a .NET sidecar doing the real work. That is the easy mode of an Electron-to-Tauri migration. If your Electron app has business logic in the main process, or a frontend that leans on Chromium-specific behavior, your story will be harder than mine.

With that caveat: if your shape looks like ours, Tauri is worth the move. Budget for the auto-updater bridge and slow Rust builds. Don't expect a free lunch on Wayland.

I want to be clear about one thing before signing off. Electron is the reason Fluxzy exists as a desktop app at all. In 2022, when we were choosing how to ship this, Electron was the only option that let a two-people project credibly run on Windows, macOS, and Linux without losing my mind. Two years of users, every bug report, every feature request, every paying customer, all of that happened on Electron. The framework worked. The community helped. To everyone who maintains Electron: thank you, genuinely.

ESC