---
title: Build a Tauri app with Bun as its backend
description: Run a Bun server next to a Tauri app with tauri-plugin-aphrody and call it from the webview through IPC.
sidebarTitle: Tauri with Bun
mode: center
---

The Aphrody Tauri fork lives in [`packages/bun-tauri`](https://github.com/aphrody-labs/bun-distribution/tree/os/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`](https://www.npmjs.com/package/@aphrody/tauri) package. The code is licensed under Apache-2.0 OR MIT.

```sh terminal icon="terminal"
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                                                |

```rust src-tauri/src/main.rs icon="rust"
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`:

```json tauri.conf.json icon="file-json"
{
  "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.

```ts server.ts icon="/icons/typescript.svg"
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:

```ts src/main.ts icon="/icons/typescript.svg"
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:

```sh terminal icon="terminal"
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.
