Hello World (Odin)
This guide creates a minimal Electrobun app with an Odin main process. It
mirrors the TypeScript walkthrough — same
project shape, same webview, but the window and webview are created from Odin
instead of Cottontail. The faster path is
hutch electrobun init odin-app --template=odin-particles-wgpu.
1. Create the project
mkdir odin-hellocd odin-helloCreate hutch.config.ts. Hutch resolves scripts only from this file; this
project does not need a package.json:
export default { electrobun: { version: "2.0.1-beta.0" }, scripts: { dev: ["hutch", "electrobun", "dev", "--watch"], build: ["hutch", "electrobun", "build", "--env=stable"], },};Create tsconfig.json so editors and TypeScript resolve the config types
projected during sync:
{ "extends": "./.hutch/devkit/tsconfig.json"}2. Add a webview
Create src/mainview/index.html:
<!doctype html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width" /> <title>Hello Electrobun</title> </head> <body> <h1>Hello from Electrobun</h1> </body></html>3. Add the main process
Create src/odin/main.odin. The SDK is imported as
import electrobun "electrobun_sdk:electrobun". Hutch projects the exact
versioned SDK into .hutch/devkit/odin-sdk and passes that directory as the
electrobun_sdk collection when it invokes Odin:
package main
import "core:fmt"import "core:thread"import "core:time"
import electrobun "electrobun_sdk:electrobun"
DEFAULT_SECRET_KEY :: "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32"
g_core: ^electrobun.Coreg_bundle_paths: ^electrobun.BundlePaths
create_ui :: proc() { time.sleep(150 * time.Millisecond)
if err := electrobun.configureWebviewRuntimeFromExecutableDir(g_core, g_bundle_paths, 0); err != .None { fmt.eprintfln("[hello] failed to configure webview runtime: %v", err) return }
window_options := electrobun.defaultWindowOptions("Hello Electrobun") window_options.frame = {x = 160, y = 100, width = 800, height = 600} window_id, window_err := electrobun.createWindow(g_core, window_options) if window_err != .None { fmt.eprintfln("[hello] failed to create window: %v", window_err) return }
webview_options := electrobun.defaultWebviewOptions(window_id) webview_options.url = "views://mainview/index.html" webview_options.frame = {x = 0, y = 0, width = 800, height = 600} webview_options.secret_key = DEFAULT_SECRET_KEY webview_options.sandbox = false webview_options.callbacks = { decide_navigation = electrobun.allowAllNavigation, event = electrobun.noopWebviewEvent, event_bridge = electrobun.noopWebviewPostMessage, }
if _, webview_err := electrobun.createWebview(g_core, webview_options); webview_err != .None { fmt.eprintfln("[hello] failed to create webview: %v", webview_err) _ = electrobun.closeWindow(g_core, window_id) }}
main :: proc() { core, core_err := electrobun.load() if core_err != .None { fmt.eprintfln("[hello] failed to load Electrobun core: %v", core_err) return } defer electrobun.close(&core)
bundle_paths, bundle_err := electrobun.resolveBundlePaths() if bundle_err != .None { fmt.eprintfln("[hello] failed to resolve bundle paths: %v", bundle_err) return } defer electrobun.deinit(&bundle_paths, context.allocator)
owned_app_info, app_info_err := electrobun.resolveAppInfoFromBundle(context.allocator, &bundle_paths) if app_info_err != .None { fmt.eprintfln("[hello] failed to resolve app info: %v", app_info_err) return } defer electrobun.deinit(&owned_app_info, context.allocator) app_info := electrobun.borrowed(owned_app_info)
g_core = &core g_bundle_paths = &bundle_paths defer g_core = nil
thread.create_and_start(create_ui, self_cleanup = true)
// Blocks running the native event loop until the app quits. if err := electrobun.runMainThread(&core, app_info); err != .None { fmt.eprintfln("[hello] main thread exited with error: %v", err) }}4. Configure the build
Create electrobun.config.ts:
import type { ElectrobunConfig } from "electrobun";
export default { app: { name: "odin-hello", identifier: "odinhello.example.dev", version: "0.0.1", }, build: { mainProcess: "odin", odin: { entrypoint: "src/odin/main.odin", }, copy: { "src/mainview/index.html": "views/mainview/index.html", }, },} satisfies ElectrobunConfig;The project owns its Odin source package. Hutch builds the directory containing
the configured entrypoint and passes the projected SDK collection; it does not
generate or require a build.odin file.
5. Run the app
hutch electrobun synchutch run devHutch builds and launches the app, then rebuilds it when watched source files change. Press Ctrl+C to stop it.
How this differs from TypeScript
The Odin SDK is a faithful port of the Zig SDK: camelCase procs over the
native core’s C ABI, taking ^Core as the first argument
(electrobun.createWindow(&core, options)). Some steps Cottontail handles
for you are explicit here:
-
Dynamic core loading.
electrobun.load()openslibElectrobunCore(.dylib/.so/ElectrobunCore.dll) from the executable’s directory at runtime and resolves every C symbol. Nothing is linked at build time. -
App identity comes from the bundle.
resolveBundlePathscomputes the executable andResources/directories, andresolveAppInfoFromBundleparsesResources/version.jsonfor the app’sidentifier,name, andchannel— identity is read at runtime, not baked in from config at compile time. -
configureWebviewRuntimeFromExecutableDirbefore webviews. It loads the bundledpreload-full.js/preload-sandboxed.jsfromResources/and configures the webview runtime. Skip it and your webviews are missing their preload and bridge scripts. -
runMainThreadblocks. It runs the native event loop on the main thread until the app quits, so windows and webviews are created from a worker thread started before it. The template sleeps 150ms first to let the loop start; the core marshals UI calls back to the main thread internally. -
Error handling is Odin-native. Most procs return an
Errorenum (.None,.LibraryLoadFailed,.ElectrobunCoreFailure, …) as a second return value — checkerr != .Noneas in the example. -
Callbacks are C ABI. All Electrobun callbacks are
proc "c"; inside one you must setcontext = runtime.default_context()(frombase:runtime) before calling into Odin runtime features:import "base:runtime"on_window_resize :: proc "c" (window_id: u32, x: f64, y: f64, width: f64, height: f64) {context = runtime.default_context()fmt.printfln("window %d resized to %.0fx%.0f", window_id, width, height)} -
Some TS APIs have no Odin equivalent. There is no typed RPC layer (the bridge is raw JSON via
sendHostMessageToWebviewandpopNextQueuedHostMessage), no updater, and no typed menu builders. See Native main process for the full comparison.
To start from a working Odin project instead of an empty folder:
hutch electrobun init odin-app --template=odin-particles-wgpuFocused Odin graphics templates also exist: odin-fluid-wgpu,
odin-jelly-bunny-wgpu, odin-alchemy-wgpu, and odin-tree-wgpu.
Toolchain
Odin is pre-1.0 and ships monthly dev releases with breaking changes;
Electrobun 2.0.0 defaults to Odin dev-2026-07a and guarantees the SDK only
against that release. Override it exactly with build.odin.version. Hutch uses
a system compiler only on an exact version match; otherwise it downloads the
selected toolchain into
~/.hutch/toolchains/odin/<version>/<platform> and reuses it across projects.
The compiler is not bundled into the finished app.
Platform requirements: macOS needs the Xcode Command Line Tools, Linux needs
clang, and Windows needs Visual Studio Build Tools (MSVC link.exe) plus
the Windows SDK. Native main processes build for the host OS/arch only —
there is no cross-compilation, so use native CI runners for a release matrix.
Windows is x64 only (Windows-on-ARM runs the x64 build under emulation).
Next: Native main process for the runtime comparison and deeper build details, and the main process APIs reference.