Deployment & Desktop Apps

  1. Opting into zero-downtime restarts
  2. How the handoff actually works
  3. xr deploy
  4. Tauri desktop apps
  5. Next

Opting into zero-downtime restarts

By default, a generated app behaves exactly like any plain axum::serve program - Ctrl+C exits immediately. Zero-downtime restart is a real, opt-in change to your process-lifecycle behavior, in two independent steps:

Application::at_root(env!("CARGO_MANIFEST_DIR"), config::app::config)?
    .router(route.into_axum_router())
    .with_graceful_shutdown(GracefulShutdown {
        drain_timeout: Duration::from_secs(30),
        restart_channel: true,
    })
    .serve().await
  • No .with_graceful_shutdown(...) call at all → unchanged, original behavior.
  • restart_channel: false → graceful shutdown on Ctrl+C/SIGTERM only (a smaller, legitimate feature on its own): stop accepting new connections, drain in-flight ones, with a hard drain_timeout backstop so a stuck connection can’t block a shutdown forever.
  • restart_channel: true → the full mechanism below.

xr new doesn’t enable either by default - this is a deliberate step your app takes once you understand the drain-timeout trade-off, not silently baked in.

How the handoff actually works

No external supervisor, reverse proxy, or process manager required (and nothing here conflicts with one either, if you already use one). The currently-running process spawns its own replacement, hands it the exact same listening socket it was already using (real inter-process socket-passing - fcntl-based fd inheritance on Unix, WSADuplicateSocketW on Windows - not a second process racing for the same port), waits for the replacement to confirm it’s genuinely serving, and only then drains its own in-flight requests and exits. Verified with real subprocess integration tests driving continuous HTTP traffic through an actual restart, with zero failed requests across the handoff.

xr restart

Sends the restart command over a local admin channel (a Unix socket / Windows named pipe). xr dev and xr deploy both build on this exact same mechanism internally - every rebuild during xr dev is a zero-downtime handoff, not a stop-then-start.

xr deploy

xr deploy [--run]
# .env
DEPLOY_TYPE=web   # default

cargo build --release, publish to a fresh, monotonically-increasing slot under storage/releases/ (release builds and xr dev’s own dev builds use separate counting namespaces, so one can’t prune the other’s files away), then the same restart-handoff xr restart uses against whatever’s currently running. Builds frontend assets first (npm run build) when node_modules/ exists, stopping the deploy outright if that build fails - a broken asset build must never ship.

The very first deploy of an app that’s never been started has nothing running to hand off to; --run cold-starts the freshly published binary in the background for you instead of just publishing it and leaving you to start it manually.

Tauri desktop apps

xr new myapp --tauri     # scaffold Tauri support from the start
xr add tauri              # or retrofit it onto an existing app
# .env
DEPLOY_TYPE=app
xr deploy       # cargo tauri build, from src-tauri/

Uses an “embedded local server” model: src-tauri/src/main.rs spawns your app’s own serve() on a background tokio runtime, then points a native OS webview at http://127.0.0.1:{port} - one real router, reached over loopback instead of a browser tab, not a rewritten or parallel implementation. This is exactly why a generated app’s boot sequence lives in src/lib.rs (connect_database/port/application/router/serve) rather than inline in src/main.rs’s main() - an embedded host reuses those functions directly instead of spawning your compiled binary as a subprocess.

src-tauri/Cargo.toml is a plain sibling crate depending on your app via path = "..". Icons aren’t generated at scaffold time - cargo tauri build fails with an install-style hint (cargo tauri icon <path>) if src-tauri/icons/ is still empty, the same “clear hint, no silent half-working state” posture xr audit takes for a missing cargo-audit install.

Next

You’ve now covered the whole framework surface. Converting a Laravel App if you’re bringing an existing project over, or back to Home for the full map.