Desktop App Packaging#
The desktop app ships as a single product file: the oxo-flow-desktop
binary contains the workflow engine, the web server, and the SPA frontend
(through oxo-flow-web), and renders the interface in a native OS
webview window — no external browser involved.
How it works#
oxo-flow.app (macOS)
└── Contents/MacOS/oxo-flow # the desktop shell binary (wry + tao)
└── Contents/Resources/static/ # SPA assets, served by the shell
└── Contents/Resources/oxo-flow.icns # app icon
On launch the shell binary:
- anchors its working directory to
~/.oxo-flow/desktop— the per-user data directory where the SQLite database, logs, and workspace files live (see below); - takes a single-instance lock in that directory (a second launch fails fast with a clear error instead of corrupting the database);
- picks a free loopback port and starts the axum web server
(
oxo-flow-web, personal mode — loopback-only) on a private tokio runtime; - waits for the listener to accept TCP connections (so the first page load never races server startup), then
- opens a native webview window (WKWebView on macOS, WebView2 on
Windows, WebKitGTK on Linux — via the
wrycrate) pointed at the server URL; - exits when the window closes, shutting the embedded server down.
Closing the window is the app lifecycle: there is no background server
process left behind. If the embedded server crashes, the window closes
and the app exits non-zero instead of freezing on a stale page. A
watchdog task races the server against SIGINT/SIGTERM, so kill and
Ctrl-C also shut the app down cleanly — including a signal arriving
during the slow cold GTK/webkit startup on Linux, which exits gracefully
instead of failing with a spurious startup error.
Data directory and lifecycle#
All desktop state lives in ~/.oxo-flow/desktop (following the CLI's
~/.oxo-flow convention):
| Path | Contents |
|---|---|
~/.oxo-flow/desktop/oxo-flow.db (+ -wal/-shm) |
SQLite database |
~/.oxo-flow/desktop/logs/ |
server and audit logs |
~/.oxo-flow/desktop/workspace/ |
run workspaces |
~/.oxo-flow/desktop/oxo-flow.desktop.lock |
single-instance lock |
The lock is an advisory flock held for the process lifetime — the OS
releases it automatically if the app crashes, so there is no stale-lock
cleanup. Closing the window sends the process a SIGTERM, which runs the
server's graceful shutdown (flush audit logs, finish in-flight requests)
before exit; SIGTERM/SIGINT from outside do the same.
Two quality-of-life details:
- Finder launches
.appbundles withcwd=/, where the server cannot create its SQLite database. The shell anchors the working directory to the data directory above before anything starts — so state never scatters across whatever directory the app happened to be launched from. - External links open in the system browser. A navigation handler
confines the app window to the app's own loopback origin; anything else
(the GitHub/docs links in the UI) is handed to the platform opener —
openon macOS,xdg-openon Linux,exploreron Windows (notcmd /c start, which re-parses its command line).target="_blank"links andwindow.opencalls are routed the same way. The window itself never navigates away from the interface.
Rendering stays in the OS webview (not a bundled browser, not Electron): the DAG canvas (React Flow) and CodeMirror editor run on the platform's own engine, exactly the engine the release server serves to.
Prerequisites#
- Build the frontend first — a prebuilt SPA ships in
crates/oxo-flow-web/static/, but rebuild it to bundle the latest UI:
- The desktop crate is excluded from the cargo workspace (tao/wry need GUI toolchains — Xcode SDKs, WebKitGTK — that headless builds and CI test jobs must not pull in). Build it with its own cargo invocation:
Platform notes: macOS needs the Xcode CLIs (xcode-select --install);
Linux needs WebKitGTK 4.1 development files
(libwebkit2gtk-4.1-dev on Debian/Ubuntu) and, for Wayland, the usual
GTK scaling env vars; Windows needs the WebView2 runtime (preinstalled
on Windows 10/11) and MSVC Build Tools.
macOS (.app + .dmg)#
The GitHub release ships a hand-rolled .app + .dmg built by CI
(desktop-shell binary, icon, SPA, and ad-hoc code signature included).
To run the shell locally without packaging:
To assemble the same bundle CI builds (.app + ad-hoc signature + DMG):
APP=oxo-flow.app
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
cp crates/oxo-flow-desktop/target/release/oxo-flow-desktop \
"$APP/Contents/MacOS/oxo-flow"
cp -r crates/oxo-flow-web/static "$APP/Contents/Resources/static"
cp assets/oxo-flow.icns "$APP/Contents/Resources/oxo-flow.icns"
codesign --force --deep --sign - "$APP"
hdiutil create -volname "oxo-flow" -srcfolder "$APP" -ov -format UDZO oxo-flow.dmg
The .app is self-contained: drag it to /Applications, double-click, and
the interface opens in a native window. Data (SQLite + workspace) lives
in $HOME (see the cwd=/ note above).
Install and first run (macOS)#
-
Do not double-click the app inside the DMG. Open the DMG and drag
oxo-flow.appinto/Applicationsfirst, then launch it from there. Apps launched in place from a downloaded DMG are evaluated by Gatekeeper before they finish starting and can hang or be reported as damaged. -
First launch — Gatekeeper. The release bundle is signed with an ad-hoc signature (no Apple Developer ID — the project cannot ship a Developer ID certificate from CI). Gatekeeper treats downloaded ad-hoc apps as untrusted, so the first launch needs one of these:
-
Recommended — clear the download quarantine once, then launch normally every time:
-
Or right-click (Control-click)
oxo-flow.appin Finder → Open → click Open in the confirmation dialog. (Note: on macOS 26 the right-click Open bypass no longer exists for every app; thexattrcommand above always works.)
If the system reports “oxo-flow.app is damaged and cannot be opened”
(or “is damaged”), this is Gatekeeper rejecting the unsigned download —
the fix is the xattr command above. On Apple Silicon, unsigned or
improperly signed arm64 apps show this “damaged” error instead of the
“unidentified developer” prompt seen on Intel. Clearing the quarantine
attribute is the only reliable cure; enabling “Allow applications from
anywhere” (sudo spctl --master-disable) alone is not sufficient.
The downloaded DMG itself carries the quarantine attribute; the command above strips it from the installed app. Alternatively strip it from the DMG before opening it:
- First launch may be slow while macOS indexes the app; subsequent launches are instant.
macOS specifics#
- Icon: the bundle ships
Contents/Resources/oxo-flow.icns(rendered fromlogo.svg, stored in the repo atassets/oxo-flow.icns) and theInfo.plistdeclaresCFBundleIconFile. - Signature: CI ad-hoc-signs the bundle (
codesign --force --deep --sign -) before creating the DMG. This makes the app internally consistent — its Mach-O,Info.plist, and resources are sealed under one signature — which prevents the “damaged” classification once the quarantine attribute is cleared. It is not a Developer ID signature: Gatekeeper still blocks quarantined downloads, and a future notarization pipeline (Developer ID certificate) would remove the quarantine workaround entirely.
Linux (.deb / .rpm / .AppImage)#
make bundle-deb # Debian/Ubuntu package
make bundle-rpm # RHEL/Fedora package
# or directly:
cargo bundle --release --format deb
The Linux desktop entries (deb/rpm/AppImage) still launch
oxo-flow serve --open, which opens the interface in the system browser —
the release packaging for the native-window shell currently covers macOS.
Install libwebkit2gtk-4.1-dev and run the desktop crate directly if you
want the windowed shell on Linux today (the crate itself is
platform-independent; verified on X11/Xvfb, Linux):
Windows#
There is no Windows bundle yet — the release pipeline does not build one
(cargo-bundle's msi format requires a Windows host). Windows users run
the Linux binaries under WSL2. The desktop crate itself compiles for
Windows (wry uses WebView2) but is not packaged by CI yet.
Verification#
After packaging, verify the shell starts its server and serves the SPA
without a source checkout (the shell resolves static/ relative to the
executable, same lookup as the standalone server):
APP="oxo-flow.app/Contents/MacOS/oxo-flow"
"$APP" & # opens a native window; server on an ephemeral port
lsof -p $! | grep -m1 TCP # shows the loopback listener
Verify the bundle signature and icon:
codesign -dv --verbose=4 oxo-flow.app # Signature=adhoc
plutil -lint oxo-flow.app/Contents/Info.plist
GitHub Release Assets#
Each tagged release publishes the desktop bundles alongside the raw tarballs (built by CI, not by hand):
| Asset | Platform |
|---|---|
oxo-flow-<ver>-desktop-x86_64-apple-darwin.dmg / -desktop-…-app.zip |
macOS Intel (Rosetta on Apple Silicon) |
oxo-flow-<ver>-desktop-aarch64-apple-darwin.dmg / -desktop-…-app.zip |
macOS Apple Silicon |
oxo-flow-<ver>-desktop-amd64.deb |
Debian / Ubuntu (menu entry opens the system browser; native window via the desktop crate) |
oxo-flow-<ver>-desktop-x86_64.rpm |
RHEL / Fedora / CentOS (menu entry opens the system browser; native window via the desktop crate) |
oxo-flow-<ver>-desktop-x86_64.AppImage |
any Linux distribution (menu entry opens the system browser; native window via the desktop crate) |
oxo-flow-<ver>-<target>.tar.gz |
CLI binary, 8 targets (macOS ×2, Linux glibc/musl ×3 architectures) — for clusters, containers, and scripted installs |
oxo-flow-web-<ver>-<target>.tar.gz |
Standalone web-server binary (no CLI subcommands) — for deployment hosts that only serve the UI |
SHA256SUMS.txt |
Checksums for every asset above |
Install from a tarball#
curl -LO https://github.com/Traitome/oxo-flow/releases/download/v0.23.2/oxo-flow-v0.23.2-x86_64-unknown-linux-gnu.tar.gz
curl -LO https://github.com/Traitome/oxo-flow/releases/download/v0.23.2/SHA256SUMS.txt
sha256sum -c SHA256SUMS.txt --ignore-missing # verify before you run it
tar xzf oxo-flow-v0.23.2-x86_64-unknown-linux-gnu.tar.gz
sudo install -m 755 oxo-flow /usr/local/bin/oxo-flow
Pick the target that matches the machine: gnu for glibc distributions,
musl for Alpine/static links, armv7 for 32-bit ARM.
# Linux one-liners
sudo dpkg -i oxo-flow-*desktop*.deb # Debian/Ubuntu
sudo rpm -i oxo-flow-*desktop*.rpm # RHEL/Fedora
chmod +x oxo-flow-*desktop*.AppImage && ./oxo-flow-*desktop*.AppImage # any distro
The AppImage runs oxo-flow serve --open on launch; the deb/rpm install
/usr/bin/oxo-flow with the SPA under /usr/share/oxo-flow/static
(resolved by the executable-relative frontend lookup, so no source
checkout is needed).
Notes#
- Version: the desktop crate repeats the workspace version
(
0.23.2) by value — it is outside the workspace, so it does not inherit[workspace.package]; bump it together with the rest on release. - Data directory: the desktop app keeps all state in
~/.oxo-flow/desktop(database, logs, workspace, single-instance lock) — see the table above. The CLI'soxo-flow serveand the desktop shell deliberately do not share a data directory, so both can run side by side (e.g. while comparing a release install with a dev build). - Icon: the app icon (
assets/oxo-flow.icns, macOS) is rendered fromlogo.svgand shipped in the CI bundle. For local cargo-bundle builds, addicon = ["../../assets/oxo-flow.icns"]under[package.metadata.bundle]incrates/oxo-flow-cli/Cargo.toml(.icnsfor macOS,.png/.icoelsewhere); the bundle works without one. - Signing: CI ad-hoc-signs the macOS bundle (
codesign --force --deep --sign -). Downloaded ad-hoc apps still hit Gatekeeper — see the install steps above. A Developer ID certificate + notarization would remove the quarantine workaround; Linux packages should be signed with a GPG key.