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:
| Feature | Plugin | Notes |
|---|---|---|
| (always) | aphrody | Bun 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-state | same name | in 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-portal | sub-features | renamed from the per-plugin features |
mcp-bridge | mcp-bridge | opt-in, requires the CEF runtime |
wrap, wrap-bundle, wrap-debug-bridge | tauri-wrap (no IPC) | opt-in |
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:
{
"plugins": {
"aphrody": {
"bun": { "entry": "server.ts", "hot": true }
}
}
}
| Key | Meaning |
|---|---|
entry | Script run with bun [--hot] <entry>. The bun binary is binary, then BUN_TAURI_BUN, then PATH. |
sidecar | Name of a bun build --compile executable placed next to the app (bundle > externalBin). Takes precedence over entry. |
hot | Pass --hot so Bun.serve hot-reloads in development. |
args, env, cwd | Extra arguments, environment and working directory. |
port | Fixed port. By default the plugin picks a free loopback port. |
readyTimeoutMs | How long to wait for the port to accept connections (default 15000). |
autostart | Start 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.
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:
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.