Skip to content
NewBun 1.4.5 is out. Release notes
AphrodyBun

Guides · Runtime & Debugging

Build a Tauri app with Bun as its backend

Run a Bun server next to a Tauri app with tauri-plugin-aphrody and call it from the webview through IPC.

The Aphrody Tauri fork lives in packages/bun-tauri of the aphrody-labs/bun-distribution repository. It is a separate Cargo workspace with the core crates (aphrody-tauri, aphrody-tauri-runtime, aphrody-tauri-runtime-cef, aphrody-tauri-utils, aphrody-tauri-plugin, aphrody-tauri-build, aphrody-tauri-codegen, aphrody-tauri-macros, aphrody-tauri-cli, aphrody-tauri-bundler) and one plugin crate, tauri-plugin-aphrody. Its JavaScript side is the @aphrody/tauri package. The code is licensed under Apache-2.0 OR MIT.

bun add @aphrody/tauri

One crate, one plugin per module

Each module of tauri-plugin-aphrody is still its own Tauri plugin. Command names (plugin:fs|read_file) and permission identifiers (fs:allow-read-file) are the same as in the separate upstream plugins. A Cargo feature with the same name enables each module:

FeaturePluginNotes
(always)aphrodyBun backend: bun_info, bun_request, bun_restart
autostart, clipboard-manager, deep-link, dialog, fs, global-shortcut, log, notification, opener, os, process, single-instance, store, updater, window-statesame namein default
fs-watch, log-colored, log-tracing, notification-windows7-compat, single-instance-semver, single-instance-deep-link, updater-zip, updater-rustls-tls, updater-native-tls, updater-system-proxy, dialog-xdg-portalsub-featuresrenamed from the per-plugin features
mcp-bridgemcp-bridgeopt-in, requires the CEF runtime
wrap, wrap-bundle, wrap-debug-bridgetauri-wrap (no IPC)opt-in
src-tauri/src/main.rs
use tauri_plugin_aphrody::BuilderExt;

fn main() {
    tauri::Builder::default()
        // the `aphrody` plugin + every enabled module that takes no argument
        .aphrody_plugins()
        .plugin(tauri_plugin_aphrody::single_instance::init(|_app, _argv, _cwd| {}))
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

aphrody_plugins() does not register single-instance (it needs a callback), updater (it needs plugins > updater with a public key), mcp-bridge or wrap.


Bun as the backend

The aphrody plugin starts a Bun server next to the app and stops it when the app exits. Configure it under plugins > aphrody > bun:

tauri.conf.json
{
  "plugins": {
    "aphrody": {
      "bun": { "entry": "server.ts", "hot": true }
    }
  }
}
KeyMeaning
entryScript run with bun [--hot] <entry>. The bun binary is binary, then BUN_TAURI_BUN, then PATH.
sidecarName of a bun build --compile executable placed next to the app (bundle > externalBin). Takes precedence over entry.
hotPass --hot so Bun.serve hot-reloads in development.
args, env, cwdExtra arguments, environment and working directory.
portFixed port. By default the plugin picks a free loopback port.
readyTimeoutMsHow long to wait for the port to accept connections (default 15000).
autostartStart with the app (default true). Otherwise it starts on the first bun_restart.

The server receives its port in PORT, which Bun.serve uses by default, and in BUN_TAURI_PORT. It also gets BUN_TAURI=1. The plugin waits until the port accepts connections. Each output line goes to the log crate and is emitted as an aphrody://bun-log event.

server.ts
import { serveForTauri } from "@aphrody/tauri/server";

serveForTauri({
  routes: { "/api/hello": () => Response.json({ runtime: `bun ${Bun.version}` }) },
});

In the webview, bunRequest and bunFetch go through IPC, so you need no CORS headers and no CSP entry for the port:

src/main.ts
import { bunFetch, bunInfo, onBunLog } from "@aphrody/tauri/bun";

const res = await bunFetch("/api/hello");
console.log(await res.json(), await bunInfo());
await onBunLog(({ stream, line }) => console.log(stream, line));

The default permission aphrody:default grants bun_info and bun_request. Restarting the server needs aphrody:allow-bun-restart. When withGlobalTauri is on, the same calls are available as window.__TAURI__.aphrody.

For a release build, compile the server with bun build --compile server.ts --outfile binaries/server-<target-triple>. List it under bundle > externalBin and set "sidecar": "server".


Runtimes

The aphrody-tauri crate enables no runtime by default; pass one with Builder::runtime(...). The desktop runtime is aphrody-tauri-runtime-cef, built on CEF. On Linux it uses X11 (XWayland under Wayland). CEF does not ship musl builds, so it does not run on Alpine.


Test

From packages/bun-tauri in the repository:

cargo test -p tauri-plugin-aphrody

tests/e2e.rs builds a mock-runtime app for each module, resolves its permissions from the generated permission files and calls the commands through IPC, as a webview would. It also starts a Bun server through the aphrody plugin and calls bun_request and bun_restart. dialog, clipboard-manager, global-shortcut and opener need a desktop session, so this suite does not run them.