Runtime modes
Portable (web) vs executable (Chroma Desktop) — same APIs and plugins, different compile targets and host behavior.
Chroma apps are always built from the same source: .chr components, chroma.json, and optional plugins. What changes is where the bundle runs and which host capabilities are available. The docs call those two deployment states Portable (Web) and Executable (Chroma Desktop).
The important idea: hooks, plugins, and public APIs are often callable from every entry point (browser tab, chroma dev, imported zip, static host). That does not mean they behave the same inside. Storage, origins, reload, and some plugin paths differ by runtime mode. Design and test against the modes you ship.
At a glance
| Portable (Web) | Executable (Chroma Desktop) | |
|---|---|---|
| What you ship | dist/ (HTML, JS bundle, WASM) on any static host or CDN | Same dist/ (often as a .zip) plus the Chroma Desktop native app on the user's machine |
| Engine | Rust → WebAssembly in the browser | Same WASM bundle, loaded in an iframe served over loopback HTTP |
| Typical dev | chroma dev --no-desktop → browser tab | chroma dev with dev.desktop: true → Desktop develop window |
| Production open | User visits your URL | User installs Desktop and imports your zip (or you distribute a custom Tauri build) |
| Chroma-Fs default | IndexedDB in the browser profile | Filesystem bridge under app data when configured for desktop |
| Live reload | Yes in the browser (Live reload) | Develop mode does not auto-reload; restart chroma dev or re-import after chroma build |
Both modes use the same chroma build output for dynamic apps. Portable mode is the usual choice for public sites; Executable mode is for offline-first or org-internal apps that should feel like a native program without maintaining a separate UI stack.
Portable (Web)
Portable means the app runs as a normal web application: the browser downloads your HTML, JavaScript bundle, and chroma_bg.wasm, then executes them in a tab or embedded WebView you do not control.
How you get there
- Develop with
chroma dev --no-desktop(ordev.desktop: falseinchroma.json). - Run
chroma validateandchroma build. - Upload
dist/to static hosting or serve it with any HTTP server (Server reference).
The engine is compiled for the web WASM target (wasm-pack build --target web). The loader in pkg/chroma.js is the only JavaScript the engine ships; your app code is bundled beside it.
What behaves "web-native"
- DOM and events — full browser APIs in the top-level page (no parent shell).
- Plugins — browser ESM plugins load from
plugins/loader.js; no Node.js at runtime. - API router —
@chroma/routerinterceptsfetchin the page; handlers underapi/run locally (Routing). - Storage — Chroma-Fs uses IndexedDB unless you explicitly choose a backend that requires a host (see Backends).
- Origins — cookies,
localStorage, and IndexedDB are keyed to your deployment URL. Changing host or port starts a fresh storage partition.
Example: a counter app works identically in Portable mode; only deployment and persistence policies change.
// src/App.chr — same in both modes
import { hookState, e } from "../pkg/chroma.js";
export function App() {
const [n, setN] = hookState(0);
return e("button", { onClick: () => setN(n() + 1) }, "Count: ", n);
}Executable (Chroma Desktop)
Executable here means the Chroma Desktop application (chroma-desktop): a Tauri-based native shell that hosts your same WASM bundle. The UI is still web technology; the executable is the installer users run on Linux or Windows.
Desktop is not a second framework. It is a loopback server plus window management, zip import, and bridges (for example Chroma-Fs over postMessage). Details: Desktop guide.
Two ways Desktop runs your app
| Mode | When | URL shape |
|---|---|---|
| Develop | chroma dev with dev.desktop: true | Dev server origin, e.g. http://127.0.0.1:<dev.port>/... |
| Library (imported zip) | After chroma build and import | http://127.0.0.1:47823/<app-id>/index.html by default |
The iframe cannot call Tauri APIs directly. Plugins that need disk access use the Desktop bridge instead of IndexedDB when configured for filesystem storage.
What behaves "desktop-native"
- Real files — with
backend: "filesystem", data lands under<app-data>/apps/<app-id>/data/(Desktop storage). - Stable library origin — Desktop tries to bind port
47823so IndexedDB for imported apps survives restarts; if the port is busy, the origin may change (troubleshooting). - Develop vs browser — IndexedDB in a browser dev tab and in Desktop develop mode are different origins; do not assume shared data.
- Security model — loopback-only serving and zip path checks; treat imported bundles like any native program (Security notes).
chroma install can place a chroma-desktop binary on your PATH for develop mode. End users can install Desktop from the installers in the Desktop guide without the full CLI.
Same API, different internals
Use this mental model when reading plugin and storage docs:
| Concern | Portable (Web) | Executable (Desktop) |
|---|---|---|
loadPlugins() | Runs in the page | Runs in the iframe; same loader contract |
@chroma/style | injectStyles + bootstrapStyle in the page; CSS via <link> or plugin URLs | Same CSS files in dist/plugins/chroma-style/; loopback origin only |
@chroma/fs writeFile | Often IndexedDB | Often filesystem bridge when backend: "filesystem" |
chroma dev reload | Browser reloads on save | Develop window does not; use --no-desktop for fast UI iteration |
Screen router (createRouter in WASM) | Hash/history in the tab | Same in the iframe |
| API router | Simulated endpoints in-page | Same simulation; still no real server in production unless you add one |
If a feature "works in Desktop," confirm it in a browser build (or the opposite) before release. Document mode-specific persistence in your own app, since storage differs between the two modes.
Choosing a mode
| Goal | Recommended mode |
|---|---|
| Public website, SSO, SEO, widest reach | Portable (Web) |
| Internal tool, offline zip, consistent file paths on disk | Executable (Desktop) |
| Fastest edit–refresh loop during UI work | Portable dev (--no-desktop) even if you ship Desktop |
| One codebase, both targets | Single chroma build; test Portable hosting and Desktop import |
Set dev.desktop in chroma.json to match how you prefer to develop; it does not change the production artifact. See dev.desktop in the manifest reference.
Experimental mobile shells wrap the same Portable build; they are not production-ready. See Desktop — experimental mobile and Limitations.
Related
- Extensions & plugins by mode — per-plugin and editor-tool compatibility matrices
- CLI — install,
chroma dev,chroma build, compile pipeline - Server — deploy
dist/(Portable) vs Desktop loopback serving - Desktop — Executable host: import, develop, storage
- Quick start — five-step path to a built app
- Architecture — WASM engine and toolchain map
- Chroma-Fs backends — IndexedDB vs filesystem selection
- Plugins — browser-side loading (both hosts)
- Chroma-Style · Chroma-t2t — optional plugins (host-specific notes on each page)
- VS Code extension — editor tooling (not tied to either runtime mode)
- Glossary — Portable and Executable terms