Desktop
Import chroma build .zip bundles and run Chroma apps outside the browser with the Chroma Desktop shell.
Chroma Desktop is a Tauri 2 desktop shell for running Chroma apps outside the browser. It imports the .zip bundles produced by chroma build and runs them locally — no browser tab, no chroma dev, no network. It lives in the sibling repo Chroma-Desktop.
Once an app is imported it stays imported across launches, and every imported app is reachable from a small library screen.
Install
Download the installer for your platform — each one is a normal, native installer (no terminal required):
Linux — .AppImage
Portable, no installation step: mark it executable and run it directly.
Linux — .deb
For Debian/Ubuntu-based distros: installs via your system's package manager.
Windows — Setup .exe
NSIS installer, installs for the current user.
Windows — .msi
WiX installer, for MSI-based deployment tooling.
Building from source instead? Chroma Desktop is a Tauri 2 app — from a checkout of Chroma-Desktop, run cargo tauri dev to run it locally, or ./build-installer.sh (Linux/macOS) / .\build-installer.ps1 (Windows) to produce the same installers as above.
Develop mode
When chroma dev runs on a dynamic app (including the standard template) with dev.desktop: true, the CLI launches Chroma Desktop in develop mode instead of opening the browser:
enginestarts in the background ondev.portchroma-desktopopens with--develop --url http://127.0.0.1:<port>/<entry> --title <name>- The library/import screen is skipped — the app iframe loads your dev URL immediately
- Closing Chroma Desktop stops the dev session (
engineis terminated)
Pass chroma dev --no-desktop to serve in the browser only. Develop mode requires chroma install from a workspace that includes Chroma-Desktop (the installer builds chroma-desktop into $CHROMA_HOME/bin).
You can also launch develop mode manually:
chroma-desktop --develop --url http://127.0.0.1:5173/index.html --title "My app"Or via URL query param: ?develop=http://127.0.0.1:5173/index.html&title=My%20App.
Importing an app
There are two ways to bring a chroma build .zip into the library:
- Drag and drop the
.zipfile anywhere onto the window. - Click "Import .zip" on the library screen to open a native file picker.
Import is handled by src/importer.rs:
- Reject anything whose extension isn't
.zip. - Derive a display name from the zip's file stem, sanitize it to filesystem-safe characters (
[A-Za-z0-9_-], everything else becomes-), and pick a unique folder name under the apps directory — appending-2,-3, … on collision. - Extract the zip into that folder. Entry paths are resolved with
enclosed_name()(from thezipcrate), which rejects absolute paths and..traversal, so a malicious zip can't write outside its destination folder. - Require an
index.htmlat the extracted root. If it's missing, the import is rejected and the partial folder is removed. - Write a small
.chroma-desktop.jsonmetadata file ({ "name": "<display name>" }) next to the app so the original display name survives even if the folder id doesn't match it (e.g. after de-duplication).
Every import gets its own folder — even re-importing a bundle with the exact same name creates a fresh, independent copy (name, name-2, name-3, …); nothing is silently overwritten. A successful import opens the app immediately; a failed one shows a toast at the bottom of the window with the error, and the library stays as it was.
For a bundle to be accepted, the zip must contain an index.html at its root — exactly what chroma build produces when package.zip: true is set in chroma.json (dynamic mode). Zipping a project's source directory by hand instead of using chroma build will fail unless index.html happens to sit at the archive root too.
The library
list_apps scans the apps directory for folders containing an index.html, reads back the display name from each folder's .chroma-desktop.json (falling back to the folder id if that file is missing or unreadable), and returns them sorted alphabetically (case-insensitively) by display name.
On launch, Chroma Desktop shows a grid of every previously imported app — an "Import .zip" card is always first — each labeled with its display name and an icon showing its initials. Selecting a card opens that app full-window; a back arrow returns to the library.
Imported apps persist across restarts: they live under the OS's Tauri app-data directory, in an apps subfolder — see Data location below.
Chroma-Fs bridge
Apps in the iframe cannot call Tauri directly. Chroma Desktop listens for postMessage messages on channel chroma-fs and forwards them to scoped Rust fs_* commands. Filesystem storage in develop mode uses app-id develop under <app-data>/apps/develop/data/ — try the standard template file explorer to verify read/write/list. See Chroma-Fs backends.
App view
Opening an app points an <iframe> at that app's local server URL (http://127.0.0.1:<port>/<app-id>/index.html) — full-window, with a back button in the titlebar to return to the library. Assets resolve relative to that same path, exactly as they would under chroma dev or any static host.
The shell loads from Tauri's asset origin (http://tauri.localhost / https://tauri.localhost); the app iframe is a separate origin on loopback HTTP. IndexedDB and other web storage inside the iframe are keyed by that iframe origin, not the shell's.
IndexedDB and app origins
Apps that use Chroma-Fs with backend: "indexeddb" (for example Targ-Apps-Finance) store data in the WebView's IndexedDB for the iframe origin http://127.0.0.1:<port> — the port is part of the origin.
| Mode | Typical iframe URL | IndexedDB origin |
|---|---|---|
| Library (imported zip) | http://127.0.0.1:47823/<app-id>/index.html | http://127.0.0.1:47823 (stable) |
Develop (chroma dev --desktop) | http://127.0.0.1:<dev.port>/<entry> | Same as the dev server (e.g. http://127.0.0.1:5173) |
Browser (chroma dev / static host) | Top-level, no iframe | Dev server port (e.g. http://127.0.0.1:5173) |
Chroma Desktop's bundled server binds 127.0.0.1:47823 by default (CHROMA_DESKTOP_SERVER_PORT in src/server.rs). That keeps the library iframe origin stable across restarts so IndexedDB databases (e.g. targ-apps-finance) persist. If that port is already in use, the server falls back to an OS-assigned ephemeral port and logs a warning — IndexedDB from earlier sessions on the stable port will not be visible until the port is free again.
Browser dev and Chroma Desktop do not share IndexedDB: http://127.0.0.1:5173 (Finance chroma dev) and http://127.0.0.1:47823 (imported zip in Desktop) are different origins. Data exported from one environment must be imported in the other if you need to move it.
Apps with backend: "filesystem" use the Chroma-Fs bridge and Rust fs_* commands instead of IndexedDB; develop mode uses app-id develop under <app-data>/apps/develop/data/.
User Interface Design
The Chroma Desktop interface embodies the Targ Apps brand identity with a minimalist B2B aesthetic:
- Brand color palette (Dark Mode): charcoal background (#262624), warm terracotta accent (#d97757), with crisp foreground text (#c3c0b6) for optimal readability and visual sophistication
- Typography: Grift Variable typeface as the primary font, paired with careful letter-spacing and clear visual hierarchy (24px headers, 13px body text)
- Branding: Targ Apps logo displayed in the titlebar, reinforcing brand presence in every interaction
- Entry animations: Staggered cascade animations on app launch—titlebar fades down (0.6s), library content fades up (0.7s offset 0.1s), then cards flow in with 50ms intervals for a polished, intentional entrance
- Generous spacing: 40px padding in the library view, 16px gap between grid items, creating breathing room and reducing cognitive load
- Smooth interactions: 0.2s transitions on hover states with warm accent highlights, subtle elevation changes (shadows with brand color tints) on cards for tactile feedback
Custom titlebar
tauri.conf.json sets "decorations": false on the main window, so there's no native title bar or window chrome — Chroma Desktop draws its own in src/view/index.html and wires it to the Tauri window API:
- Minimize / maximize / close buttons call
appWindow.minimize(),appWindow.toggleMaximize(), andappWindow.close(). - The titlebar area itself is draggable (
"dragDropEnabled": trueplus thecore:window:allow-start-draggingpermission), so the window can be moved without native decorations. - The titlebar height is 48px with a single accent dot indicator.
The window starts at 1080×720 with a minimum size of 640×480 (tauri.conf.json).
How apps run
Every imported app is served — not opened via file:// — by a small local HTTP server (src/server.rs) that starts automatically when Chroma Desktop launches:
- Binds
127.0.0.1:47823by default so IndexedDB origins stay stable across restarts. If that port is busy, falls back to127.0.0.1:0(ephemeral) with a stderr warning. - Serves the entire apps directory as its root, so every imported app is reachable under its own path prefix:
/<app-id>/.... - One thread per connection,
GET/HEADonly — the same request-handling design as the CLI'senginebinary, detailed on the Server page.
src/server.rs and Chroma-Server's engine are independent implementations of the same design, not the same binary — Chroma Desktop doesn't shell out to engine; the server is a Rust module compiled directly into the Tauri binary, with one difference in scope: engine serves one project directory rooted at whatever --dir was passed, while Chroma Desktop's server always serves the whole apps directory, so it can multiplex every imported app behind one listener using the /<app-id>/ path prefix.
Data location
Imported apps live under the platform's Tauri app-data directory, in an apps subfolder — for example ~/.local/share/com.chroma.desktop/apps on Linux. Each app is its own folder named after its (sanitized, de-duplicated) display name, containing the extracted bundle plus a .chroma-desktop.json metadata file.
Per-app persistent storage for Chroma-Fs lives at <apps>/<app-id>/data/ (e.g. apps/my-app/data/notes/hello.txt).
Security notes
- The bundled server binds
127.0.0.1only and is never exposed to the network — an imported app is reachable only from Chroma Desktop itself. - Zip extraction rejects absolute paths and
..traversal in entries viaenclosed_name(), so an imported bundle can't write outside its own destination folder. - HTTP path resolution applies the same style of root-escape check as extraction: every resolved path is canonicalized (
fs::canonicalize) and checked withstarts_with(root), so a request can't read files outside the apps directory even via..segments or symlink tricks. - The main window's Tauri capability set (
capabilities/main.json) is intentionally narrow:core:defaultplus window close/minimize/toggle-maximize/start-dragging anddialog:allow-open— no filesystem, shell, or HTTP client permissions are granted to the frontend beyond that. tauri.conf.jsonsets an explicit CSP on the shell so the library can embed loopback app iframes, call Tauri IPC, and compile the bundled Chroma WASM (script-srcincludes'wasm-unsafe-eval'):frame-src http://127.0.0.1:*,connect-srcincludeshttp://127.0.0.1:*andipc:. Imported apps run in the iframe with their own origin; the shell CSP does not replace per-app policies inside the iframe document.
The shell CSP only governs the library/titlebar document. It does not sandbox JavaScript inside an imported app beyond loopback-only networking and the filesystem/zip-traversal protections above. Only import bundles you trust, the same way you'd only run a native binary you trust.
Versioning
Chroma Desktop uses semver. The canonical version is Chroma-Desktop/VERSION (currently 0.1.0). It is kept in sync with Cargo.toml and tauri.conf.json by the workspace bump script; build.rs fails the compile if VERSION and Cargo.toml diverge.
The shell titlebar shows the running version (e.g. v0.1.0) on the right side of the drag region, loaded at runtime via the Tauri command get_app_version.
From the TargApps workspace root:
./scripts/bump-chroma-desktop-version.sh # patch
./scripts/bump-chroma-desktop-version.sh minor
./scripts/bump-chroma-desktop-version.sh majorSee also Chroma versioning for the engine/CLI policy.
Building and running
From the Chroma-Desktop repo, with the Tauri CLI installed:
cargo build # compile the Rust side
cargo test # run importer + server unit tests
cargo tauri dev # run the app in dev mode
cargo tauri build # produce a release bundlecargo test exercises both src/importer.rs (zip extraction into a fresh folder, de-duplication when importing the same name twice, rejection of non-.zip files and zips without a root index.html, alphabetical listing) and src/server.rs (rejecting paths that ..-escape the served root).
Project structure
Chroma-Desktop/
├── VERSION # canonical semver (synced to Cargo.toml + tauri.conf.json)
├── src/
│ ├── main.rs # Tauri app setup, commands, drag-and-drop handling
│ ├── importer.rs # zip import + app listing (+ unit tests)
│ ├── server.rs # local static file server (+ unit tests)
│ ├── fs.rs # scoped per-app filesystem commands for Chroma-Fs
│ └── view/index.html # frontend (library + app view, titlebar, fs bridge)
├── pkg/ # vendored Chroma engine build (wasm + JS loader)
├── capabilities/main.json # Tauri permission set for the main window
├── build.rs # tauri-build hook
├── Cargo.toml
└── tauri.conf.jsonTroubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Toast: '<file>' is not a .zip file | A non-.zip file was passed to the file picker. | Import the .zip produced by chroma build with package.zip: true, not a raw project folder. |
Toast: the zip does not contain an index.html at its root | The zip's index.html is nested in a subfolder, or missing entirely. | Rebuild with chroma build — its zip output already has index.html at the archive root — rather than zipping the project source directly. |
| Dropped file does nothing | The drop handler in main.rs only imports paths with a .zip extension; anything else in the drop event is silently skipped. | Use the "Import .zip" file picker instead to see the actual rejection reason. |
| An app looks blank or shows a 404 for its assets | The zip was built for a different base path, or a required file wasn't included in the bundle. | Re-check the chroma build output locally (serve dist/ with chroma dev — see Server — or any static server) before importing; if it works there, re-import, otherwise fix the build first. |
| Re-importing doesn't update an already-open app | Each import is a new, separate folder/app entry — it never overwrites the one you already have. | Re-import to get the new version as a new library entry, then open that one; delete the stale folder from the apps directory if you don't need it. |
| IndexedDB data missing after restart (imported app) | An older build used an ephemeral loopback port (127.0.0.1:0), so each launch had a different origin. | Upgrade to a build with stable port 47823, ensure nothing else binds that port, then re-enter data or import a JSON backup. |
| Finance data in browser but empty in Desktop | Browser dev (http://127.0.0.1:5173) and Desktop library (http://127.0.0.1:47823) are different IndexedDB origins. | Use export/import in Finance Settings, or run chroma dev --desktop to share the dev-server origin. |
Verify IndexedDB (Targ-Apps-Finance)
- Build and import:
cd Targ-Apps-Finance && chroma build, then import the.zipin Chroma Desktop. - Confirm the app iframe URL is
http://127.0.0.1:47823/<app-id>/index.html(DevTools → iframe document → location). - Create an account or transaction, reload the app (back to library → open again). Data should remain.
- Quit Chroma Desktop completely and reopen the same imported app. Data should still be present.
- Optional: DevTools → Application → IndexedDB →
targ-apps-financeon originhttp://127.0.0.1:47823.
Where this fits in the pipeline
Chroma Desktop is the last step of the Chroma tooling pipeline: write an app, iterate with chroma dev (backed by the dev server), package it with chroma build (package.zip: true), then drop the resulting .zip here to run it as a standalone local app, with its own always-on local server and no browser tab required.
CLI
Scaffold, serve, and build the app you're about to import.
Server
The request-handling design both the CLI's engine and this app's local server share.
License
Chroma Desktop is licensed under the GNU GPL version 3. See Licencia GPLv3.