DocsStart here
Getting started
Install the CLI, scaffold a project, and serve it. Three commands, no JavaScript toolchain, and a running application at the end of them.
Install the CLI
cargo-rahti is the scaffolder. It is the only piece you install globally; everything else arrives as a normal Cargo dependency of the project it creates.
cargo install cargo-rahtiCreate a project
Each feature flag adds something, and leaving it out is how you say no — there are no opposing flags to reconcile. An interactive run asks about whatever you did not name, defaulting to no.
cargo rahti new my-app --tailwind| Flag | What it adds |
|---|---|
| --tailwind | Compiles globals.css with the pinned standalone Tailwind CLI. Without it, plain CSS. |
| --db [backend] | SeaORM wiring for sqlite, postgres or mysql — bare --db means SQLite. Writes src/db.rs, src/models/ and src/migrations/. |
| --ws | Puts features = ["ws"] on the rahti dependency, which is what compiles rahti::ws and the #[socket] attribute. |
| --local <path> | Path dependencies instead of published crates. For working on the framework itself. |
# A project with Tailwind, a SQLite database, and WebSockets.
cargo rahti new my-app --tailwind --db sqlite --wsRun it
cd my-app
cargo run
# Server running on http://127.0.0.1:3000In development a busy port is not fatal: the server increments from the configured port until it finds a free one, and prints the address it actually bound. A release build treats the same conflict as an error and stops — a deployment's reverse proxy points at the configured port, and moving off it silently would break the deployment without anything reporting it.
Everything read at runtime — CSS, static files, page markup — reloads on its own. A Rust edit needs a rebuild and a restart, which nothing inside the running process can do, so the scaffold ships a cargo dev alias that hands that job to cargo-watch:
cargo install cargo-watch # once per machine
cargo dev # cargo run, restarted on every editYour first page
A page is a file called page.rs under src/app/, and where it sits is its URL. Nothing registers it; the build step reads the directory and regenerates the router.
// src/app/about/page.rs → /about
use crate::rahti::{Html, html};
pub async fn page() -> Html {
let team = ["Ada", "Grace", "Alan"];
html! {
<section class="mx-auto max-w-2xl px-6 py-16">
<h1 class="text-3xl font-bold">"About us"</h1>
<ul class="mt-4 space-y-1">
@{Html::concat(team.iter().map(|name| html! {
<li>@{name}</li>
}))}
</ul>
</section>
}
}Save the file, and /about is a route. Note the two things doing the work: html!, which is parsed at compile time, and @{…}, which evaluates a Rust expression and escapes the result on its way into the document.
Add a component
A component is a PascalCase function marked #[component] that returns Html. Props are its arguments; children arrive as an explicit Html argument and render where you write <slot />.
// src/components/card.rs — one file may export several components.
use crate::rahti::{Html, component, html};
#[component]
pub fn Card(title: &str, children: Html) -> Html {
html! {
<section class="rounded-xl border p-4">
<h2 class="font-semibold">@{title}</h2>
<div class="mt-2 text-sm"><slot /></div>
</section>
}
}// In any page — the component is in scope as a tag.
use crate::components::card::Card;
pub async fn page() -> Html {
html! {
<Card title="Profile">
<p>"Ada Lovelace"</p>
</Card>
}
}Call the server from the browser
A #[rpc] function lives beside the page that calls it. The browser calls it by name over a typed, CSRF-protected wire, and never learns there was a Rust function on the other end.
// src/app/counter/page.rs
use crate::rahti::{Html, html, rpc};
pub async fn page() -> Html {
html! {
<div>
<p>"Count: "{count}</p>
<button onclick={bump()}>"Add one"</button>
<script>
const [count, setCount] = pp.state(0);
async function bump() {
setCount(await pp.rpc("increment", { by: 1 }));
}
</script>
</div>
}
}
// Runs on the server. The browser calls it by name; the function itself
// never leaves the binary.
#[rpc]
pub async fn increment(by: u32) -> u32 {
by + 41
}Styling
With --tailwind, the build compiles src/app/globals.css to public/css/styles.css using the pinned standalone Tailwind CLI — no Node, no PostCSS config. The output is committed, so a checkout builds without downloading anything.
/* src/app/globals.css — the entry the build compiles. */
@import "tailwindcss" source(none);
@source "../";
@theme inline {
--color-brand: oklch(0.54 0.212 277);
}Component libraries you depend on are scanned for classes and may ship their own ui.css, which the build appends. The other two engines are plain — copy the entry as written — and none, which leaves the output alone for a stylesheet you manage yourself.
What to read next
Routing is where most of the shape of an application lives, so start there. If you would rather see how the pieces fit together first, How Rahti works walks the path from a saved file to a served response.
