One Rust codebase. Two runtimes: desktop GUI + headless service.
A fast, opinionated scaffold for building Rust apps that ship as both a Tauri desktop application and a standalone headless server — sharing the same domain logic and the exact same command contract over HTTP and IPC.
Most Rust app templates force an early, expensive choice: desktop app or backend service.* Build the wrong one first and you rewrite the business layer when the other half is needed.
dualkit is built around a different premise: write your domain once, run it two ways —
- as a Tauri desktop app (native window, WebView UI, IPC to your Rust commands), and
- as a headless HTTP service (the same commands exposed at
POST /rpc/:command), runnable in a container, behind systemd, or anywhere you'd put a normal server.
Both transports speak the same ApiError contract, so the React frontend
talks to whichever runtime is alive through one API client that auto-detects
HTTP vs IPC. Swap deployment without swapping code.
dualkit is a scaffold, not a framework you depend on at runtime. Fork it, rename the crates, replace the example
UserServicewith your own domain, and ship.
- Dual-mode by design — one binary tree compiles to a GUI (
dualkit) and a headless server (dualkit-headless); identical domain + command definitions. - Unified transport contract — HTTP
/rpc/:commandand Tauri IPC produce the same{ code, message, httpStatus? }envelope; a single React Query client (@dualkit/shell) works over both. - Production-grade defaults baked in — atomic config writes (temp + rename + parent-dir fsync), graceful shutdown (SIGTERM aware), request-level timeouts with real cancellation, a JSON content-type gate, bounded/validated config loading, and least-privilege, SHA-pinned CI.
- Typed everything —
Exposure/WindowModeparsed independently from CLI;bindtyped asSocketAddr(unparseable = unrepresentable); a singleErrorEnvelopeshape end-to-end. - Frontend included — React 18 + TanStack Query + Vite + TypeScript, with MSW-based transport tests.
- First-class ops — multi-stage Dockerfile (non-root, read-only static assets), systemd unit, MSRV-enforcing CI, and a Docker smoke test.
┌───────────────────────────────────────────┐
your domain ───▶│ crates/dualkit-domain │
(write once) │ services · repository · POST /rpc dispatch│
└───────────────┬───────────────┬───────────┘
│ │
┌──────────────▼──────┐ ┌─────▼─────────────────┐
desktop app ◀───│ Tauri IPC commands │ │ HTTP /rpc/:command │──▶ headless server
(dualkit bin) │ crates/dualkit- │ │ crates/dualkit- │ (dualkit-headless)
│ tauri + src-tauri │ │ headless (axum) │
└─────────────────────┘ └─────────────────────────┘
▲ ▲
└───────┬───────┘
│
@dualkit/shell ── one API client, auto-detects transport
@dualkit/web ── React + React Query UI
The framework core lives in dualkit-core (server runtime, config, transport
plumbing) on the Rust side, and in @dualkit/shell (the unified HTTP/IPC
client) on the frontend side. The example layer you replace —
dualkit-domain, dualkit-tauri, and @dualkit/web — sits on top of both.
cargo generate yolorouter/dualkit --branch templateRenames every dualkit reference into your project name and produces a
ready-to-build project. See CONTRIBUTING. Requires
cargo install cargo-generate.
| Tool | Version |
|---|---|
| Rust | 1.88+ (GUI/full workspace) · 1.86+ for headless-only builds |
| Node.js | 20+ |
| pnpm | 9.6+ (pinned via packageManager) |
| Platform | macOS, Linux, Windows (Linux GUI build needs webkit2gtk dev packages) |
git clone https://github.com/yolorouter/dualkit.git
cd dualkit
pnpm install
# 1) Run as a desktop app (Tauri window)
pnpm tauri dev
# 2) Run as a headless server (defaults to 0.0.0.0:8080)
pnpm --filter @dualkit/web build # build the UI once
DUALKIT_STATIC_DIR="$PWD/packages/web/dist" \
cargo run -p dualkit-headless -- --headless
# → open http://localhost:8080 (the actual bind address is logged on startup)Flags are mutually exclusive (giving more than one exposure flag is an error, not a silent precedence pick):
| Flag / env | Meaning |
|---|---|
--headless |
Run headless (no GUI window). Defaults to LAN exposure 0.0.0.0:8080. |
--lan |
Bind all interfaces (0.0.0.0:8080). |
--local-only |
Bind loopback only (random port; logged on startup). |
--bind <ip:port> |
Bind an explicit address. |
DUALKIT_CONFIG |
Path to a persisted config JSON (remembers a bind across runs; used when no exposure flag is given). |
DUALKIT_STATIC_DIR |
Serve a web UI from disk instead of the embedded assets. |
dualkit/
├── crates/
│ ├── dualkit-core/ # framework core: server runtime, config, transport, errors
│ ├── dualkit-domain/ # EXAMPLE app: services, repository, /rpc dispatch
│ ├── dualkit-tauri/ # EXAMPLE app: Tauri command handlers (IPC)
│ └── dualkit-headless/ # headless binary entry point
├── src-tauri/ # GUI binary entry point (bin `dualkit`), embeds the web UI
├── packages/
│ ├── shell/ # @dualkit/shell: unified HTTP/IPC client + React Query
│ └── web/ # @dualkit/web: React UI
├── deploy/ # systemd unit
├── Dockerfile # non-root, read-only-assets production image
└── .github/workflows/ # CI: fmt/clippy/test, MSRV gates, Docker build+smoke
# Frontend first — src-tauri embeds packages/web/dist at compile time, and it's
# gitignored, so the Rust checks below fail on a clean checkout without it.
pnpm install --frozen-lockfile
pnpm -r typecheck && pnpm -r test && pnpm -r build
cargo fmt --check
cargo clippy --workspace --all-targets -- -D warnings
./scripts/check-file-size.sh
cargo test --workspace # 111+ tests
./scripts/assert-no-tauri.shCI (.github/workflows/ci.yml) runs the full gate on every push, plus dedicated
MSRV jobs (1.86 headless / 1.88 GUI) and a Docker build + boot smoke test. All
GitHub Actions are pinned to commit SHAs with a top-level permissions: contents: read.
# Desktop installers (deb / AppImage / MSI / NSIS / DMG — platform dependent)
pnpm tauri build
# Headless release binary + Docker image
cargo build --release -p dualkit-headless
docker build -t dualkit .
docker run --rm -p 8080:8080 dualkitdualkit is alpha — the dual-mode architecture, transport contract, and ops
story are complete and battle-tested through an exhaustive adversarial review,
but the public API is still settling. The dualkit-domain / dualkit-tauri
crates are deliberately a minimal example to fork, not a stable library.
This is a scaffold — the expectation is that you fork and adapt it. Bug fixes and improvements to the core are very welcome via pull request. Please run the full gate above before submitting.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.