DocsWorking with Rahti
Configuration
One JSON file for the project, one directory for static assets, three CSS engines, and an environment that names the address a deployment actually binds.
rahti.config.json
The project configuration and the scaffold ledger live in one file, validated by rahti.schema.json. All operational keys have defaults; keep the file valid JSON.
{
"$schema": "./rahti.schema.json",
"schema": 1,
"createdWith": "0.0.24",
"app": {
"dir": "src/app",
"public": "public"
},
"server": {
"host": "0.0.0.0",
"port": 3000
},
"css": {
"engine": "tailwind",
"entry": "src/app/globals.css",
"output": "public/css/styles.css"
},
"ws": true,
"mcp": true,
"db": {
"backend": "sqlite",
"models": "src/models",
"migrations": "src/migrations"
},
"scaffold": {}
}| Key | Meaning |
|---|---|
| schema | The format version. A newer unsupported value stops the build. |
| createdWith | Scaffold provenance — not the active dependency version, which comes from Cargo.toml. |
| scaffold | Maps scaffold-owned files to hashes, which is what makes an upgrade safe. |
| ws | Absent unless the project uses WebSockets. The build never needs it — a #[socket] in the tree is what wires the endpoint — but upgrade reads it to know the manifest is expected to carry the cargo feature, and to say precisely what to add when it does not. |
| mcp | Absent unless the project opted into the MCP developer tooling. It says that .mcp.json is scaffold-owned; it adds no dependency and no route, because the server it configures is a separate executable. See MCP server. |
| db | Absent unless the project has a database. Carries no connection string: that is a credential, and this file is committed. |
A project that has run cargo rahti native init gets a second file beside this one, rahti.native.json, with a schema of its own. It holds product names, window sizes and identifiers, and — like this file — no credentials, because it is committed too. See Native packaging.
server.host and server.port are where the server binds. A deployment can name its own address instead: PORT overrides the configured port and HOST overrides the configured host, both read through rahti::load_env() — so a .env sets them on your machine and the platform's environment sets them in production.
A project scaffolds with "host": "0.0.0.0", the one address that is correct both here and in a container — so a deployment usually names only the port, and HOST is the override for narrowing back to the loopback. See Where the server binds for the whole story.
In development a busy port is not fatal — the server increments until it finds a free one and prints the address it bound, however that port was chosen. A release build treats the same conflict as an error and stops, naming where the port came from, because a platform's router or a reverse proxy points at that port and moving off it silently would break the deployment without anything reporting it.
Static files
Contents of the configured public directory are served from the URL root.
public/css/styles.css -> /css/styles.css
public/js/main.js -> /js/main.js
public/favicon.ico -> /favicon.icoExplicit application routes win. Public files are the router fallback, and the root not-found.rs runs only when neither claims a path. RAHTI_PUBLIC_DIR overrides the configured directory at runtime, and a relative path in it resolves against the working directory — which is why it is the variable a native package sets: an installed application does not control where it was launched from.
Environment
| Variable | Read by | For |
|---|---|---|
| PORT | rahti::listen | Overrides the configured port. The name every managed platform sets. |
| HOST | rahti::listen | Overrides the configured host. 0.0.0.0 where a platform routes traffic in from outside the process. |
| DATABASE_URL | src/db.rs | The connection string. See Database. |
| AUTH_SECRET | rahti::auth | Signing the session cookie. Generated per project. |
| AUTH_COOKIE_NAME | rahti::auth | The cookie's name. Generated per project. |
| SESSION_LIFETIME_HOURS | rahti::auth | Optional session lifetime. |
| RAHTI_PUBLIC_DIR | the runtime | Overrides the static directory. A relative path resolves against the working directory. |
| RAHTI_DEV | the runtime | Forces development reload on or off. |
| RAHTI_TAILWIND | the build step | A Tailwind binary to use instead of the one in .rahti/bin or on PATH. |
rahti::load_env() is the one .env reader for the process. It is idempotent and never overwrites a variable the environment already carries — a real environment variable always wins over the file. None of the auth values belong in rahti.config.json: the policy is code, and the secret is a credential.
PulsePoint assets
The application entry does three things, and none of them is a build step.
import "/js/pp-reactive-v2.min.js";
import { twMerge } from "/js/tailwind-merge.mjs";
const pp = globalThis.pp;
globalThis.twMerge = twMerge;
if (document.readyState !== "loading") {
pp?.mount?.();
} else {
document.addEventListener("DOMContentLoaded", () => pp?.mount?.(), { once: true });
}twMerge is then available to every mounted script as a global. Use it when a reactive class string can contain conflicting Tailwind utilities; it handles variants, !important, arbitrary values and overlapping utility groups.
CSS engines
| Engine | What the build does |
|---|---|
| tailwind | Compiles the entry with the pinned standalone Tailwind CLI. |
| plain | Copies the entry, and appends component-library styles. |
| none | Leaves the output untouched, for a stylesheet you manage yourself. |
The editable entry and the served output keep the same roles in all three modes, and the output is committed so builds remain usable without downloading a CLI. Tailwind settings: version pins the standalone release, download allows fetching a missing binary, minify controls output style, and sha256 maps platform keys such as windows-x64 to trusted hashes. A configured hash mismatch is fatal. Binary lookup covers .rahti/bin, RAHTI_TAILWIND, and tailwindcss on PATH.
The development runtime
Development reload is on in debug builds and off in release, unless RAHTI_DEV says otherwise. Rahti injects a dev client into page HTML, watches public files, swaps changed stylesheets, reloads for other changes, and forwards browser warnings and errors to .rahti/dev.log.
That covers everything read at runtime. A Rust edit needs a rebuild and a restart, which nothing inside the running process can do — so the scaffold writes a .cargo/config.toml whose dev alias hands that job to cargo-watch.
cargo run # regenerate routes and serve
cargo dev # cargo run, restarted on every edit (needs cargo-watch)
cargo check # verify without running
cargo test # this application's tests
cargo rahti upgrade # refresh unedited scaffold filespublic/ is deliberately not on the watch list — the running server handles it without a recompile. The .rahti directory is gitignored by both the repository and the new-project scaffold: generated manifests, CSS scratch files, and the development log. None of it is application input.
The scaffold ledger
The scaffold object is the half of this file you never write. It maps every scaffold-owned path to the hash of the bytes the scaffold wrote there, which is what lets cargo rahti upgrade tell a file nobody has touched from one you have made your own — and rewrite only the first.
cargo rahti new <name> [--tailwind] [--db [backend]] [--ws] [--mcp] [--local <path>]
cargo rahti upgrade [--dry-run] [--force [path...]] [--db [backend]] [--ws] [--mcp]upgraderewrites or removes only files whose current hash still matches the ledger. Edited files are preserved and reported;--forceis how you take one back deliberately.- In this file,
upgraderewrites only what it owns:createdWithand thescaffoldledger. The port, the app directory, and every CSS setting stay exactly as you wrote them. - It can add a database or WebSockets to a project that started without them. The files are written and the choice is recorded here, in
dbandws. - The amendments it makes to
Cargo.tomland.envare driven by this file rather than by what the run added, so a project that gained a feature by editing its config is repaired by runningupgradeagain.
Commands has the whole CLI: every flag new and upgrade take, what --force takes back, and how the version pins move.
