Skip to content

Build Configuration

electrobun.config.ts is a TypeScript module in the project root. Hutch loads its default export with Cottontail and merges it with Electrobun’s defaults. Use satisfies ElectrobunConfig so unsupported fields and values fail during development. Electrobun release overrides, tasks, and package-manager selection belong in hutch.config.ts; the release and external package-manager fields are optional. This file owns the app, build, tool overrides, and distribution description.

import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "My App",
identifier: "com.example.my-app",
version: "1.0.0",
},
build: {
mainProcess: "cottontail",
cottontail: {
entrypoint: "src/bun/index.ts",
},
views: {
main: {
entrypoint: "src/mainview/index.ts",
},
},
copy: {
"src/mainview/index.html": "views/main/index.html",
"src/mainview/index.css": "views/main/index.css",
},
},
runtime: {
exitOnLastWindowClosed: true,
},
} satisfies ElectrobunConfig;

Run hutch electrobun config --env=dev to inspect the merged configuration that Hutch will use.

hutch.config.ts may set electrobun.version to an exact release such as 2.0.0 or 2.1.0-beta.3. That optional pin wins over the npm bootstrap’s paired release default or a direct Hutch invocation’s floating channel. The application version in app.version remains independent.

Hutch resolves and verifies the selected release under ~/.hutch/releases/electrobun, then copies its config types and language SDKs into the generated project .hutch/devkit sysroot. Run hutch electrobun prepare to materialize or refresh that project copy without building or advancing a valid floating projection. build, run, and dev prepare implicitly. Run hutch electrobun update to move an existing exact pin to the latest stable release and sync the app; use hutch electrobun sync when you deliberately want an unpinned direct-Hutch project to follow the current release channel head. See Project Ownership and the Devkit for the config responsibility table, generated-state lifecycle, and external-bundler aliases.

app is required.

Field Type Purpose
name string Display name and base artifact name.
identifier string Platform application identifier, normally reverse-DNS.
version string Application version written into release metadata.
description string? Human-readable description.
urlSchemes string[]? Custom URL schemes on macOS.
fileAssociations FileAssociation[]? Document associations on macOS.

Custom URL schemes are currently registered on macOS. Install the built app in /Applications or ~/Applications so Launch Services can register it.

import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "Link Receiver",
identifier: "com.example.link-receiver",
version: "1.0.0",
urlSchemes: ["link-receiver", "link-receiver-dev"],
},
} satisfies ElectrobunConfig;

Handle the resulting URL in the main process:

import Electrobun from "electrobun/main";
Electrobun.events.on("open-url", (event) => {
const url = new URL(event.data.url);
console.log(url.protocol, url.pathname);
});

File associations are currently packaged on macOS. Extensions do not include the leading dot. An optional icon must be an .icns source file.

import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "Document Editor",
identifier: "com.example.document-editor",
version: "1.0.0",
fileAssociations: [
{
name: "Example Document",
ext: ["example", "example-project"],
role: "Editor",
icon: "assets/example-document.icns",
},
],
},
} satisfies ElectrobunConfig;

Opened files arrive through open-url as file: URLs.

import Electrobun from "electrobun/main";
Electrobun.events.on("open-url", (event) => {
const url = new URL(event.data.url);
if (url.protocol === "file:") {
console.log("Opened file", decodeURIComponent(url.pathname));
}
});

Windows and Linux URL schemes and file associations are not yet implemented.

build.mainProcess selects the runtime and compiler. It defaults to "cottontail".

Value Configuration Project build input
cottontail build.cottontail src/bun/index.ts
bun build.bun src/bun/index.ts
zig build.zig project build.zig
rust build.rust Cargo.toml, binary target main
go build.go go.mod, package ./src/go
odin build.odin package containing src/odin/main.odin
import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "Native Example",
identifier: "com.example.native",
version: "1.0.0",
},
build: {
mainProcess: "zig",
zig: { version: "0.16.0" },
},
} satisfies ElectrobunConfig;

Each selected Electrobun release declares default Zig, Rust, Go, and Odin versions. The matching build.zig.version, build.rust.version, build.go.version, or build.odin.version field is an exact per-project override. Hutch reuses exact matching system compilers where supported, or stores the selected compiler under ~/.hutch/toolchains/<language>/<version>/<platform>.

Native build descriptions remain project-owned:

  • Zig: build.zig; Hutch passes -Delectrobun-sdk=<project>/.hutch/devkit/zig-sdk/electrobun.zig.
  • Rust: Cargo.toml and Cargo.lock; configure manifest (default Cargo.toml) and binary (default main). A local dependency may point to .hutch/devkit/rust-sdk.
  • Go: go.mod and go.sum; configure package (default ./src/go). Use require electrobun v0.0.0 and replace electrobun => ./.hutch/devkit/go-sdk in the module root.
  • Odin: normal source package; entrypoint defaults to src/odin/main.odin, and Hutch passes the projected electrobun_sdk collection. No build.odin is generated or required.

Hutch invokes the normal project build tool. It does not generate build.zig, Cargo manifests, Go modules, or a temporary GOPATH.

Hutch builds for the current host platform and architecture. Use native CI runners when producing the macOS, Windows, and Linux release matrix.

Field Type Default Purpose
entrypoint string "src/bun/index.ts" Main-process source entrypoint.
capabilities string[] [] Additional capability modules to package when dynamic usage cannot be found by the bundle scan.
bytecode false | true | "obfuscate" false Precompile the main process; this option is declared but not yet wired.

The JavaScript bundler options described below can also be set in this object.

Cottontail keeps optional standard-library APIs outside its core executable and loads each capability on demand through module imports or Cottontail.* namespace access. When build.mainProcess is "cottontail", Hutch scans the final tree-shaken main-process bundle for recognized bun:*, node:*, and Cottontail.* references. It packages the core runtime plus the selected capability bytecode and native libraries, including transitive dependencies declared by the runtime’s capability manifest. Each build reports the automatic result:

hutch electrobun: Cottontail capabilities discovered by scan: [sqlite, compression]

This message contains the scan result only. Explicit configuration entries are merged afterward, followed by dependency expansion. This is not a complete inventory of the packaged runtime.

Detection matches known text in the bundle rather than evaluating JavaScript. For example, bun:sqlite, node:sqlite, and Cottontail.sqlite select sqlite; node:zlib and Cottontail.compression select compression. The scan includes bundled dependency code, but cannot reliably infer aliased or destructured namespaces, computed property access, or code loaded later. Global Bun compatibility APIs are not all scan markers: Bun.SQL alone, for example, needs an explicit sql include or a recognized bun:sql import.

Set build.cottontail.capabilities when a capability is reached through a computed dynamic import, generated source, a plugin loader, or another path the bundle scan cannot see. These entries are additive: Hutch still includes every capability found by its scan. An empty list does not disable detection, and the list cannot exclude automatically selected modules. This is a packaging setting, not a runtime permission policy, and does not change Hutch’s build-time runtime or a Bun main process.

import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "Dynamic Modules",
identifier: "com.example.dynamic-modules",
version: "1.0.0",
},
build: {
mainProcess: "cottontail",
cottontail: {
entrypoint: "src/bun/index.ts",
capabilities: ["sqlite", "compression"],
},
},
} satisfies ElectrobunConfig;

Supported capability names are archive, bake, build, colors, compression, cookies, csrf, data, ffi, filesystemRouter, glob, hashing, htmlRewriter, inspector, jscTools, json5, markdown, password, redis, repl, s3, sea, secrets, shell, sql, sqlite, terminal, test, text, toml, uuid, websocket, and yaml. Unknown names fail the build. If an omitted capability is reached at runtime, Cottontail’s error identifies the missing name and points back to build.cottontail.capabilities.

The precompiled standard-library capability modules above are already packaged by Hutch. The setting below concerns compilation of your application’s own main-process source, which is separate.

When the main process runs on Cottontail, build.cottontail.bytecode can ship it as precompiled JavaScriptCore bytecode instead of source. This is a Cottontail-only option — Bun main processes are unaffected.

There are three levels, and the two “on” levels are deliberately distinct because they carry different trade-offs:

bytecode Ships Startup Source protection Debuggable Risk
false (default) .js normal none yes —
true .jsc + .js faster (skips parsing) none yes low — falls back to parsing the source
"obfuscate" .jsc only faster source not shipped no see below
import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "My App",
identifier: "com.example.my-app",
version: "1.0.0",
},
build: {
mainProcess: "cottontail",
cottontail: {
entrypoint: "src/bun/index.ts",
// Faster startup, source still shipped and debuggable:
bytecode: true,
// Or protect your source (accept the trade-offs below):
// bytecode: "obfuscate",
},
},
} satisfies ElectrobunConfig;

bytecode: true compiles the main process to bytecode so the runtime skips parsing at launch. Your source is still shipped alongside the bytecode as a fallback, so Function.prototype.toString returns real source, error stacks show source frames, and a version mismatch simply reparses the source. This is the safe “just make my app start faster” choice.

bytecode: "obfuscate" additionally strips the source, so only bytecode is distributed. Consider the trade-offs before enabling it:

  • Function.prototype.toString returns [native code] for your functions. Some libraries introspect function source (certain dependency-injection, ORM, and validation frameworks) and can break.
  • Error stacks keep line and column numbers but lose source frames, and no sourcemaps are emitted — shipped-app debugging is degraded.
  • There is no source fallback, so a runtime/version mismatch is a hard error rather than a silent reparse.

This is source obfuscation, not encryption: the bytecode ships without your source text, comments, or original identifiers, which makes it materially harder to read than minified JavaScript, but a determined party with the matching Cottontail binary can still disassemble it.

Two things hold regardless of the setting:

  • Dev builds ignore bytecode (hutch electrobun dev / --env=dev) and always ship source, so sourcemaps and the debugger keep working. In watch mode, Hutch rebuilds and relaunches the app from that source. Bytecode applies to canary and stable builds.
  • The bytecode is generated against the exact Cottontail bundled into your app and regenerated on every build, so it never drifts from the runtime that will execute it.

Hutch owns the output path, platform, and base format for Cottontail, Bun, and view bundles. The following serializable options are currently forwarded to Cottontail’s build API:

Option Type Applies to
minify boolean main process and views
sourcemap boolean | "inline" | "external" | "linked" main process and views
target string | string[] main process and views
external string[] main process and views
define Record<string, string> main process and views
alias Record<string, string> main process and views
format "esm" | "cjs" | "iife" views only

Config is serialized while Hutch loads it. Function-valued bundler plugins cannot cross that boundary and are not currently supported in electrobun.config.ts. Framework templates that need plugin transforms run their own Vite build and copy its output.

import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "Bundled App",
identifier: "com.example.bundled-app",
version: "1.0.0",
},
build: {
cottontail: {
entrypoint: "src/bun/index.ts",
minify: true,
sourcemap: "linked",
external: ["optional-native-module"],
define: {
"process.env.APP_EDITION": '"desktop"',
},
alias: {
"@shared": "./src/shared/index.ts",
},
},
views: {
main: {
entrypoint: "src/mainview/index.ts",
minify: true,
sourcemap: "linked",
format: "esm",
},
},
},
} satisfies ElectrobunConfig;

build.copy maps project-relative source paths to safe relative paths inside the packaged application resources. buildFolder and artifactFolder are also project-relative. Nested paths are supported, but absolute paths, empty paths, and . or .. path components are rejected before Hutch creates, copies, or removes output files.

Field Type Default
copy Record<string, string> {}
buildFolder string build
artifactFolder string artifacts
import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "Packaged App",
identifier: "com.example.packaged-app",
version: "1.0.0",
},
build: {
copy: {
"src/mainview/index.html": "views/main/index.html",
"assets": "assets",
},
buildFolder: "build",
artifactFolder: "artifacts",
},
} satisfies ElectrobunConfig;

hutch electrobun dev --watch derives watch roots from configured source and native build inputs, view entrypoints, and copy sources. Add other source locations with build.watch; suppress generated or high-churn paths with build.watchIgnore. build, artifacts, and node_modules are always ignored.

Watch mode rebuilds and relaunches the whole app; it is distinct from webview-only hot module replacement. See Hot Reloading for the main-process, Vite, and Warren reload boundaries.

import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "Watched App",
identifier: "com.example.watched-app",
version: "1.0.0",
},
build: {
watch: ["scripts", "vendor/native-library"],
watchIgnore: ["**/*.generated.*", "data/cache/**"],
},
} satisfies ElectrobunConfig;
Field Type Default
codesign boolean false
createDmg boolean true
notarize boolean false
bundleCEF boolean false
bundleWGPU boolean false
defaultRenderer "native" | "cef" "native"
chromiumFlags Record<string, string | boolean> none
entitlements Record<string, boolean | string | string[]> JIT defaults
icons string icon.iconset

An icon source can be an .iconset directory or an Icon Composer .icon file. notarize requires signing credentials; see Code Signing.

Field Type Default
bundleCEF boolean false
bundleWGPU boolean false
defaultRenderer "native" | "cef" "native"
chromiumFlags Record<string, string | boolean> none
autoGrantPermissions WindowsWebView2Permission[] []
icon string none

autoGrantPermissions applies to every origin in native WebView2 views and does not affect CEF. Grant only capabilities the application intentionally delegates to its web content.

Field Type Default
bundleCEF boolean false
bundleWGPU boolean false
defaultRenderer "native" | "cef" "native"
chromiumFlags Record<string, string | boolean> none
icon string none
flatpak FlatpakConfig disabled

Flatpak mode writes an expanded flatpak-builder manifest and /app payload; Hutch does not invoke flatpak-builder itself.

import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "Cross Platform App",
identifier: "com.example.cross-platform",
version: "1.0.0",
},
build: {
mac: {
bundleCEF: true,
bundleWGPU: true,
defaultRenderer: "cef",
codesign: true,
notarize: true,
icons: "App.icon",
},
win: {
bundleCEF: true,
bundleWGPU: true,
defaultRenderer: "cef",
autoGrantPermissions: ["camera", "microphone"],
icon: "assets/icon.ico",
},
linux: {
bundleCEF: true,
bundleWGPU: true,
defaultRenderer: "cef",
icon: "assets/icon.png",
flatpak: {
enabled: true,
runtimeVersion: "25.08",
},
},
},
} satisfies ElectrobunConfig;

chromiumFlags is used only by CEF. Keys omit the -- prefix. A true value adds a switch, a string adds a switch with a value, and false suppresses an Electrobun default with the same name.

import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "CEF App",
identifier: "com.example.cef-app",
version: "1.0.0",
},
build: {
mac: {
bundleCEF: true,
chromiumFlags: {
"show-paint-rects": true,
"user-agent": "CEF App/1.0",
"disable-gpu": false,
},
},
},
} satisfies ElectrobunConfig;

Development CEF builds automatically select an available loopback debugging port from 9222 through 9232. Canary and stable builds disable remote debugging unless "remote-debugging-port" is set to a string port from 1024 through 65535. Set it to false to disable the dev endpoint too.

ELECTROBUN_CEF_REMOTE_DEBUGGING_PORT can override this for one launch. Set it to a port, or to 0, off, or false to disable the endpoint.

The runtime object is copied into build.json. exitOnLastWindowClosed defaults to true; set it to false for tray and background applications. Additional JSON-compatible values are available through BuildConfig.

import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "Background App",
identifier: "com.example.background-app",
version: "1.0.0",
},
runtime: {
exitOnLastWindowClosed: false,
serviceEndpoint: "https://api.example.com",
},
} satisfies ElectrobunConfig;
import { BuildConfig } from "electrobun/main";
const config = await BuildConfig.get();
console.log(config.runtime?.serviceEndpoint);

Hook paths are relative to the project root. Hutch transpiles each TypeScript or JavaScript file and executes it with the host-platform Cottontail binary.

Hook Timing
preBuild Before any application build work.
postBuild After main process, views, copied assets, and Carrot output are built.
postWrap After a non-dev release wrapper is assembled, before final packaging/signing.
postPackage After dev output or all release artifacts are complete.
import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "Hooked App",
identifier: "com.example.hooked-app",
version: "1.0.0",
},
scripts: {
preBuild: "./scripts/pre-build.ts",
postBuild: "./scripts/post-build.ts",
postWrap: "./scripts/post-wrap.ts",
postPackage: "./scripts/post-package.ts",
},
} satisfies ElectrobunConfig;

All hooks inherit the process environment and receive:

  • ELECTROBUN_BUILD_ENV: dev, canary, or stable
  • ELECTROBUN_OS: macos, linux, or win
  • ELECTROBUN_ARCH: x64 or arm64
  • ELECTROBUN_BUILD_DIR
  • ELECTROBUN_ARTIFACT_DIR
  • ELECTROBUN_APP_NAME
  • ELECTROBUN_APP_VERSION
  • ELECTROBUN_APP_IDENTIFIER

postWrap also receives ELECTROBUN_WRAPPER_BUNDLE_PATH. postBuild receives ELECTROBUN_CARROT_DIR when the build produced a Carrot.

import { cpSync, existsSync } from "node:fs";
import { join } from "node:path";
const wrapper = process.env.ELECTROBUN_WRAPPER_BUNDLE_PATH;
if (!wrapper) throw new Error("This hook requires a release wrapper");
const source = "assets/release-only";
if (existsSync(source)) {
cpSync(source, join(wrapper, "Contents", "Resources", "release-only"), {
recursive: true,
});
}

The path layout in that example is macOS-specific; branch on ELECTROBUN_OS for a cross-platform hook.

release.baseUrl is the public artifact base used by update metadata. release.generatePatch defaults to true; disable it when a local canary or stable build should skip downloading and diffing the previous release.

import type { ElectrobunConfig } from "electrobun";
export default {
app: {
name: "Released App",
identifier: "com.example.released-app",
version: "2.0.0",
},
release: {
baseUrl: "https://releases.example.com/released-app/",
generatePatch: true,
},
} satisfies ElectrobunConfig;

build.carrot produces a Carrot artifact in addition to the standalone app. Set carrotOnly: true to omit the standalone application. The object accepts an ID, display name, optional description and mode, dependencies, remote/slate UI definitions, and file-activator contributions. bunny.carrot.dependencies declares dependencies used when the same project is distributed through Bunny Ears.