Skip to content

Sectly/buntop

Repository files navigation

BunTop

Ask DeepWiki

A tiny desktop framework using Bun and WebView. Light, simple, and just works. If your app needs more than this, use Electron or similar instead.

No IPC. Communication between the frontend and backend happens over a local WebSocket.

NPM Packages:

All of the above support Windows, macOS, and Linux.

How it works

  • The main process runs a local Bun.serve() that serves your static files (HTML, CSS, JS, images, fonts, etc.) and a WebSocket endpoint for RPC and events.
  • The webview window runs in a separate child process that connects back over that WebSocket.
  • This keeps the server responsive even while the window's native event loop is running.

Getting started

bun add buntop
bunx buntop init   # interactive: scaffolds buntopconfig.json (and index.js if missing)
bunx buntop init --yes   # non-interactive: writes a default config (also used automatically when stdin isn't a TTY)
bunx buntop build dev

Usage

import { BunTop } from "buntop";

const app = new BunTop({ title: "My App", width: 800, height: 600 });

app.expose("getVersion", () => "1.0.0");

app.start({ path: "./ui" }); // directory containing index.html, or a single .html file

start() accepts one of:

  • path - an .html file or a directory containing index.html. Other local files referenced from it (.css, .js, images, fonts, etc.) are served alongside it.
  • html - an inline HTML string.
  • url - a remote URL to navigate to.

Pass sandbox: true to load less-trusted content (e.g. a remote url): this disables the buntop IPC bridge, the /__buntop_ws and /__buntop_focus endpoints, and devtools, so the page has no access to exposed functions or backend APIs.

Frontend API

const version = await buntop.call("getVersion");

window.addEventListener("buntop:tick", (e) => console.log(e.detail));

Backend API

  • app.expose(name, fn) - register a function callable from the frontend via buntop.call(name, ...args).
  • app.emit(event, data) - dispatch a buntop:<event> CustomEvent to the frontend.
  • app.notify(options) - show a native OS notification. (docs)
  • app.tray(options) - create a system tray icon and menu. (docs)
  • app.protocol.register/deregister/isRegistered/getDefaultApp - manage custom URL protocol handlers. (docs)
  • app.dialog.openFile/saveFile/openFolder/message/confirm - native file and message dialogs. (docs)
  • app.updater.check/download/apply - check for, download, and install updates. (docs)
  • app.open(target) - open a URL or file/folder path in the OS default browser/app.
  • app.paths.home/appData/config/cache/temp/logs/documents/downloads/desktop/pictures/music/videos - standard OS directories. (docs)
  • app.paths.userData/userCache/userLogs - per-app, per-channel directories ({appData|cache|logs}/{identifier}/{channel}, channel from BUNTOP_CHANNEL, default "dev"). identifier comes from "identifier" in buntopconfig.json for compiled builds, falling back to package.json's "name". (docs)
  • app.moveToTrash(path) - move a file or folder to the OS trash/recycle bin.
  • app.log.debug/info/warn/error(...) - rolling, newline-delimited JSON file logger (see "Logging").
  • app.close() - close the window and exit cleanly.

When singleInstance is enabled, launching a second copy of the app focuses the existing window (via window.focus()) and emits buntop:second-instance with { argv } (the new process's argv.slice(2)) in the running instance, then exits the new process. (docs)

Window options

new BunTop({
  title: "My App",
  width: 800,
  height: 600,
  sizeHint: "none", // "none" | "min" | "max" | "fixed"
  debug: false,     // enable webview devtools (always off in beta/release builds)
  shareEnv: false,  // include process.env in buntop.info on the frontend
  singleInstance: false, // only allow one running instance per app + channel
  log: {}, // logger options, or `false` to disable logging
});

Examples

  • examples/minimal.js - the smallest possible app.
  • examples/basic.js - exposed functions, events, and the frontend buntop API.
  • examples/tray.js - system tray icon, menu, and notifications.
  • examples/notes-app/ - a small but complete app: static site serving, paths, dialogs, tray, notifications, the trash, and single-instance handling.

Exit handling

The window closes and the process exits cleanly on:

  • the window being closed by the user,
  • Ctrl+C (SIGINT) or SIGTERM,
  • an uncaught error,
  • the backend process exiting for any other reason.

Icons

Set "icon" in buntopconfig.json to a single source .png (ideally 1024x1024). The build script (bun run build:<channel>) uses icon-gen to generate, into {outDir}/icons/:

  • icon.ico - used for the compiled exe (--windows-icon)
  • icon.icns - for macOS app bundles
  • favicon.ico - served automatically as /favicon.ico if your site/HTML doesn't have its own
  • icon16.png ... icon1024.png - for tray icons, notifications, and Linux desktop files
{
  "name": "my-app",
  "title": "My App",
  "icon": "./assets/icon.png",
  "entry": "index.js"
}

Then wire the generated files up:

  • Compiled exe icon / title: set automatically from "icon" and "title" for Windows targets.
  • Window / titlebar icon: not set by BunTop. Use an HTML <link rel="icon"> tag, or a <head> favicon, in your page.
  • Tray icon: app.tray({ icon: { png: "./dist/icons/icon32.png", ico: "./dist/icons/icon.ico" } }).
  • Notification icon: app.notify({ icon: "./dist/icons/icon128.png", ... }).

You can also set "icon" directly to a pre-made .ico file to skip generation.

Logging

app.log.info("ready", { port: app.server.port });
app.log.error("something broke", err);

console.log("log file:", app.log.path()); // {logs}/{identifier}/{channel}/app.log
  • Writes newline-delimited JSON ({ time, level, message }) to app.paths.userLogs().
  • Rotates to app.log.1, app.log.2, ... once the active file exceeds maxSize (default 5MB), keeping maxFiles rotated files (default 5).
  • Uncaught exceptions, unhandled rejections, server/socket errors, and webview crashes are logged automatically.
  • app.log.files() returns the active log file plus any rotated ones, useful for attaching to a bug report or crash uploader.
  • Pass log: false to disable logging entirely.

Updater

const update = await app.updater.check({
  currentVersion: app.info().version,
  source: { type: "github", repo: "owner/repo", asset: process.platform === "win32" ? /\.exe$/ : /linux/ },
  // or: { type: "manifest", url: "https://example.com/update.json" }
  // or: { type: "file", path: "./update.json" }
});

if (update.hasUpdate) {
  const file = await app.updater.download(update, "./update-download");
  app.updater.apply(file); // replaces the running binary and restarts (compiled builds only)
}

Manifest/file sources are JSON: { "version": "1.2.3", "url": "https://.../app-linux", "notes": "..." }. url may be http(s)://, a local path, or file://.

Building

buntopconfig.json defines how bunx buntop build <channel> (dev, alpha, beta, release) compiles your app with bun build --compile. Run bunx buntop init to generate a starter config.

{
  "name": "buntop-app",
  "entry": "examples/basic.js",
  "outDir": "dist",
  "buildBin": "build-bin",
  "icon": "./examples/icon.png",
  "license": "./LICENSE",
  "identifier": "online.sectly.buntop-app",
  "title": "BunTop App",
  "description": "A standalone BunTop application",
  "publisher": "Sectly",
  "version": "0.0.1",
  "copyright": "Copyright 2026 Sectly",
  "channels": {
    "dev": { "targets": ["current"], "minify": false, "sourcemap": true, "bytecode": false },
    "release": { "targets": ["bun-windows-x64", "bun-linux-x64", "bun-linux-arm64", "bun-darwin-x64", "bun-darwin-arm64"], "minify": true, "sourcemap": false, "bytecode": false }
  }
}
  • title, description, publisher, version, copyright are embedded as Windows executable metadata (--windows-title/-description/-publisher/-version/-copyright).
  • icon is embedded as the .exe icon (--windows-icon) and used to generate the app/window/taskbar icons described above.
  • identifier (e.g. online.sectly.buntop-app) is the app's bundle/package ID. It's embedded into the binary (used for app.paths.userData/userCache/userLogs) and used as the macOS CFBundleIdentifier and Linux .desktop filename. Defaults to com.<publisher>.<name>.
  • The selected channel is embedded via __BUNTOP_CHANNEL__ and exposed at runtime (BUNTOP_CHANNEL); all Windows builds use --windows-hide-console (console output still works when run from a terminal); beta and release builds also have devtools disabled.
  • The build script prints per-target progress, timing, and output file sizes, and exits non-zero if any target fails to build.

Installers

beta and release builds also produce an installer/bundle for each target alongside the portable executable (set "installer": false/true on a channel to override). The compiled exe still works standalone if you'd rather ship that.

  • Windows: an NSIS *-setup.exe with Start Menu + Desktop shortcuts and an uninstaller. makensis is downloaded automatically and extracted into buildBin (default ./build-bin, configurable in buntopconfig.json) on first use, so no manual NSIS install is needed and this works from any host OS. Safe to delete; it'll be re-downloaded on the next build.
    • The setup wizard lets the user choose a per-user install (%LOCALAPPDATA%, no prompt) or an all-users install (Program Files, triggers a UAC prompt only in that case).
    • If a previous version is detected (via the registry), the installer offers to silently uninstall it first.
    • If the app is currently running during install or uninstall, the user is prompted to close it before continuing.
    • Installs/uninstalls a standard Add/Remove Programs entry.
    • If "license" is set, it's shown as a license-acceptance page.
  • macOS: a *.app bundle (with Info.plist and .icns icon, plus the license file in Resources if "license" is set). If hdiutil is available (i.e. building on macOS), also produces a *.dmg.
  • Linux: an *.AppDir with a .desktop file, icon, AppRun launcher, and the license file if "license" is set, packaged as a *.tar.gz.

If a step can't be completed (e.g. .dmg generation off macOS), the build logs a warning and skips just that part - the portable executable/bundle is still built normally.

Error handling

If BunTop can't start (unsupported OS, missing Bun runtime, missing system webview dependencies, or a webview crash), it prints an error with install hints and writes the failure to the log file (so it's visible even in --windows-hide-console builds), then exits with a non-zero code instead of leaving a silent/blank window.

Planned features

  • SQLite (Bun's built-in SQLite)

About

BunTop - a tiny desktop framework using Bun and WebView.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors