Guide
Using Rust off Rails, day to day.
You say what the app should do. The agent writes all the code, and the compiler and the test suite decide when it's done. Here is how that works, and the tools and guardrails around it.
You write intent. The agent writes all the code.
That's the framework's founding rule, doctrine §0. Every line of Rust, SQL, HTML, CSS, TypeScript, every migration, test and Dockerfile is written by the agent.
You
- Specs in
product/, in plain English - Answers in the interview
- Feedback in conversation: "make the review form shorter"
- Decisions only you can make: who can see what, what happens on delete, what it costs
The agent
- Every line of code, config and scripts
- The spec first, then the code, for anything you only said
- Tests for every acceptance criterion, on both databases
- A trail:
docs/decisions.md, one dated line per non-obvious choice
The files you touch
product/README.md- What the app is, who uses it, the rules that must always hold, the look and feel. The interview's answers land in its Decisions section.
product/features/*.md- One file per feature. Write as much or as little as you like; the agent fills the gaps, gives each acceptance criterion a stable ID, writes the threat model, and marks what it assumed.
AGENTS.md(the header)- The app's modes and choices, set by the interview. Read it to know what the app is built with; change it only by asking.
# Feature: Reviews
## Why
Readers want to know what others thought of a book.
## Who
Any logged-in user can review a book once. Only the author edits or deletes it.
## Behavior
- A review has 1–5 stars and a body (max 2,000 characters).
- The book's owner gets an email for each new review.
- Deleting a book deletes its reviews.
## Acceptance criteria
- [ ] AC-reviews-1: A logged-in user can review a book with 1–5 stars and a body
- [ ] AC-reviews-2: A second review of the same book shows "You already reviewed this book"
- [ ] AC-reviews-3: The book's owner gets an email when a review is posted
## Security (agent-written threat model)
| STRIDE | Threat | Control | Test |
|-----------|-------------------------------------|---------------------|-----------------------------------|
| Spoofing | An anonymous visitor posts a review | RequireUser | anonymous_cannot_create_a_review |
| Tampering | A user edits someone else's review | policy: author only | other_user_gets_404_on_update |
## Assumptions (agent-written)
- Owners can't opt out of review emails yet.APP: bookshelf
FRONTEND_MODE: htmx
UI_LIBRARY: none
ISLANDS: no
API: none
PROD_DATABASE: postgres
DEPLOY_TARGET: later
ACCOUNTS: magic-link ROLES: none TENANCY: per-userAbridged. The full template is the feature spec blueprint.
The loop
Every change, from a new app to a one-line fix, goes through the same loop, one vertical slice at a time. The tree compiles between slices.
- AskOnly what a human can answer, each with a recommended default.
- SpecWrite or update
product/features/*.md. The spec is the record; chat is not. - MigrateMigration and entity together, rollback proven.
- ModelParams, validation, queries: typed, never strings.
- Failing testsOne per acceptance criterion, citing its ID.
- BuildControllers, routes, views, jobs, mailers.
- Check
cargo checkuntil the compiler agrees. - Verify in a browserRun the app and click through the change. curl alone isn't enough.
- RecordTick the spec, update routes and decisions.
The Definition of Done
"Done" is a command list, not a feeling. Green or it didn't happen.
- Format and lint
cargo fmt,cargo clippywith warnings as errors - Both databases the whole suite on SQLite and Postgres
- Traceability every
AC-…ID inproduct/is cited by a test - Coverage at least 80% of lines, 90% for models and policies
- Security gates dependency audit and a secret scan; a threat model and attack tests for security-relevant changes
- Browser smoke UI changes exercised in a real browser: styles, Back/Forward, a console with no errors
The full list: workflow and test standards.
Skills
Nine slash commands in Claude Code, each a thin entry point into the docs. They also trigger on plain requests ("add a background job that…").
-
/offrails:newUse it when you start a new app in an empty directory.
/offrails:new bookshelf: people track books they've read -
/offrails:featureUse it when you want something added or changed, or you edited
product/(no argument: build what changed)./offrails:feature let people review books; email the owner -
/offrails:scaffoldUse it when you need a full resource: migration, model, policy, controller, routes, views and tests.
/offrails:scaffold Book title:string author:string published_on:date? rating:integer owner:references -
/offrails:generateUse it when you need one piece: a model, resource, migration, job, mailer, channel, authentication or task.
/offrails:generate job weekly_digest -
/offrails:checkUse it when you want to know if it's green: it runs the Definition of Done and fixes what fails.
--deepadds mutation testing./offrails:check --deep -
/offrails:doctorUse it when you want an audit against every convention, especially after someone edited code by hand.
--fixrepairs what it finds./offrails:doctor --fix -
/offrails:securityUse it when you're about to ship, or changed auth, payments, uploads, realtime or an API: threat model, scanners, headers, attack tests.
/offrails:security uploads -
/offrails:deployUse it when you want it in production: Fly.io, Kamal, Railway or Render. It confirms before anything outward-facing.
/offrails:deploy fly -
/offrails:upgradeUse it when the session start says the framework has moved on: it brings the app's
.offrails/up to date and migrates the conventions./offrails:upgrade
Guardrails
The plugin installs hooks that enforce the conventions while the agent works, so mistakes are caught on the edit that makes them, not in review. They only act inside an app with a .offrails/ directory.
Blocked before an edit
.offrails/is read-only: it's the vendored framework. Deviations go indocs/decisions.md; updates go through/offrails:upgrade.web/src/types/is generated from Rust by ts-rs. Change the Rust type instead.- Committed migrations are append-only: databases that ran one never see an edit, so the agent writes a new migration.
Flagged after an edit
- Raw SQL outside
src/kernel/: use typed SeaORM queries. - A raw
.begin()transaction: usekernel::db::begin, which takes the SQLite write lock up front. - Banned crates and JS packages.
/:idroutes: axum 0.8 paths are/{id}.sleepin tests: drain the jobs or await the condition instead.
Security tripwires
- Askama
|safe, inline<script>andon*handlers dangerouslySetInnerHTMLand Svelte{@html}unsafe-inlineorunsafe-evalin a CSP- Permissive CORS, SQL built with
format!, cookies with Secure or HttpOnly turned off
Each needs a logged decision and your explicit OK.
Around the session
- No
pkillorkillall: killing by name can take down your other servers. The agent stops only the PID or port it started. - The stop gate: the agent can't end its turn while
cargo fmt --checkorcargo checkfails. - Drift detection: at session start, it compares the app's
.offrails/with the plugin's and mentions/offrails:upgradeonce if they differ.
Choosing modes
Three choices, made once in the interview and recorded in the AGENTS.md header. Every guide has a section per mode, and the agent follows only yours. Switching later is a decision you make, never the agent.
FRONTEND_MODE
htmxdefault- Server-rendered HTML with compile-checked templates and minimal JavaScript. No Node. Pick it for most apps: CRUD, dashboards, content sites. Guide
inertia- React or Svelte pages with server-side routing and controllers, types generated from Rust. Pick it when you want a JS UI. Guide
spa- A separate single-page app plus a JSON API. Pick it only when the UI must be a standalone app.
apiin progress- No UI at all, only an API for mobile apps, partners and scripts. Pick it for a backend with no pages.
UI_LIBRARY
Only with a JS UI: inertia, spa, or htmx with islands.
reactdefault- Pick it for the largest ecosystem.
svelte- Pick it for smaller bundles and less code.
API
A public, versioned API for other programs, independent of the UI.
nonedefault- Pick it when only your own UI talks to the server.
rest- REST under
/api/v1with an OpenAPI document and bearer tokens. Pick it for mobile apps, partners and integrations (the default forapimode). graphql- One
/graphqlendpoint with a schema file. Pick it when clients need to shape their own queries. both- Pick it when you need REST for some clients and GraphQL for others.
Upgrading apps
Each app vendors a pinned copy of the framework in .offrails/, so it keeps working with any agent, with or without the plugin, whatever happens upstream. Nothing changes until you upgrade. It works the same whichever agent built the app.
-
Commit, then get the latest release
Start from a clean git tree.
offrails updateinstalls the latest release into~/.offrails/current(or re-run the one-liner).offrails update -
Check whether the app is behind framework 0.4.0+
It compares a fingerprint of the app's
.offrails/with the installed framework's, so even doc changes without a version bump show up.cd my-app && offrails upgrade --check -
Upgrade framework 0.4.0+
It launches your agent with the upgrade recipe: Claude Code runs
/offrails:upgrade; opencode and Codex get the recipe automatically; for any other agent, paste the prompt it prints.offrails upgradeAlready in Claude Code? Run the skill directly:
/offrails:upgradeOn a release before 0.4.0, after
offrails update, give your agent this prompt in the app:Upgrade this Rust off Rails app to the framework in ~/.offrails/current, following https://github.com/rust-off-rails/rustoffrails/blob/main/generators/upgrade.md step by step. Reconcile, don't re-create; never edit applied migrations. Start from a clean git tree, show me the plan before changing anything, and finish with the Definition of Done on SQLite and Postgres.
What the agent does
It follows the framework's upgrade recipe, the same steps in every agent:
- Reads the CHANGELOG's App migration steps from your version up, and diffs the old docs against the new to catch anything the changelog doesn't mention.
- Reconcile, don't re-create. Each change is classified: new (build it), present and conforming (nothing to do), present but divergent (reshape it in place, with a new migration that keeps your data), or conflicting with a product decision (kept, recorded, and raised with you only if it blocks).
- Never edits an applied migration. Schema changes are new migrations with a working rollback.
- Shows you the plan first and waits for your yes. Then it replaces
.offrails/and applies the plan in slices. - Finishes green: the Definition of Done on SQLite and Postgres, the upgrade logged in
docs/decisions.md.
Rules of thumb
- Commit first, so the upgrade is one reviewable diff.
- Several releases behind? Upgrade one release at a time.
- Drift is detected by content, not version. In Claude Code, the session start compares fingerprints and mentions
/offrails:upgradeonce; it never upgrades unprompted.
FAQ
The short version. The full FAQ covers contributing, models, cost, security and more.
Can I edit the code myself?
Yes, nothing stops you. The rule is that humans write intent, but when you paste a fix, the agent doesn't revert it: it makes it conform to the conventions, records the intent behind it in the spec or docs/decisions.md so it survives future changes, and owns that code again from then on. /offrails:doctor also looks for hand edits. Usually it's faster to tell the agent what you wanted.
Which agents work?
Claude Code with the plugin is the recommended setup: you get the skills and the guardrail hooks. Codex, Cursor and any other agent that reads AGENTS.md work from the same Markdown, because the docs are the framework; you just ask in plain words instead of slash commands. How to start without Claude Code.
Can my agent read the docs without installing anything?
Yes. Connect it to the hosted MCP server at https://rustoffrails.org/mcp: it can search the docs, read any guide, list the skills and hand you a starter prompt. It's read-only and needs no account. To build an app you still install the framework (and, in Claude Code, the plugin).
Is it production-ready?
Honestly: it's young. It is dogfooded on real apps (a bookshelf with accounts and live reviews, an API-only demo, and this website), and its guides are verified against the current versions of the crates they use, recorded in the docs. What's unfinished is marked "in progress". Every app it builds carries its own test suite on SQLite and Postgres, a security checklist and a Definition of Done, so you can judge your app by its tests, not by our word. Run /offrails:security before you ship anything sensitive.
Why Rust?
Because the compiler is the best reviewer an agent can have. Queries are typed Rust, templates are checked at compile time, and front-end types are generated from Rust, so most of an agent's mistakes become compile errors in seconds instead of bugs in production. What types can't prove, the tests prove on both databases. And the result is one fast binary in one Docker image. The full doctrine.