DocsStart here
How Rahti works
A build step reads your directories and writes ordinary Rust. The server renders the first response. The browser mounts what the server named. Nothing is discovered at runtime.
From a saved file to a served response
Rahti renders the initial response on the server. PulsePoint compiles and binds reactive expressions in the browser after the document loads. Between the two sits a build step that turns conventions into code.
src/app + src/components + src/models + src/migrations
|
v
rahti-build scans conventions
|
+--> src/routes.rs (generated, checked in)
+--> src/components/mod.rs (generated, checked in)
+--> src/models/mod.rs (generated, when a db is configured)
+--> src/migrations/mod.rs (generated, carries the Migrator)
+--> .rahti/manifest.json (generated tooling metadata)
+--> public/css/styles.css (built/copied according to config)
|
v
Rust handlers render Html
|
v
browser loads /js/main.js
|
v
PulsePoint v2 mounts reactive blocksWhich means the startup has no magic in it, and it is two short files. src/lib.rs is the application — it connects whatever needs connecting and builds the generated router; src/main.rs is the web binary, which is the one part only a web deployment has: a public address and a terminal to report to.
// src/lib.rs — the application, and nothing hidden from you.
pub use rahti;
mod components;
pub mod routes;
pub async fn initialize_application() -> Result<ApplicationRuntime, StartupError> {
// Connect a database here, apply this application's migrations, install
// its auth policy — all of it before the router is built, and all of it
// in one place both hosts read.
//
// rahti::auth::configure(auth::settings());
Ok(ApplicationRuntime {
router: routes::router(),
host: routes::HOST,
port: routes::PORT,
})
}// src/main.rs — the web binary: a public address, and a terminal.
use my_app::{initialize_application, rahti};
#[tokio::main]
async fn main() {
// A returned error rather than a printed one: a window has nowhere to
// print, and an optional native package calls this same function.
let app = match initialize_application().await {
Ok(app) => app,
Err(e) => {
eprintln!("rahti: the application could not start ({}): {e}", e.step);
std::process::exit(1);
}
};
// The configured address, unless PORT or HOST names another.
let listener = rahti::listen(app.host, app.port).await;
// Not the bind address verbatim: `0.0.0.0` is every interface, which is
// not somewhere a browser can go.
println!("Server running on {}", rahti::local_url(&listener));
axum::serve(listener, app.router)
.with_graceful_shutdown(async {
rahti::shutdown_signal().await;
})
.await
.unwrap();
}The workspace
Five crates, released in lockstep on one version number — html! expands to paths inside rahti, and rahti-build writes code that calls into it, so a project that mixed versions would fail to compile with an error naming neither crate.
| Crate | Role |
|---|---|
| rahti | The server runtime: Html, layouts, errors, RPC, streaming, uploads, auth, and — behind the ws feature — sockets. |
| rahti-macros | html!, #[component], #[rpc], #[socket]. Re-exported by rahti. |
| rahti-build | The build step: scans conventions, generates the router and modules, compiles CSS. |
| cargo-rahti | cargo rahti new and cargo rahti upgrade — see Commands. |
| rahti-mcp | Optional, and never linked into the application: a read-only stdio MCP server that reads the config, the generated manifest, the convention documents and a bounded tail of .rahti/dev.log. See MCP server. |
Two optional packages live outside this workspace, on release lines of their own: icons and native packaging. Neither is compiled, downloaded or resolved by a project that has not asked for it. Official packages is the whole list.
What the runtime exports
Html,Render,RenderJsandJson;Attrs,Attributes,AttrList,Presenceandattribute: whether an attribute is written at all, and what a spread puts on a tag;- layout and error-boundary response layers, including the one that names the document a response was rendered in;
Error,ErrorInfoandResult;- RPC parsing, serialization, CSRF, streaming and upload types;
auth: a signed session cookie, a route-protection policy, and the guard layer that applies it — no user storage, no password handling, no providers, no roles;- link-time component RPC registration, and socket registration when the
wsfeature is on; listen: the server's listener —PORTandHOSToverride the configured address so a deployment needs no code change, and a busy port falls forward to the next free one in dev while release treats it as an error;local_url: where to open a browser for a listener, printing a wildcard bind aslocalhostbecause0.0.0.0is not a destination;- public-directory resolution, development reload routes, and graceful-shutdown coordination.
What the macros do
html!parses typed inline HTML, separates Rust from client expressions, escapes interpolated data, and stamps component boundaries.- In an open tag it also decides attribute presence: an
@{…}value writes its own name, so aNonewrites no attribute, and aboolon one of the names HTML reads by presence writesname=""or nothing. That list is consulted at compile time, which is how the samefalsemeans opposite things ondisabledand onaria-hidden. A tag carrying a spread builds its whole open tag throughAttrList, so a later attribute replaces an earlier one instead of being written beside it. #[component]validates a PascalCase function, stamps its returned root, and generates the hidden named-props companion the tag form calls through.#[rpc]preserves the Rust function and generates its request shim and registry entry.#[rpc(auth)]adds a session check ahead of reading the payload.#[socket]generates the shim that reads the connection's first frame into the parameters.#[socket(auth)]refuses the handshake with a 401 before the upgrade.
What the build step does
On every relevant Cargo build, rahti-build:
- reads
rahti.config.json; - scans the route convention files and
src/components/*.rs; - validates route, RPC and socket-name conflicts;
- generates routes, modules and the manifest — recording, along the way, which rpcs and sockets were written
(auth), so tooling can report the requirement the router enforces; - generates model and migration wiring when a database is configured — without depending on SeaORM or reading any of its code;
- compiles or copies CSS, scanning
src/app/andsrc/components/and nothing outside this project; - emits Cargo watch directives for every relevant path.
Generated artifacts
All of these are checked in on purpose: a clone builds and runs without a code generator having to succeed first, and a generated diff is reviewable.
src/routes.rs the Axum router, checked in
src/components/mod.rs module declarations for every component file
src/models/mod.rs entity modules, when a database is configured
src/migrations/mod.rs migration modules and the Migrator, in filename order
.rahti/manifest.json routes, layouts, components, rpcs, sockets, db
public/css/styles.css compiled or copied, and deliberately committedWhen behaviour is unclear
The framework keeps an explicit source-of-truth order:
- executable tests;
- the implementation in
crates/; - generated output —
src/routes.rs,.rahti/manifest.json; - the examples under
src/app/andsrc/components/; - the convention documents — which is what this site is.
Generated output is evidence, not an editing surface.
The browser runtime asset
PulsePoint ships as a prebuilt bundle. Nothing in the workspace compiles or bundles JavaScript, and a checkout is complete as it stands. public/js/main.js imports the minified runtime, installs twMerge, and calls pp.mount() once the DOM is ready.
