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.

Electrobun Devkit

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.

Application Metadata

app is required.

FieldTypePurpose
namestringDisplay name and base artifact name.
identifierstringPlatform application identifier, normally reverse-DNS.
versionstringApplication version written into release metadata.
descriptionstring?Human-readable description.
urlSchemesstring[]?Custom URL schemes on macOS.
fileAssociationsFileAssociation[]?Document associations on macOS.

URL Schemes

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

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.

Main Process

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

ValueConfigurationProject build input
cottontailbuild.cottontailsrc/bun/index.ts
bunbuild.bunsrc/bun/index.ts
zigbuild.zigproject build.zig
rustbuild.rustCargo.toml, binary target main
gobuild.gogo.mod, package ./src/go
odinbuild.odinpackage 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.

Bytecode And Obfuscation

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:

bytecodeShipsStartupSource protectionDebuggableRisk
false (default).jsnormalnoneyes
true.jsc + .jsfaster (skips parsing)noneyeslow — falls back to parsing the source
"obfuscate".jsc onlyfastersource not shippednosee 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.

JavaScript Bundling

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:

OptionTypeApplies to
minifybooleanmain process and views
sourcemapboolean | "inline" | "external" | "linked"main process and views
targetstring | string[]main process and views
externalstring[]main process and views
defineRecord<string, string>main process and views
aliasRecord<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;

Static Files And Output

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.

FieldTypeDefault
copyRecord<string, string>{}
buildFolderstringbuild
artifactFolderstringartifacts
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;

Watch Mode

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;

Platform Options

macOS

FieldTypeDefault
codesignbooleanfalse
createDmgbooleantrue
notarizebooleanfalse
bundleCEFbooleanfalse
bundleWGPUbooleanfalse
defaultRenderer"native" | "cef""native"
chromiumFlagsRecord<string, string | boolean>none
entitlementsRecord<string, boolean | string | string[]>JIT defaults
iconsstringicon.iconset

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

Windows

FieldTypeDefault
bundleCEFbooleanfalse
bundleWGPUbooleanfalse
defaultRenderer"native" | "cef""native"
chromiumFlagsRecord<string, string | boolean>none
autoGrantPermissionsWindowsWebView2Permission[][]
iconstringnone

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.

Linux

FieldTypeDefault
bundleCEFbooleanfalse
bundleWGPUbooleanfalse
defaultRenderer"native" | "cef""native"
chromiumFlagsRecord<string, string | boolean>none
iconstringnone
flatpakFlatpakConfigdisabled

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;

Chromium Flags

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.

Runtime Values

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);

Build Hooks

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

HookTiming
preBuildBefore any application build work.
postBuildAfter main process, views, copied assets, and Carrot output are built.
postWrapAfter a non-dev release wrapper is assembled, before final packaging/signing.
postPackageAfter 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 Configuration

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;

Carrot Packaging

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.