A lightweight desktop app framework for building native applications with TypeScript, HTML, and CSS. Powered by Bun.
Butter gives you a native window with a webview and a direct IPC bridge between your TypeScript backend and your frontend — no bundled browser engine, no background servers, and a single-file binary output. Write native C, Moxy, Rust, or Zig extensions and call them directly from TypeScript.
| Electron | Tauri | Butter | |
|---|---|---|---|
| Runtime | Chromium (~150MB) | System webview | System webview |
| Backend | Node.js | Rust | Bun (TypeScript) |
| Native extensions | N/A | Rust | C / Moxy / Rust / Zig |
| Binary size | ~200MB | ~5MB | ~60MB |
| IPC | JSON over IPC pipe | JSON commands | Shared memory ring buffer |
| Language | JS/TS | Rust + JS/TS | TypeScript + C/Moxy/Rust/Zig |
| Build tool | webpack/vite | Cargo | Bun |
| Installers | Squirrel/electron-builder | Tauri bundler | butter package (DMG/AppImage/NSIS) |
| Single-instance | API | plugin | singleinstance plugin |
| SQLite | userland | sql plugin |
database plugin (bun:sqlite) |
| Persistent KV | userland | store plugin |
store plugin |
| Sidecar binaries | userland | yes | bundle.sidecars + sidecar plugin |
| Translucent window | API | window effects | window.material (vibrancy / mica / acrylic) |
| Auto-launch at login | API | plugin | autolaunch plugin |
| Power / idle / screen events | API | plugin | power plugin |
| Capability permissions | — | yes (v2) | security.capabilities[] |
Butter's sweet spot: you want native desktop apps with TypeScript on both sides, native performance where you need it via C/Moxy/Rust/Zig, and zero configuration.
Requires Bun v1.2+.
Install via Bun (recommended):
bun add -g butterframeworkInstall via curl:
curl -fsSL https://raw.githubusercontent.com/wess/butter/main/scripts/install.sh | bashInstall via Homebrew:
brew tap wess/packages
brew install butterVerify installation:
butter doctor# Create a new project
butter init myapp
cd myapp
bun install
# Start development (opens a native window)
bun run dev
# Build a single binary
bun run build
# Create an .app bundle (macOS)
butter bundleTemplates available: vanilla (default), react, svelte, vue
butter init myapp --template reactButter runs two processes:
+--------------------------+ +--------------------------+
| Bun Process (parent) | | Native Shim (child) |
| | | |
| Your TypeScript host |<--->| Native window |
| code runs here | IPC | WKWebView (macOS) |
| | | WebKitGTK (Linux) |
| import { on } from | | WebView2 (Windows) |
| "butter" | | |
| | | Your HTML/CSS/JS |
| Native modules via FFI: | | runs here |
| C / Moxy / Rust / Zig | | |
+--------------------------+ +--------------------------+
Shared Memory Ring Buffer
- No web server — assets served via
butter://custom protocol - No bundled browser — uses the OS native webview
- Shared memory IPC — fast communication via ring buffers
- Native extensions — write C, Moxy, Rust, or Zig; auto-compiled and bound via FFI
- Single binary —
butter compileproduces one executable
myapp/
src/
app/
index.html # Entry point (loaded in webview)
main.ts # Frontend TypeScript
styles.css # Styles
host/
index.ts # Backend TypeScript (runs in Bun)
menu.ts # Native menu definition (optional)
native/ # Native extensions, optional. Drop in any of:
math.mxy # .mxy — Moxy
hash.c # .c — C
fib.zig # .zig — Zig
mathrs.rs # .rs — single-file Rust
hashlib/ # <dir>/Cargo.toml — multi-file Rust project
env.d.ts # Type declarations for webview globals
butter.yaml # Configuration
package.json
butter.yaml:
window:
title: My App
width: 800
height: 600
icon: assets/icon.png # optional
material: vibrancy # optional: vibrancy | mica | acrylic | tabbed | none
build:
entry: src/app/index.html
host: src/host/index.ts
bundle:
identifier: com.example.myapp
category: public.app-category.utilities
urlSchemes:
- myapp
sidecars: # optional: external binaries shipped with the app
- bin/ffmpeg
- bin/yt-dlp
security:
csp: "default-src 'self' butter:"
# Flat allowlist (back-compat). Patterns: exact match or `prefix:*`.
allowlist:
- "dialog:*"
- "greet"
# Capabilities — groups of actions granted as a unit. Either form (or both)
# works; the union grants access.
capabilities:
- name: filesystem
actions: ["fs:*", "dialog:open"]
- name: storage
actions: ["store:*", "db:*"]
splash: src/app/splash.html
plugins:
- dialog
- singleinstance
- autolaunch
- database
- store
- power
- sidecarYour backend code in src/host/index.ts:
import { on, send, getWindow, setWindow } from "butter"
// Handle calls from the webview
on("greet", (name: string) => {
return `Hello, ${name}!`
})
// Async handlers work too
on("fetch:data", async (url: string) => {
const res = await fetch(url)
return await res.json()
})
// Push events to the webview
send("status:updated", { ready: true })
// Window control
setWindow({ title: "New Title" })
const { width, height } = getWindow()
// Window events
on("window:resize", (data: { width: number; height: number }) => {
console.log("Window resized to", data.width, data.height)
})
on("window:focus", () => console.log("Window focused"))
on("window:blur", () => console.log("Window blurred"))Your frontend code in src/app/main.ts:
// Call host handlers
const greeting = await butter.invoke("greet", "World")
// With timeout (rejects if no response within 5 seconds)
const data = await butter.invoke("fetch:data", url, { timeout: 5000 })
// Stream large results with progress
await butter.stream("process:file", filePath, (chunk) => {
console.log("Progress:", chunk)
})
// Listen for events from the host
butter.on("status:updated", (data) => {
console.log(data.ready)
})
// Stop listening
butter.off("status:updated", handler)
// Native context menu
const action = await butter.contextMenu([
{ label: "Copy", action: "copy" },
{ separator: true },
{ label: "Delete", action: "delete" },
])
// Persistent KV store (per-app, JSON-backed)
const settings = butter.store("settings")
await settings.set("theme", "dark")
const theme = await settings.get("theme")
// Embedded SQLite database
const db = await butter.db.open("app")
await db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
await db.exec("INSERT INTO notes (body) VALUES (?)", ["hello"])
const rows = await db.query("SELECT * FROM notes")
// Auto-launch at login
await butter.autoLaunch.enable()
const enabled = await butter.autoLaunch.isEnabled()
await butter.autoLaunch.disable()
// Single-instance — second launches fire this on the leader
butter.singleInstance.onSecondInstance((info) => {
console.log("another launch:", info.argv, info.cwd)
})
// Power / display events
butter.power.onSleep(() => console.log("system going to sleep"))
butter.power.onLock(() => console.log("screen locked"))
const idle = await butter.power.idleSeconds() // seconds since last input
const screens = await butter.screen.list() // [{ id, primary, scale, bounds, workArea }]
// Sidecar binaries (declared in butter.yaml under bundle.sidecars)
const ffmpeg = await butter.sidecar.spawn("ffmpeg", { args: ["-version"] })
ffmpeg.onStdout((d) => console.log(d.data))
ffmpeg.onExit((d) => console.log("exit", d.code))The butter global is automatically injected into the webview. TypeScript types are provided via src/env.d.ts.
Write performance-critical code in C, Moxy, Rust, or Zig and call it directly from TypeScript. Drop a source file (or a Cargo project) into src/native/ and Butter auto-compiles it and generates FFI bindings — everything crosses the C ABI so the resulting binding is identical from the caller's side.
| Layout | Build |
|---|---|
src/native/foo.c |
clang / cc / cl.exe |
src/native/foo.mxy |
moxy → C → compiler |
src/native/foo.rs |
rustc --crate-type cdylib --edition 2021 -C opt-level=3 |
src/native/foo.zig |
zig build-lib -dynamic -OReleaseFast |
src/native/foo/Cargo.toml |
cargo build --release (must set crate-type = ["cdylib"]) |
Moxy (src/native/math.mxy):
// @butter-export
int fibonacci(int n) {
if (n <= 1) { return n; }
int a = 0;
int b = 1;
for i in 2..n+1 {
int tmp = b;
b = a + b;
a = tmp;
}
return b;
}
C (src/native/crypto.c):
#include "butter.h"
BUTTER_EXPORT(
int fast_hash(const char *input, int len) {
int hash = 0;
for (int i = 0; i < len; i++) hash = hash * 31 + input[i];
return hash;
}
)Rust (src/native/mathrs.rs):
// @butter-export
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
// @butter-export
#[no_mangle]
pub extern "C" fn fast_hash(input: *const u8, len: i32) -> i32 {
let mut h: i32 = 0;
unsafe {
for i in 0..len {
h = h.wrapping_mul(31).wrapping_add(*input.offset(i as isize) as i32);
}
}
h
}For multi-file Rust projects with crate dependencies, point a directory at a Cargo.toml instead — Butter runs cargo build --release and uses the directory name as the module name.
Zig (src/native/fib.zig):
// @butter-export
export fn fib(n: i32) i32 {
return if (n < 2) n else fib(n - 1) + fib(n - 2);
}Use from TypeScript:
import { native } from "butter/native"
const math = await native("math")
const fib = math.fibonacci(20) // 6765 — computed in native code
const crypto = await native("crypto")
const hash = crypto.fast_hash("hello", 5)
const mathrs = await native("mathrs")
const sum = mathrs.add(2, 3) // 5 — computed in Rust
const fibZig = await native("fib")
const f10 = fibZig.fib(10) // 55 — computed in ZigButter parses the appropriate marker for each language — BUTTER_EXPORT() blocks in C, // @butter-export annotations above functions in Moxy, Rust, and Zig — extracts the signatures, compiles to a shared library, and generates typed TypeScript bindings. A SHA-256 fingerprint of the source + compiler flags + platform is cached alongside each library; rebuilds only run when something actually changed. butter doctor reports Rust and Zig as optional toolchains, so missing them never fails the doctor unless you actually have .rs or .zig sources.
Define native menus in src/host/menu.ts:
import type { Menu } from "butter"
export default [
{
label: "File",
items: [
{ label: "New", action: "file:new", shortcut: "CmdOrCtrl+N" },
{ label: "Open", action: "file:open", shortcut: "CmdOrCtrl+O" },
{ separator: true },
{ label: "Quit", action: "app:quit", shortcut: "CmdOrCtrl+Q" },
],
},
{
label: "Edit",
items: [
{ label: "Undo", action: "edit:undo", shortcut: "CmdOrCtrl+Z" },
{ label: "Redo", action: "edit:redo", shortcut: "CmdOrCtrl+Shift+Z" },
{ separator: true },
{ label: "Cut", action: "edit:cut", shortcut: "CmdOrCtrl+X" },
{ label: "Copy", action: "edit:copy", shortcut: "CmdOrCtrl+C" },
{ label: "Paste", action: "edit:paste", shortcut: "CmdOrCtrl+V" },
],
},
] satisfies MenuCmdOrCtrlresolves to Cmd on macOS, Ctrl on Linux/Windows- Standard edit actions map to native OS behavior
- Custom actions fire as IPC events — handle with
on("file:new", ...) - On macOS, the app menu is built automatically from your app title
For type-safe IPC between host and webview:
// shared/types.ts — define your IPC contract
import type { InvokeMap } from "butter"
export type AppInvokes = {
greet: { input: string; output: string }
"math:add": { input: { a: number; b: number }; output: number }
}// host side
import { createTypedHandlers } from "butter/types"
const { on } = createTypedHandlers<AppInvokes>()
on("greet", (name) => `Hello, ${name}!`) // fully typed// webview side
import { createTypedInvoke } from "butter/types"
const { invoke } = createTypedInvoke<AppInvokes>()
const greeting = await invoke("greet", "World") // typed as stringbutter init <name> [--template vanilla|react|svelte|vue]
Create a new project
butter dev Start development mode (hot reload + DevTools)
butter compile Build a single-file binary
butter bundle Create OS-native app package (.app / AppDir)
butter package Build a distributable installer (DMG / AppImage / NSIS)
butter sign Code-sign and notarize the app bundle
butter doctor Check platform prerequisites
Starts development mode:
- Compiles native extensions (C / Moxy / Rust / Zig) if present
- Compiles the native shim (cached)
- Bundles frontend assets
- Opens a native window with DevTools enabled (right-click to inspect)
- Watches for file changes and reloads automatically
Produces a single executable:
- Compiles native extensions and shim
- Bundles and embeds all assets
- Strips debug symbols
- Output:
dist/<appname>(~60MB)
Creates an OS-native app package:
- macOS:
.appbundle withInfo.plist, icon, and the compiled binary - Linux: AppDir structure with
.desktopfile andAppRunsymlink - Windows: Distribution folder with
.exeand resources
Any sidecars declared in bundle.sidecars are copied into a sidecars/ directory adjacent to the main executable.
Wraps the platform bundle as a distributable artifact:
- macOS:
dist/<App>.dmg— built withhdiutil, includes a drag-to-Applications shortcut. - Linux:
dist/<App>-x86_64.AppImage— built viaappimagetool, auto-downloaded into.butter/tools/on first run. - Windows:
dist/<App>-setup.exeifmakensis(NSIS) is on PATH; otherwise falls back todist/<App>.zip(portable).
Run butter bundle first; package operates on the output bundle.
$ butter doctor
Bun ........................... v1.3.13
Compiler ...................... clang 17.0.0
Webview ....................... WKWebView (macOS)
Rust (optional) ............... rustc 1.95.0, cargo 1.95.0
Zig (optional) ................ zig 0.14.0
All checks passed.
Built-in plugins for common native capabilities:
| Plugin | Capabilities |
|---|---|
dialog |
Native open, save, and folder selection dialogs |
navigation |
Webview navigation control (back, forward, reload) |
findinpage |
In-page text search with highlight and match cycling |
dock |
macOS Dock badge, bounce, and progress bar |
| Plugin | Capabilities |
|---|---|
tray |
System tray icon with context menu |
notifications |
OS notification center with actions and grouping |
clipboard |
Read and write system clipboard (text, image, rich text) |
globalshortcuts |
Register hotkeys that work when the app is unfocused |
shell |
Open URLs, files, and folders in the default application |
theme |
Detect and respond to system light/dark mode changes |
lifecycle |
App lifecycle events (ready, will-quit, activate, reopen) |
| Plugin | Capabilities |
|---|---|
fs |
Sandboxed file system access (read, write, watch) |
securestorage |
Encrypted key-value storage backed by OS keychain |
downloads |
Download files with progress tracking and destination control |
| Plugin | Capabilities |
|---|---|
network |
Online/offline detection and connectivity change events |
logging |
Structured logging to file with rotation and log levels |
crashreporter |
Capture and report uncaught exceptions and native crashes |
| Plugin | Capabilities |
|---|---|
autoupdater |
Check for updates, download, and apply new versions |
autolaunch |
Register the app to launch at user login (macOS/Linux/Windows) |
| Plugin | Capabilities |
|---|---|
singleinstance |
Enforce a single running instance; second launches forward argv to the leader |
| Plugin | Capabilities |
|---|---|
power |
Sleep/wake, screen sleep/wake, screen lock/unlock events, and idle-time queries (butter.power, butter.screen.list) |
| Plugin | Capabilities |
|---|---|
sidecar |
Spawn, kill, and stream stdio of bundled external executables (ffmpeg, yt-dlp, etc.) declared in bundle.sidecars |
| Plugin | Capabilities |
|---|---|
database |
Embedded SQLite via bun:sqlite — butter.db.open(name) returns a connection with query, exec, get |
store |
Persistent JSON-file KV store — butter.store("settings").set/get/delete/keys/clear |
| Plugin | Capabilities |
|---|---|
i18n |
Internationalization with locale detection and string lookup |
accessibility |
Screen reader announcements and accessibility attributes |
import { on } from "butter"
// File dialogs (via osascript on macOS)
on("open-file", async () => {
const path = await butter.invoke("dialog:open", { prompt: "Select a file" })
return path
})| Platform | Webview | Compiler | Status |
|---|---|---|---|
| macOS | WKWebView | clang (Xcode CLI tools) | Supported |
| Linux | WebKitGTK | cc/gcc | Supported |
| Windows | WebView2 | MSVC/MinGW | Supported |
No additional dependencies — WKWebView and clang ship with macOS.
# Ubuntu/Debian
sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev
# Fedora
sudo dnf install webkit2gtk4.1-devel gtk3-devel
# Arch
sudo pacman -S webkit2gtk-4.1 gtk3Requires Bun for Windows, a C compiler (MSVC or MinGW), and the WebView2 runtime (pre-installed on Windows 10 21H2+ and Windows 11).
# Install Bun
powershell -c "irm bun.sh/install.ps1 | iex"
# Compiler: install Visual Studio Build Tools (includes cl.exe)
# Or install MinGW-GCCApp Code (TS/HTML/CSS) You write this
Native Extensions Optional, auto-compiled
(C / Moxy / Rust / Zig)
Butter Runtime (Bun/TS) CLI, IPC bridge, API, FFI bindings
Platform Shim (ObjC/C) Native window + webview
Shared memory with two ring buffers. Messages are length-prefixed JSON. Signaling via platform-native mechanisms (POSIX semaphores on macOS/Linux, named events on Windows).
+----------+------------------+------------------+
| Header | Host -> Webview | Webview -> Host |
| (64B) | ring buffer | ring buffer |
+----------+------------------+------------------+
128KB total shared memory
Assets are served via the butter:// custom protocol, eliminating file:// CORS restrictions.
git clone https://github.com/wess/butter.git
cd butter
bun install
# Run the example (includes native Moxy extension)
cd example/hello
bun install
bun run dev
# Run tests
cd ../..
bun testMIT
