Rust on the server. Real HTML in the browser.
Rahti is a server-rendered web framework for Rust. Pages are files, components are ordinary functions, server calls are attributes — and reactivity arrives exactly where you asked for it. No Node, no bundler, no JavaScript build step.
cargo install cargo-rahti
cargo rahti new my-app --tailwind
cd my-app && cargo run// src/app/customers/[id]/page.rs → /customers/{id}
use axum::extract::Path;
use crate::rahti::{Html, html, rpc};
pub async fn page(Path(id): Path<String>) -> Html {
let customer = load(&id).await;
html! {
<article class="card">
<h1>@{customer.name}</h1>
<p>"Seats in use: "{seats}</p>
<button onclick={buy()}>"Add a seat"</button>
<script>
const [seats, setSeats] = pp.state(@{customer.seats});
async function buy() {
setSeats(await pp.rpc("add_seat", { id: @{id} }));
}
</script>
</article>
}
}
#[rpc]
pub async fn add_seat(id: String) -> rahti::Result<u32> {
Ok(billing::add_seat(&id).await?)
}One file: a route, a rendered page, browser state, and a server function the browser can call.
0
npm packages, bundlers or build steps
1
binary to deploy, beside public/
2
expression dialects to learn
9
convention documents in every project
The dividing line
Two dialects, one file
Every framework has to decide where the server stops and the browser starts. Rahti decides it in the syntax, so you can always see it.
@{expression}
Rust, evaluated on the server while the page renders. The value is escaped on its way into the document, so a name with an angle bracket in it is a name, not markup.
{expression}
JavaScript, evaluated in the browser by PulsePoint whenever the state it reads changes. A React-shaped hook surface — state, effects, memo, context, reducers, transitions — with nothing to install.
They never interchange, and each side is handled by the machinery that understands it. What the browser receives is the page, already rendered, with a name on the parts the runtime owns:
<!-- What the browser is handed: markup, and a name for the runtime. -->
<section class="card" pp-component="page_9f31c0a4">
<h1>Ada Lovelace</h1>
<p>Seats in use: <!--pp-->3<!--/pp--></p>
<button>Add a seat</button>
</section>In the box
A whole application, not a rendering library
Routing, rendering, reactivity, server calls, sockets, sessions and a database story — each with one way to do it, and a build error when you do it another way.
File-system routing
Typed HTML
html! is parsed at compile time, so a stray tag is a compiler error rather than a broken page. Interpolated values are escaped by default.Components as functions
#[component], called as <Card />. Props are the function's arguments, so rustc checks the call site like any other call.Typed RPC
#[rpc] beside its caller. CSRF protection by default, typed arguments and returns, error propagation through rahti::Result, server streaming, and uploads with progress.PulsePoint reactivity
public/js/, not as a build step.WebSockets
#[socket] functions beside their pages, arguments as the connection's first frame, a same-origin handshake check, and cloneable senders for broadcast.Authentication
Database, when you want one
Diagnostics you can read
.rahti/dev.log as JSON Lines — a file a person, or an agent, can grep.Components
A function you can call, or a tag you can write
There is no props struct to keep in sync and no component registry to update. Save a file under src/components/ and the component is in scope, as a tag, everywhere.
- Props are typed arguments, and the ones you leave out take the component's defaults.
- A client binding written on a component tag travels through and lands on the element it renders.
- Children arrive as an ordinary Html argument and render with <slot />.
- Each rendered instance is its own reactive scope, so two counters count independently.
- src/components/ is the whole story: the only directory scanned for components, and the only one Tailwind reads besides src/app/.
// src/components/card.rs — in scope in every page, as a tag.
use crate::rahti::{Html, component, html};
#[component]
pub fn Card(title: &str, children: Html) -> Html {
html! {
<section class="card">
<h2>@{title}</h2>
<slot />
</section>
}
}<Card title="Profile">
<p>"Ada"</p>
<Badge text="Rust" />
</Card>On crates.io
Nine crates, three release lines
The framework is five crates released in lockstep. Everything else is optional and versioned on its own — a project that never asks for it compiles, downloads and depends on none of it.
The framework
One version across all five. html! expands to paths in rahti and rahti-build writes code that calls into it, so a mixed pair could not compile.
- rahti
- rahti-macros
- rahti-build
- cargo-rahti
- rahti-mcp
Icons
An icon is fetched once and written into src/components/, so the binary carries the icons a page uses rather than a catalog of 1,600.
- rahti-icons
- cargo-rahti-icons
Native packaging
Windows and Android: your router on a loopback socket inside a WebView. Not a widget compiler, and not compiled into a web build.
- rahti-native
- cargo-rahti-native
Built to be handed over
Written for coding agents, too
Rahti is not in any model's training data. So every project it scaffolds carries the framework's rules with it, stays current as the framework moves, and — if you ask for it — answers questions about itself over MCP.
The rules ship with the project
An AGENTS.md guide, a CLAUDE.md pointing at it, and convention documents under docs/conventions/ covering routing, rendering, reactivity, RPC, auth and security.
The project answers questions
cargo rahti new --mcp writes .mcp.json, and rahti-mcp serves eight read-only tools: routes, components, rpcs, sockets, diagnostics. It never reads .env and never writes a file.
Mistakes are loud
There is one way to do each thing. Break a rule and the build says which rule, in a message written to be read.
Upgrades leave your work alone
cargo rahti upgrade refreshes scaffold files whose recorded hashes show you never edited them, and leaves everything you touched alone.
Deployment is a binary and a directory
Compile the application, copy it next to public/, and run it. There is no separate front-end to build, host, or keep in step.
