DocsStart here
Introduction
Rahti is a server-rendered web framework for Rust. Pages are files, components are ordinary functions, server calls are attributes, and the browser receives real HTML with reactivity exactly where you asked for it.
There is no Node in a Rahti project, no bundler, and no JavaScript build step. A page is a Rust file whose position in src/app/ is its URL. It returns markup written in html!, a macro parsed at compile time, and the server sends that markup as HTML. Where a page needs the browser to do something, it writes a script inside the markup that needs it, and the bundled PulsePoint runtime binds it.
The example below is a complete page: a route, a rendered document, a piece of browser state, and a server function the browser can call — one file, no wiring.
// src/app/page.rs — where this file sits is the / route.
use crate::rahti::{Html, html, rpc};
pub async fn page() -> Html {
html! {
<section class="page">
<h1>"Hello"</h1>
<button onclick={greet()}>"Say hello"</button>
<p>{message}</p>
<script>
const [message, setMessage] = pp.state("");
async function greet() {
setMessage(await pp.rpc("hello", {}));
}
</script>
</section>
}
}
/// Runs on the server, in Rust. The browser calls it by name over a typed,
/// CSRF-protected wire and never learns there was a function here.
#[rpc]
pub async fn hello() -> String {
"Hello from Rust.".to_string()
}One file, two dialects
The dividing line in that example is the interpolation, and it is the single most important rule in Rahti.
@{rust_expression}
Evaluated on the server, at render time, by Rust. Values are escaped as they are written into the document. This is how a page prints what it fetched, formatted, or computed.
{javascript_expression}
Evaluated in the browser, by PulsePoint, every time the state it reads changes. This is how a page reacts to a click without shipping a component tree to do it.
They never interchange. A server value the browser needs is seeded into the script with @{…}; a browser value the server needs is sent with an RPC call. Everything else in Rahti follows from keeping those two worlds honest about which one owns a given expression.
The shape of an application
Rahti has one place for each kind of thing, and the build step reads the directory rather than a configuration file. A new project looks like this:
my-app/
├── src/
│ ├── app/ the router: every URL is a directory here
│ │ ├── layout.rs the document shell
│ │ ├── page.rs /
│ │ ├── globals.css the CSS entry Tailwind compiles
│ │ └── docs/
│ │ └── page.rs /docs
│ ├── components/ PascalCase functions, in scope everywhere
│ ├── routes.rs generated — never edited by hand
│ └── main.rs builds the router, serves it
├── public/ static files, served as-is
├── docs/conventions/ the rules, shipped with the project
├── .rahti/ the manifest and dev.log
└── rahti.config.json host, port, CSS engine, app directoryWritten to be read — by people and by agents
Rahti is not in any model's training data, so every scaffolded project carries the framework's rules with it: an AGENTS.md guide, a CLAUDE.md pointing at it, and the convention documents under docs/conventions/ — the same documents this site is built from. Open a fresh project in a coding agent and it has what it needs to write correct Rahti code, and cargo rahti upgrade keeps unedited copies current as the framework moves.
The framework holds up its end: there is one way to do each thing, mistakes surface as build errors that name the broken rule, and development diagnostics — browser warnings included — are captured as JSON Lines in .rahti/dev.log, a file a person or an agent can read.
Where to go next
Every page below documents one feature area, in the order they tend to matter. If you are starting from nothing, take them in order; if you are here for one answer, take the card that names it.
