DocsStart here
Getting started
Install the CLI, scaffold a project, and serve it. No JavaScript toolchain, no bundler, and a running application at the end of it.
Install the CLI
cargo-rahti is the scaffolder, and cargo-watch is what the project's cargo dev alias hands the restart to. Those two are the only pieces you install globally; everything else arrives as a normal Cargo dependency of the project cargo-rahti creates.
A current stable Rust is the one prerequisite — the framework names 1.97 as its minimum, so Cargo refuses an older compiler before it starts building anything rather than failing halfway through a dependency. There is no second toolchain: no Node, no bundler, nothing to install for the browser half.
cargo install cargo-rahti # the scaffolder
cargo install cargo-watch # what the project's `cargo dev` alias runsSet up VS Code (optional)
Rahti has an official VS Code extension on the Visual Studio Marketplace, published by JeffersonAbrahamOmier. Open the listing and choose Install, or search the Extensions view for @id:JeffersonAbrahamOmier.rahti to find the exact extension.
If the VS Code command-line launcher is available, you can also install it from a terminal:
code --install-extension JeffersonAbrahamOmier.rahtiOpen your project's Rust files in VS Code. The extension highlights html! templates in place: HTML and component tags, PulsePoint bindings, server interpolation, and embedded JavaScript and CSS. Keep Rust Analyzer enabled for Rust language features; Rahti adds template highlighting rather than replacing your Rust tooling.
This is an editor convenience, not a build dependency. You can use another editor and run the same Cargo commands.
Create 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. |
| --mcp | Adds local stdio plus bearer-protected Streamable HTTP MCP, discovery, and an editable public service description. See MCP server. |
| --local <path> | Path dependencies instead of published crates. For working on the framework itself. |
# Tailwind, a SQLite database, WebSockets, and the MCP configuration.
cargo rahti new my-app --tailwind --db sqlite --ws --mcpRun it
cd my-app
cargo dev
# Server running on http://localhost:3000cargo dev is not a Rahti subcommand, and not a Cargo one either. It is an ordinary Cargo alias the scaffolder writes into the new project's .cargo/config.toml, which is why it means something inside a Rahti project and nothing outside one:
# .cargo/config.toml — written by the scaffolder, not by Cargo.
[alias]
dev = "watch -w src -w rahti.config.json -x run"Everything read at runtime — CSS, static files, page markup — reloads on its own, so the alias covers the one thing the running process cannot do for itself. A Rust edit needs a rebuild and a restart: cargo-watch re-runs cargo run on every change under src/ or to rahti.config.json, and the open tab reloads itself when the new server comes up. public/ is left off the watch list on purpose.
The alias is the convenience, not the requirement. Without cargo-watch installed it fails, and plain cargo run builds and serves exactly the same application — you restart it yourself:
cargo run # the same server, built and served once — you restart it yourselfIn 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.
Your 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);
}Tailwind scans this project's own sources and nothing else: src/app/ and src/components/, the two directories the build already watches — so a changed class rebuilds the stylesheet, and a component written outside them is styled by nothing. The other two engines are plain — copy the entry as written — and none, which leaves the output alone for a stylesheet you manage yourself.
A Tailwind project is also what UI components needs: cargo rahti-ui add button writes shadcn/ui's button into src/components/rahti_ui/ as source you own, and brings the theme those classes name with it. It is optional, and nothing about a project changes until you run it.
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.
