| Crates.io | heisenberg |
| lib.rs | heisenberg |
| version | 0.4.0 |
| created_at | 2025-08-25 01:51:17.001011+00 |
| updated_at | 2025-12-23 07:57:28.090086+00 |
| description | Framework-agnostic dual-mode web serving for Rust applications. Seamlessly switch between proxy mode (forwarding to frontend dev servers) and embed mode (serving embedded static assets). |
| homepage | https://github.com/adlio/heisenberg |
| repository | https://github.com/adlio/heisenberg |
| max_upload_size | |
| id | 1808968 |
| size | 259,885 |
Heisenberg serves SPAs from Rust web applications. It switches between proxy mode (forwarding to a frontend dev server) and embed mode (serving assets compiled into your binary).
In development, run cargo heisenberg run. This starts your frontend dev server and your Rust backend, proxying frontend requests (including WebSocket HMR) to the dev server.
For release builds, run cargo heisenberg build --release. This builds your frontend, then compiles the assets into your Rust binary using the embed_spa! macro.
Add to your Cargo.toml:
[dependencies]
heisenberg = "0.4"
axum = "0.7"
tokio = { version = "1.35", features = ["full"] }
rust-embed = "8.0"
Write your server:
use axum::{routing::get, Router};
use heisenberg::{Heisenberg, HeisenbergLayer};
#[tokio::main]
async fn main() {
let spa = heisenberg::embed_spa!();
let config = Heisenberg::new()
.route("/*", spa)
.dev_server("http://localhost:5173")
.build();
let app = Router::new()
.route("/api/hello", get(|| async { "Hello!" }))
.layer(HeisenbergLayer::new(config));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
axum::serve(listener, app)
.with_graceful_shutdown(heisenberg::shutdown_signal())
.await
.unwrap();
}
Install and run the cargo plugin:
cargo install cargo-heisenberg
# Development (proxy mode with HMR)
cargo heisenberg run
# Release build (embeds assets)
cargo heisenberg build --release
cargo heisenberg init # Generate heisenberg.toml
cargo heisenberg build # Build frontend, then cargo build
cargo heisenberg run # Start frontend + backend with split-pane TUI
Add --no-tui to cargo heisenberg run for plain output (useful for copying error messages).
The plugin auto-detects your frontend if you have a single SPA in ./web or ./frontend. No config file needed.
Create heisenberg.toml when you have:
Single SPA example:
[spa]
working_dir = "./client"
output_dir = "./client/dist"
Multiple SPAs:
[[spa]]
name = "app"
working_dir = "./app"
output_dir = "./app/dist"
dev_server = "http://localhost:5173"
[[spa]]
name = "admin"
working_dir = "./admin"
output_dir = "./admin/dist"
dev_server = "http://localhost:5174"
In your Rust code, reference named SPAs:
let app = heisenberg::embed_spa!("app");
let admin = heisenberg::embed_spa!("admin");
let config = Heisenberg::new()
.route("/admin/*", admin)
.route("/*", app)
.build();
| Command | Mode | Behavior |
|---|---|---|
cargo heisenberg run |
Proxy | Forwards to dev server with HMR |
cargo run |
Embed | Serves embedded assets |
cargo build --release |
Embed | Compiles assets into binary |
HEISENBERG_MODE=proxy cargo run |
Proxy | Force proxy mode |
HEISENBERG_MODE=embed cargo run |
Embed | Force embed mode |
let spa = heisenberg::embed_spa!();
let config = Heisenberg::new()
.route("/*", spa)
.build();
let app = Router::new()
.route("/api/hello", get(handler))
.layer(HeisenbergLayer::new(config));
let spa = heisenberg::embed_spa!();
let config = Heisenberg::new()
.route("/*", spa)
.build();
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(config.clone()))
.route("/api/hello", web::get().to(handler))
.default_service(web::to(heisenberg::adapters::actix::serve_spa))
})
let spa = heisenberg::embed_spa!();
let config = Heisenberg::new()
.route("/*", spa)
.build();
#[launch]
fn rocket() -> _ {
rocket::build()
.manage(config)
.mount("/api", routes![hello])
.mount("/", spa_routes())
}
MIT License. See LICENSE.