# Build a Tauri app with Bun as its runtime

`packages/bun-tauri` holds the aphrody fork of Tauri 3 (alpha) as its own Cargo workspace: the core crates (`aphrody-tauri`, `-runtime`, `-runtime-cef`, `-utils`, `-plugin`, `-build`, `-codegen`, `-macros`, `-cli`, `-bundler`) and one plugin crate, `tauri-plugin-aphrody`. That crate contains every plugin of the fork plus the MCP debug bridge and `tauri-wrap`. Its JavaScript side is `@aphrody/tauri`.

The code comes from `aphrody-labs/aphrody@55652e3f56` (`crates/tauri`, `crates/ui/tauri-plugin-mcp-bridge`, `crates/ui/tauri-wrap`), under Apache-2.0 OR MIT. The MCP bridge is MIT (hypothesi).

---

## One crate, one plugin per module

Each module 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 plugins. A Cargo feature with the same name enables each module:

| Feature | Plugin | Notes |
| --- | --- | --- |
| (always) | `aphrody` | Bun runtime: `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, pulls `tauri-runtime-cef` |
| `wrap`, `wrap-bundle`, `wrap-debug-bridge` | `tauri-wrap` (no IPC) | opt-in |

A single crate can publish permissions for several plugins because of a change in `tauri-utils` and `tauri-plugin`. `tauri_plugin::Bundle` builds every member: it writes `permissions/<name>/`, the autogenerated command permissions, the scope schema and the global API script. Each member's permission list is announced as `cargo:__BUNDLE__<NAME>_PERMISSION_FILES_PATH`. The app's `tauri-build` maps that key back to the plugin `<name>` (`tauri_utils::acl::build::plugin_name_from_dep_var`). `generate_handler![#![plugin(<name>)] …]` makes sure unused-command removal targets the right plugin.

```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 and platforms

Tauri 3 has no default runtime. Pass one with `Builder::runtime(...)`.

- **CEF** (`aphrody-tauri-runtime-cef`, CEF 152.3.0) is the desktop runtime on Windows and Linux with glibc. On Linux it forces X11/XWayland. On Windows Chromium runs without a sandbox, whatever the policy.
- **Alpine (musl)**: CEF does not ship musl builds. A window on Alpine depends on the WebKitGTK runtime (`aphrody-tauri-runtime-wry`), which comes from the shared webview layer (`packages/bun-webview-core`). The runtime crates belong to that layer, not to this plugin.
- **Mobile**: the Android and iOS projects of each plugin are kept under `crates/tauri-plugin-aphrody/mobile/<name>/`. They are not wired into the bundle yet.

---

## Test

```sh terminal icon="terminal"
cd packages/bun-tauri
cargo test -p tauri-plugin-aphrody
```

`tests/e2e.rs` builds a `MockRuntime` app for each module and resolves its ACL from the permission files the build script generated. It then calls the commands through real IPC, as a webview would. That covers `os`, `fs` (inside and outside its scope), `store`, `log`, `window-state`, `notification` and `deep-link`. It also starts a real Bun server through `aphrody` 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.
