The template, toolchain, and documentation for shipping a server-less web app as a desktop app.
Bun, TypeScript, optional native Rust extensions, and operating-system WebViews. One window, one executable, no Chromium.
Crumb is a template you clone. It gives you a native window, an embedded offline document, a narrow validated bridge between your page and a Bun host, first-class support for application-owned Rust extensions, and a build that compiles all of it into one self-contained executable per platform.
If you can build your app as a web page that needs no server, Crumb turns it into a desktop application — without shipping Chromium, without starting a local web server, and without asking anyone to install Bun or Node.js beside the finished binary.
When TypeScript is not enough, declare a Rust crate by logical name in your application config and import it from trusted host code as app:ext/<name>. The normal bun run dev and bun run build commands compile, cache, load, watch, and embed it. There is no separate native packaging workflow and no adjacent .node file or Rust toolchain beside the finished executable.
The repository includes three worked applications: file-explorer shows the TypeScript-only path, activity-monitor shows the same ordinary web-app structure gaining real operating-system access from an application-owned Rust extension, and crumbbrot uses a multi-core Rust extension for CPU-intensive fractal rendering. All three live under examples/, separate from the starter you edit.
Crumb is built on Bun and is, in passing, a practical exploration of Bun as an application toolchain: package management, direct TypeScript execution, browser bundling, automated testing, Node-API compatibility, Cargo orchestration for Rust extensions, and compilation into standalone executables.
Important
Linux x64 on native Wayland and macOS arm64 are verified. X11 and XWayland are not supported. The executables Crumb produces are currently unsigned and are not packaged as installers or macOS .app bundles.
- One resizable native window using the operating system's own WebView — no bundled Chromium
- Your HTML, CSS, and JavaScript embedded in the executable, loaded without a local server, a network request, or a lookup in the source tree
- A narrow bridge between page and host: only the operations your app declares are reachable, each validated at runtime and each returning a serializable result or a normalized error
- A restrictive Content Security Policy by default, blocking remote connections, frames, object embedding, forms, and unintended navigation
- Optional Rust extensions declared by logical name, automatically compiled with Cargo, watched during development, cached safely per source and target, and imported through stable
app:ext/<name>modules - A release build that bundles the UI and compiles it, your host code, every declared Rust extension, and the Bun runtime into one self-contained executable per target
- An executable that runs from an empty directory with no Bun, Node.js, npm, Rust toolchain, source tree, adjacent native addon, or other application file
- No telemetry, settings database, or persistent application state of any kind
A view-only three-pane file browser, shipped in examples/file-explorer/. It exists to prove the template end to end and to give you working code to read. None of it is part of what Crumb promises, and none of it is in your way — it is a separate application, not the starting point.
bun run dev --example=file-explorer- Three-pane layout for locations, directory contents, and previews
- Home, root, common folders, and mounted-location discovery
- Back, Forward, parent-directory, and sidebar navigation
- Hidden-file toggle with clear on/off state
- Natural, case-insensitive sorting with directories first
- Text, image, directory, and generic metadata previews
- Symlink and broken-link information
- Light and dark appearance
- Keyboard navigation and accessible pane separators
- Recoverable handling of missing files, removed volumes, and permission errors
- No file creation, editing, deletion, renaming, moving, or shell execution
That last point is the example's own choice, not a limit Crumb imposes. file-explorer restricts itself to inspection and bounded reads because that keeps a file browser simple and auditable, and bun run verify:readonly enforces it. An app you build on Crumb may read and write freely.
file-explorer example on macOS
The file-explorer example on Linux
examples/activity-monitor/ is the complete worked example for Crumb's Rust path. Its HTML, CSS, and TypeScript remain a conventional client-side interface; an application-owned system-monitor crate uses sysinfo and napi-rs to collect live system and process information asynchronously. Trusted host handlers import it as app:ext/system-monitor, normalize its structured output, and expose only three validated, read-only operations to the page.
bun run dev --example=activity-monitorThe example shows CPU and memory totals, load averages, a sortable process list, and per-process detail. Sampling runs outside the window event loop, overlapping refreshes are prevented, shutdown cancels in-flight work, unavailable metrics remain visibly unavailable, and process names are inserted with textContent. There is deliberately no operation that kills, suspends, reprioritizes, or otherwise acts on a process.
Building this example requires stable Rust and Cargo; rustup is the recommended installer. Its native addon, embedded standalone build, and executable-only relocation journey are verified on Linux x64 and macOS arm64.
Theactivity-monitor example on macOS
The activity-monitor example on Linux
examples/crumbbrot/ is an interactive Mandelbrot and Julia-set explorer. Its ordinary HTML, CSS, and TypeScript interface handles the canvas, controls, zooming, panning, resizing, and stale-render coordination. A declared renderFractal operation validates each viewport request before trusted host code calls the application-owned fractal-renderer Rust extension. The extension performs each coarse-grained RGBA render asynchronously so computation does not block the window.
Each render is also spread across every CPU core the machine offers. The extension splits the image into rows and hands them to a pool of scoped threads sized by std::thread::available_parallelism, drawing on nothing beyond the standard library. Workers claim one row at a time rather than taking fixed blocks, because rows crossing the interior of the set run to the full iteration limit while rows outside it escape after a few steps; a static split would strand most workers on cheap regions while one thread finished the expensive middle. The cancellation generation is re-checked before each row is claimed, so shutdown still interrupts a render promptly. On a 16-core Linux x64 machine this renders roughly ten times faster than the equivalent single-threaded loop, taking a 1600×1200 deep zoom at the 2,000-iteration limit from about 4.5 seconds to about 0.43 seconds. The crate's tests assert that the parallel output is byte-identical to a serial reference render for both fractal modes.
bun run dev --example=crumbbrotCrumbbrot was created as the end-to-end exercise for Crumb's three shipped agent skills: crumb-new-application, crumb-add-operation, and crumb-add-native-extension. The assistant followed specs/crumbot-skill-demo.md as its application guide, then corrected the skills where the exercise exposed incomplete guidance. It is therefore both a native-extension example and a concrete demonstration that the skills can carry an application from specification through registration, validated operation, Rust crate, tests, and standalone build.
The example supports cursor-centered zoom, drag-to-pan navigation, bounded iteration controls, configurable Julia parameters, coalesced render requests, obsolete-result rejection, and shutdown cancellation. Its addon and standalone executable have been built and exercised on both Linux x64 and macOS arm64.
Thecrumbbrot example on macOS
The crumbbrot example on Linux
- Bun 1.4 or newer
- A supported native WebView stack
- Apple Silicon with macOS 13 or newer, or Ubuntu 26.04 x64 with a native Wayland session
- A stable Rust toolchain with
rustcand Cargo for clean Linux builds and whenever an application declares a Rust extension; rustup is the recommended installer
Install dependencies and run:
bun install
bun run devA window opens showing the starter — a minimal application in src/app/: one page, one declared operation, and a button that calls the Bun host. It is about forty lines you can read in full before changing anything, and it is what you edit to build your own app.
For a step-by-step walkthrough of the application structure, browser-to-host bridge, interface structure, development loop, and standalone release build, see How to build a desktop app with Bun. Rust extensions continue in Rust native extensions.
To see the complete Rust path — declaration, automatic Cargo build, logical import, host operation, and embedded release fixture — run:
bun run dev --example=native-probeThis requires the Rust and platform linker setup documented under Rust native extensions.
To see something substantial, run the worked example:
bun run dev --example=file-explorerThat is the three-pane file browser in the screenshots below. It lives in examples/file-explorer/ and you never have to delete it — it is a separate application, not something in your way.
To see the substantial native-extension example, run:
bun run dev --example=activity-monitorThe first run compiles its Rust crate. Later runs reuse the verified cached addon until its Rust sources or lockfile change.
To run the skill-built fractal example, use:
bun run dev --example=crumbbrotDrag the canvas to pan, use the wheel or trackpad to zoom toward the pointer, and switch between Mandelbrot and Julia modes from the toolbar.
If you already have a web application in its own repository, you do not have to move it into a Crumb clone. From a Crumb clone, run:
bun run extract -- --dest /path/to/your-projectThis does two things: it stages everything template-owned into your-project/crumb-source/ (the kit, the full build and development pipeline, the native binding patch, main.ts, tsconfig.json, the Crumb agent skills, and the template docs/), and it installs the Crumb skills into your-project/.claude/skills/ (and .codex/, .github/). Outside crumb-source/ it only ever adds those crumb- skill directories — it never modifies an existing file and never touches the clone. --dry-run previews; --force re-stages over an existing crumb-source/.
Then open your coding assistant in the project and ask it to adopt Crumb. The crumb-adopt-existing-project skill is already installed; it assesses the project and applies crumb-source/ — merging the tree into place, reconciling package.json and tsconfig.json, shaping your interface into src/app/, and reaching bun run dev — or proposes a migration, or explains why the project will not fit. crumb-source/MERGE.md has the same steps as a manual checklist. Once applied, every Crumb command works locally with nothing referring back to the clone.
Brainflip — a Concentration-style memory game that shipped with Node/Express and Python/Flask backends — was migrated to Crumb this way. The whole migration ran automatically in Claude Code (Opus 5): bun run extract against the Brainflip checkout, then a single "adopt Crumb into this project" instruction. The assistant assessed the project, merged the pipeline (renaming the Express scripts), replaced the /api/scores REST calls with two validated host operations backed by a local file, embedded the card art, vendored Tailwind, and reached a running bun run dev and a standalone executable — with no hand edits. The full walkthrough is specs/migrate-brainflip-to-crumb.md.
Bun is more than the runtime your app happens to sit on — it is the whole toolchain Crumb is built from. Every stage below is Bun doing a job that would otherwise need a separate tool:
| Bun feature | How the toolchain uses it |
|---|---|
| Package manager | bun install installs the pinned JavaScript dependencies from bun.lock |
| TypeScript runtime | Development and build scripts run directly from .ts files |
| Bundler | Bun.build() bundles the browser UI into one embedded HTML artifact |
| Executable compiler | Bun.build({ compile: ... }) produces a standalone host executable containing Bun, the UI, and your application code |
| Test runner | bun test runs the template's platform and validation tests alongside the example's filesystem, preview, and state tests |
| Web and runtime APIs | fetch, Bun.file, Bun.write, and Bun.CryptoHasher download, store, and verify native source |
| Process API | Bun.spawn runs tar, patch, and Cargo while preserving their exit status |
| Node-API compatibility | The Bun host loads the compiled native WebView addon directly |
Both Linux entry paths call the same buildNativeAddon() function:
bun run dev
└── scripts/dev.ts
└── buildNativeAddon()
bun run build --target=linux-x64
└── scripts/build.ts
└── buildNativeAddon()
On a clean clone, .build/ is absent because it is ignored by Git. Bun therefore performs this sequence:
- Download the source archive for one pinned
@nativewindow/webviewcommit. - Calculate its SHA-256 digest with
Bun.CryptoHasher. - Stop if the digest does not match the value pinned in
scripts/build-native.ts. - Extract a fresh source tree.
- Apply
native/nativewindow-webview-v1.0.6-wayland.patchusingpatch --fuzz=0. - Run
cargo build --release --locked. - Store the resulting Node-API addon in
.build/. - Load that addon for development or embed it in the standalone Linux executable.
Any download, checksum, patch, or compilation failure stops the command. The build does not fall back to the unpatched published Linux addon.
The generated addon is cached after a successful build. If .build/nativewindow-webview-v1.0.6/native-window.linux-x64-gnu.node already exists, the current script trusts it and skips rebuilding. A normal Git clone cannot inherit that file, but a copied working directory can. Remove the generated addon before running bun run build:native when you need to force a clean native rebuild.
Crumb supports Apple Silicon (arm64) on macOS 13 or newer and uses the WKWebView included with macOS. A TypeScript-only application needs no separate WebView runtime, Rust toolchain, C compiler, Xcode, or Homebrew package for ordinary development or release builds. The verified host is macOS 26.5.2 arm64 with Bun 1.4.0; the generated executable declares macOS 13.0 as its minimum deployment version.
Declaring a Rust extension adds build-time requirements: a stable Rust toolchain containing rustc and Cargo, plus Apple Command Line Tools for the linker, SDK, install_name_tool, and release inspection. rustup is the recommended way to install and update the Rust toolchain; Homebrew is not required. Install both toolchains, follow the rustup prompts, and verify them:
xcode-select --install
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
. "$HOME/.cargo/env"
rustup default stable
rustc --version
cargo --versionThe probe was verified with Rust 1.97.1 and Command Line Tools at /Library/Developer/CommandLineTools; full Xcode is not required.
Confirm the machine and runtime before installing dependencies:
uname -m
sw_vers -productVersion
bun --versionuname -m must print arm64. Then install the pinned packages and launch the development build:
bun install
bun run devDevelopment loads the prebuilt @nativewindow/webview-darwin-arm64 binding from node_modules; the Linux-only native patch and GTK packages are not involved. Close the window to stop the development process.
The file-explorer example discovers Home, existing common directories, root, and accessible children of /Volumes. macOS may restrict Desktop, Documents, Downloads, removable volumes, or other protected locations. The example reports those failures without requesting elevated privileges. If access is desired, grant it to the terminal application used to launch it under System Settings → Privacy & Security → Files and Folders (or Full Disk Access only when intentionally required).
The initial supported Linux distribution is Ubuntu 26.04 x64 with GTK 3, WebKitGTK 4.1, and native Wayland. Other distributions may work when they provide equivalent libraries, but are not part of the verified support matrix.
Install the runtime and build dependencies on Ubuntu:
sudo apt install \
build-essential \
curl \
libgtk-3-dev \
libjavascriptcoregtk-4.1-0 \
libwebkit2gtk-4.1-dev \
patch \
pkg-configInstall the stable Rust toolchain with rustup rather than depending on the distribution's potentially older cargo and rustc packages:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
. "$HOME/.cargo/env"
rustup default stable
rustc --version
cargo --versionRust is required for a clean Linux build because Crumb compiles its pinned native Wayland binding; the same toolchain also builds any application-declared extensions. Crumb forces GDK_BACKEND=wayland internally. It will not fall back to X11 or XWayland. Distribution GTK and WebKitGTK packages may still contain dormant links to X11 compatibility libraries; those links are not used as Crumb's display backend.
Release artifacts must currently be built on their target operating system.
The artifact is named after the application being built — bun run build produces dist/starter-<target>, and --example=file-explorer produces dist/file-explorer-<target>. Pass --output=<name> to choose the stem yourself.
bun run build --target=macos-arm64 # dist/starter-macos-arm64
bun run build --example=file-explorer --target=macos-arm64 # dist/file-explorer-macos-arm64
bun run build --example=activity-monitor --target=macos-arm64 # dist/activity-monitor-macos-arm64
bun run build --example=file-explorer --output=crumb --target=macos-arm64Every registered application builds the same way; substitute its registry key.
activity-monitor declares a Rust extension, so its build additionally
validates the declaration, compiles the crate if it is missing or stale, and
reports any non-system dynamic dependency the crate introduced. That is the only
difference in the command you run.
bun run build --example=file-explorer --target=linux-x64
./dist/file-explorer-linux-x64
bun run build --example=activity-monitor --target=linux-x64
ldd dist/activity-monitor-linux-x64
./dist/activity-monitor-linux-x64bun run build --example=file-explorer --target=macos-arm64
file dist/file-explorer-macos-arm64
otool -L dist/file-explorer-macos-arm64
./dist/file-explorer-macos-arm64
bun run build --example=activity-monitor --target=macos-arm64
file dist/activity-monitor-macos-arm64
otool -L dist/activity-monitor-macos-arm64
./dist/activity-monitor-macos-arm64The expected file result is a 64-bit arm64 Mach-O executable. otool -L must list only macOS system libraries; the embedded native addon uses the system WebKit, AppKit, and related Apple frameworks. This is the same for both applications: on the verification host, file-explorer and activity-monitor reported identical dependency lists — libicucore, libresolv, libc++, and libSystem — so the system-monitor crate added no dynamic dependency of its own, matching the no non-system dynamic dependencies line the build reports. otool is a release-inspection tool supplied by Apple Command Line Tools (xcode-select --install), but those tools are not required merely to build or run Crumb.
To verify that no application files are required beside the executable:
CRUMB_CHECK_DIR="$(mktemp -d)"
cp dist/file-explorer-macos-arm64 dist/activity-monitor-macos-arm64 "$CRUMB_CHECK_DIR/"
cd "$CRUMB_CHECK_DIR"
./file-explorer-macos-arm64
./activity-monitor-macos-arm64Both relocated executables have been verified from an otherwise empty directory without Bun, Node.js, npm, the repository, or network access in its runtime path. file-explorer is approximately 62 MiB and activity-monitor approximately 63 MiB, because each embeds Bun, the host, the UI, the native binding, and — for activity-monitor — the declared Rust extension.
For an application that declares an extension, exercise an operation that actually calls it: activity-monitor must display a real system snapshot and process list. Opening the window proves the executable relocated; it does not prove the embedded addon loaded.
The raw executable is linker-signed ad hoc, not signed with an Apple Developer ID, and not notarized. A locally built executable runs normally. If a downloaded copy is quarantined, the safest option is to build it from source; otherwise, verify its provenance and use System Settings → Privacy & Security → Open Anyway if macOS offers that control. Never run it with sudo.
The resulting executable contains the Bun runtime, host code, browser UI, CSS, and application-owned assets. It does not require Bun, Node.js, the source tree, or adjacent application files at runtime. Native system WebView libraries remain operating-system dependencies.
These instructions describe the example application, not the template.
- Select a location in the left pane to open it.
- Select a directory entry once to preview it.
- Double-click a directory, or select it and press Enter, to open it.
- Press Escape to clear the current selection.
- Use the Hidden: On/Off button to show or hide dotfiles.
- Drag either separator to resize its adjacent panes. Focused separators also respond to the arrow keys.
| Action | Linux | macOS |
|---|---|---|
| Move through entries | Up / Down |
Up / Down |
| First or last entry | Home / End |
Home / End |
| Open selected directory | Enter |
Enter |
| Clear selection | Escape |
Escape |
| Parent directory | Ctrl + Up |
Command + Up |
| Back | Ctrl + [ |
Command + [ |
| Forward | Ctrl + ] |
Command + ] |
| Toggle hidden files | Ctrl + Shift + . |
Command + Shift + . |
The example treats names, paths, metadata, and file contents as untrusted data.
| Content | Behavior |
|---|---|
| Directories | Shows metadata and a bounded direct-child count; never calculates recursive size |
| Text | Reads at most 1 MiB and renders it as inert text |
| Unknown files | Samples at most 8 KiB to distinguish text from binary content |
| PNG, JPEG, GIF, WebP | Validates the file signature and previews files up to 25 MiB |
| SVG, HTML, JavaScript | Never executes content; SVG receives a generic metadata preview |
| Unsupported files | Shows available metadata without interpreting the complete file |
Directory listings contain at most 50,000 entries and never recurse automatically.
Every Crumb application has two layers — the template supplies the outer three rows, your code supplies the handlers:
Native window and WebView
└── Embedded HTML, CSS, and browser TypeScript
└── Validated JSON RPC
└── Bun host
├── Location discovery
├── Directory inspection
├── Bounded preview generation
└── Native window lifecycle
The WebView can call only the operations declared in app.config.ts — nothing else is reachable, and every input is validated before a handler runs. An operation absent from that table has no route to a handler. There is no generic filesystem binding and no eval-style bridge. The embedded document also uses a restrictive Content Security Policy that blocks external connections, frames, objects, forms, and unintended scripts.
The file-explorer example declares four operations:
getPlatformInfogetLocationslistDirectorygetPreview
All four are read-only, and bun run verify:readonly statically checks the file-explorer's TypeScript under examples/file-explorer/src/ for mutation, whole-file generic reads, and shell execution. It scans host-language source only. It does not inspect Rust or make any claim about a native extension, so an application combining a read-only policy with an extension must review that native code separately. The check belongs to the example; your application declares its own operations in app.config.ts and is not bound by it.
An application can declare an application-owned Rust Node-API cdylib by logical name. The complete walkthrough is in Rust native extensions.
Install a stable Rust toolchain with rustup and make sure both commands resolve in the terminal that runs Bun:
rustc --version
cargo --versionPut a starter application's crate under src/app/native/<name>/. A registered application under examples/<app>/ normally owns crates under examples/<app>/native/<name>/. The crate must have a valid Cargo.toml, produce a cdylib, and commit a Cargo.lock because Crumb invokes cargo build --release --locked.
[package]
name = "my-extension"
version = "0.1.0"
edition = "2024"
[lib]
name = "my_extension"
crate-type = ["cdylib"]The library must expose Node-API exports that Bun can load. A plain Rust cdylib with no Node-API module initializer will compile but cannot be imported as an extension. Start from the dependency-free reference files in src/app/native/probe/, including its macOS linker setup in build.rs, then replace the example answer() export with your implementation. After changing dependencies, regenerate and commit the lockfile:
cargo generate-lockfile --manifest-path src/app/native/my-extension/Cargo.tomlDeclare source intent in the application's ApplicationConfig. Paths are relative to the repository root; do not put .node, .so, .dylib, a target name, or an output path in the declaration. Crumb derives and verifies all target-specific artifact locations.
export const application: ApplicationConfig = {
// ...window, CSP, entries, and operations...
nativeExtensions: {
"system-info": "src/app/native/my-extension",
},
};Logical names must start with a lowercase letter and contain only lowercase letters, digits, -, or _. Import the declared extension from trusted host code with the same logical module on every platform. Check its runtime export and normalize the result before returning it through a declared operation:
import systemInfo from "app:ext/system-info";
const readSystemInfo = systemInfo.readSystemInfo;
if (typeof readSystemInfo !== "function") {
throw new Error('Native extension "system-info" does not export readSystemInfo()');
}
const result = readSystemInfo();The permanent native-probe fixture demonstrates the smallest dependency-free shape in examples/native-probe/ and src/app/native/probe/. The activity-monitor example demonstrates the production-shaped path: napi-rs objects, asynchronous tasks, a third-party Rust dependency, repeated calls, shutdown cancellation, structured normalization, and a complete UI.
The ordinary commands own the native build; do not run cargo build as a separate prerequisite. For the default starter:
bun run dev
bun run rebuild:extensions
bun run build --target=macos-arm64 # run on Apple Silicon macOS
bun run build --target=linux-x64 # run on Linux x64For a named registered application, add --example=<app> to the Crumb commands. Cross-compilation is not supported: build each release on its target operating system. Release builds embed every declared extension in the standalone executable and report non-system dynamic libraries introduced by an extension.
Extension source is watched during bun run dev. A native change compiles the crate and restarts the host process; native code is never replaced inside a running process. UI-only changes reuse the verified artifact. A successful artifact is accepted only when its source fingerprint, extension name, target, architecture, and digest match its provenance metadata. Use bun run rebuild:extensions --example=<app> for a forced clean extension rebuild after changing toolchains, linker settings, or dependencies.
Native extensions are trusted host code. They run in the Bun process with its full operating-system permissions, can read or write files, use the network, and crash or terminate the application. They do not create a WebView-native bridge: page input still reaches native code only through declared operations and their validators.
Register cleanup with registerShutdownHandler from src/kit/host/shutdown.ts. Handlers run once in registration order when the window closes. Failures are reported without skipping later handlers; the whole shutdown phase is bounded to 3 seconds, after which the incomplete handler is named and the process exits anyway.
Measured on the verified macOS arm64 host, the dependency-free probe took 2.12 seconds for a cold release compile, 0.61 seconds for an incremental forced compile, and 1.4 milliseconds for a verified warm-cache lookup. Real extensions with dependencies will cost more. The release build reports non-system dynamic dependencies; the probe adds none.
On the Linux x64 implementation host, the first optimized activity-monitor extension compile with sysinfo and napi-rs took 8.2 seconds. A verified warm-cache lookup took 2.2 milliseconds and the subsequent selected-application build completed in 0.18 seconds. On macOS arm64, the first dependency-fetching compile took 11.14 seconds; with dependency sources cached and extension targets removed, all four registered applications built in 7.90 seconds, including 6.32 seconds for activity-monitor's native compile. Its verified warm-cache lookup took 1.5 milliseconds and selected-application build took 0.17 seconds. These are observations, not performance guarantees.
bun run dev enables the WebView's developer tools so you can inspect the page you are building. Release builds disable them, and no application configuration can turn them back on — the flag comes from the launch path, not from app.config.ts, so a released executable cannot ship an inspectable WebView.
The published @nativewindow/webview 1.0.6 Linux addon attaches WebKitGTK to Tao's already-occupied top-level GTK window, producing a gray window. Crumb builds a checksum-pinned minimal fork that attaches the WebView to Tao's default content box instead.
The patch is stored at native/nativewindow-webview-v1.0.6-wayland.patch. The build verifies the pinned upstream archive before applying the patch with zero fuzz.
| Command | Purpose |
|---|---|
bun run dev |
Build and launch the default application, watching for changes |
bun run dev --example=file-explorer |
Run a named application instead |
bun run dev --example=activity-monitor |
Run the Rust-backed activity monitor example |
bun run dev --example=crumbbrot |
Run the skill-built Rust fractal explorer |
bun run dev --no-watch |
Run once without watching |
bun run build:native |
Build the patched Linux native addon |
bun run rebuild:extensions --example=<name> |
Clean and rebuild a selected application's declared extensions |
bun run build:ui |
Build dist/ui.html |
bun run build --target=linux-x64 |
Build the Linux standalone executable |
bun run build --example=<name> --target=<target> |
Build a named application; the artifact is named after it |
bun run build --output=<name> … |
Override the artifact's filename stem |
bun run build --target=macos-arm64 |
Build the macOS standalone executable |
bun test |
Run the automated test suite |
bun run typecheck |
Run strict TypeScript checks |
bun run verify:performance |
Run bounded host and native 5,000-row UI performance checks |
bun run verify:readonly |
Verify the production read-only boundary |
bun run install:skills |
Install the template's agent skills for every supported assistant |
bun run install:skills --check |
Fail if an installed skill copy has drifted from skills/ |
bun run extract -- --dest <path> |
Stage the template-owned machinery into an existing project's crumb-source/ |
src/kit/ The template: window bootstrap, RPC router and browser bridge,
validation primitives, platform detection, transport types
src/app/ The starter — a minimal application. This is what you edit.
examples/
file-explorer/ TypeScript-only worked example, with its own config and tests
activity-monitor/ Rust native-extension worked example and system-monitor crate
crumbbrot/ Skill-built fractal example and fractal-renderer crate
native-probe/ Permanent end-to-end fixture for native extensions
app.config.ts Registry of available applications and which one is default
main.ts Release entry point: hands an application to the kit
scripts/ Development supervisor, runner, native, UI, and build scripts
native/ Minimal pinned native binding patch
test/kit/ Template tests: platform, validation, RPC surface, selection,
and the release developer-tools invariant
docs/ Build walkthrough, native extensions, runtime, and verification notes
openspec/ Change proposals, design, requirements, and task tracking
skills/ Canonical, vendor-neutral agent skills; installed copies are generated
images/ Crumb artwork
Nothing under src/kit/ imports from src/app/, and the kit names no operation. Moving src/app/ aside leaves the kit typechecking cleanly — that is a test, not an aspiration.
Crumb ships skills for coding assistants, covering the places where a partially correct result still builds and runs: declaring a host operation, adding a Rust native extension, registering a second application, and adopting Crumb into an existing web-app project.
skills/ is the canonical source and names no vendor. Install the
copies each assistant reads with one command:
bun run install:skills # Claude, Codex, and Copilot
bun run install:skills --target=claude # one assistant
bun run install:skills --list # what a run would write, writing nothing
bun run install:skills --check # fail if an installed copy has driftedThe copies are committed, so a fresh clone works with an assistant before anyone
runs anything; --check is what keeps them honest. The installer only creates
and replaces the skill names skills/ contains — it never touches anything else
in those directories, including the skills the OpenSpec CLI writes there, and it
does not remove a skill you delete from skills/.
A skill is a shortcut to the documented workflow, not a second specification of
it. Where a skill and a requirement under openspec/specs/ disagree, the
requirement governs.
These local commands are the per-change check. Automated verification runs only when a version tag is published — a run compiles the Linux native addon from source and builds every application on both platforms, which is too costly to spend on every push.
Run the complete local checks:
bun test
bun run typecheck
bun run verify:performance
bun run verify:readonlyThe current suite contains 152 automated tests and 450 expectations covering filesystem behavior, validation, previews, navigation state, activity-monitor formatting and refresh coordination, Crumbbrot configuration, validation, viewport mapping and render coordination, supported platforms, shutdown, extension declarations, cache safety, watching, installed agent-skill integrity, and the production capability boundary. The performance command briefly opens a native window and closes it automatically. The activity monitor's Rust tests, TypeScript tests, real-data addon calls, embedded release build, and executable-only relocation journey are verified on macOS arm64 and Ubuntu 26.04 x64/Wayland. Both relocated probe executables returned nativeProbeAnswer: 42; both relocated activity-monitor executables displayed real system snapshots and process lists. Crumbbrot's Linux x64 native renderer returned distinct real Mandelbrot and Julia RGBA buffers, its Rust tests confirm the row-parallel render is byte-identical to a serial reference for both fractal modes, and its relocated executable launched successfully from an otherwise empty directory. Fixtures are created only below the operating system's temporary directory and are removed after each run.
These apply to anything you build with Crumb:
- Windows is not supported.
- Linux requires a native Wayland session; X11 and XWayland are not supported.
- Intel Macs are not supported; the macOS release target is arm64 only.
- Releases must be built on their target operating system.
- Release binaries are currently unsigned and are not packaged as installers or macOS application bundles.
- One window per application; multiple windows are not yet supported.
- Native extensions are source-built on the target operating system; cross-compilation and prebuilt reusable extensions are not supported yet.
These apply only to the file-explorer example and disappear when you delete it:
- Search, file watching, tabs, and custom sorting are out of scope.
- The example does not edit, launch, copy, move, rename, or delete files.
- PDF, audio, video, archives, Office documents, and SVG receive metadata-only previews.
Confirm that the session is using Wayland and that GTK 3 and WebKitGTK 4.1 are installed. If GDK_BACKEND is set, it must be wayland.
echo "$XDG_SESSION_TYPE"
echo "$GDK_BACKEND"Check that rustc, Cargo, a C toolchain, pkg-config, patch, and the GTK/WebKitGTK development packages are installed. If Rust was installed with rustup in the current shell, . "$HOME/.cargo/env" makes its commands available. Then run:
bun run build:nativeFor an application extension, confirm that its Cargo.toml declares crate-type = ["cdylib"], its Cargo.lock is current, and its repository-relative source directory matches nativeExtensions. Force a clean rebuild with:
bun run rebuild:extensions --example=native-probeThe macOS artifact requires Apple Silicon. Confirm that uname -m prints arm64; Intel (x86_64) Macs are not supported.
Preserve or restore the executable bit, then run the binary without sudo:
chmod +x dist/file-explorer-macos-arm64
./dist/file-explorer-macos-arm64If the example opens but a protected folder is unavailable, review the macOS privacy guidance in macOS setup. Filesystem permission errors are recoverable and the example does not change permissions.
Release binaries are not Developer ID-signed or notarized. Prefer building from source. For a binary whose provenance you have independently verified, macOS may provide Open Anyway under System Settings → Privacy & Security.
Run bun install on the Apple Silicon Mac and confirm that this file exists:
file node_modules/@nativewindow/webview-darwin-arm64/native-window.darwin-arm64.nodeIt must be an arm64 Mach-O library. Do not reuse node_modules copied from Linux or an Intel Mac.
file dist/file-explorer-linux-x64
ldd .build/nativewindow-webview-v1.0.6/native-window.linux-x64-gnu.nodeNo dependency should be reported as not found.
Everyone is invited and welcome to contribute: open issues, propose pull requests, share ideas, or help improve documentation. Participation is open to all, regardless of background or viewpoint.
This project follows the FOSS Pluralism Manifesto, which affirms respect for people, freedom to critique ideas, and space for diverse perspectives.
Keep changes aligned with Crumb's core constraints: native WebViews, an embedded offline document, a narrow declared and validated host surface, no local server, no network dependency at runtime, and no X11/XWayland fallback on Linux.
The file-explorer example carries two constraints of its own that Crumb does not impose on you: view-only filesystem access and bounded reads. Keep those intact when changing the example — but do not treat them as rules for an application you build on the template.
Before submitting a change, run the verification commands above and update the relevant OpenSpec artifacts when behavior or requirements change.
Copyright (c) 2026 Iwan van der Kleijn
This project is licensed under the MIT License. See the LICENSE.md file for details.





