.offrails/template/README.md

The app template

TL;DR

  • template/app/ is a complete, compiling, tested blank app (htmx). A new app is a copy of it, renamed, with this framework vendored into .offrails/: one command, no code retyped.
  • template/parts/<name>/ are the optional kernel parts (mailer, dev mailbox, jobs, jobs dashboard, accounts, realtime, dev dashboard). They are the samples, vendored into the release. Add one at any time with cargo offrails add-part <name>.
  • template/snippets/<name>/ are small, tested code to copy (a feed, slugs, Markdown, a pager, a theme toggle, backdated seeds, a public page by slug, a CRUD resource): cargo offrails snippets, snippet show <name>, snippet add <name> (snippets).
  • The generators are tools/offrails-gen, a standard-library-only Rust tool: the same commands on Linux, macOS and Windows. Snippets and generators are both optional starting points; whatever you start from, a feature is small files in its own folders plus one marked line per shared spot.
  • Pages are built from the component kit, templates/components/ui.html (every component at /dev/components), in one of five layouts, branded by brand.toml (cargo offrails brand); generated and snippet pages use the kit. BASE=… cargo offrails prove screenshots and checks every page and writes docs/proof/PROOF.md (views, testing).
  • After generating, the app's code is the app's: agents read it, adapt it and own it, as if they had written it. The guides stay the spec for why.

Read Using it to make an app, Choosing parts to pick parts, Other modes for inertia/spa/api.

Using it

A new app (after the interview, new). $FW is the framework: the plugin root, ~/.offrails/current, or a checkout.

cargo run -q --manifest-path "$FW/tools/offrails-gen/Cargo.toml" --target-dir bookshelf/target/offrails-gen -- \
  new bookshelf bookshelf "Bookshelf" --pitch "track the books you read" --parts dev-dashboard,mailer,dev-mailbox
# Linux / macOS / Git Bash shorthand: sh "$FW/tools/new-app.sh" bookshelf bookshelf "Bookshelf" --parts …

new <dir> <crate_name> "<Display Name>" copies template/app/ into <dir>, replaces the tokens below, vendors $FW into <dir>/.offrails/ (the new excludes), writes AGENTS.md and product/README.md from the blueprints, logs the generation in docs/decisions.md, adds --parts (each with its dependencies), runs cargo fmt, and git inits. It refuses a non-empty directory. Options: --pitch "<one line>" (the home page tagline and meta description), --parts a,b|all|none (default none), --mode htmx|inertia|spa|api (see other modes), --prod-db postgres|sqlite, --no-git, --offline (passes --offline to cargo add), --framework <path>. Nothing networked runs unless parts are asked for: a part's crates arrive with cargo add, which needs the crates.io index (cached or online).

In an app, from its root, through the alias in .cargo/config.toml:

cargo offrails parts                      # every part: installed or available, what it needs, what it adds
cargo offrails add mailer                 # the part and its dependencies; prints exactly what it changed and what to run next
cargo offrails add jobs-dashboard --dry-run        # what it would do, changing nothing
cargo offrails update-part mailer --framework "$FW"   # a newer framework's part, merged 3-way into the app's copy
cargo offrails g resource posts title:string body:text   # the generators: migration, model, resource, controller, page, api, job, mailer, channel, test
cargo offrails d resource posts           # undo a generator (files nobody changed)
cargo offrails snippets                   # the snippets; `snippet show <name>` prints one, `snippet add <name>` copies and wires it
cargo offrails snippet remove atom-feed   # take a snippet back (files nobody changed, its marked lines)

The generators (generators) and snippet add (snippets) write a feature's files and its wiring at the same anchors the parts use, each wired line marked with its owner (// posts), format them, and record them in docs/generated.toml (so a re-run is a no-op and destroy / snippet remove removes exactly what they wrote). add is add-part, parts is list-parts.

add-part checks everything before changing anything: a file the part brings that exists and differs, or a missing anchor, refuses with the reason and changes nothing (--force overwrites files). It then runs each cargo add, copies the part's files/ (crate name replaced), inserts each wire above its samples: <anchor> comment, logs installed sample <name> <version> in docs/decisions.md, updates the AGENTS.md header line the part changes (EMAIL for mailer, ACCOUNTS for an auth method), and runs cargo fmt --all. Re-running it is a no-op. It is the samples repo's tools/install.sh in Rust, with the same file format and the same record line, so either tool sees the other's work. Next steps it prints: cargo run -q -- db migrate when the part has tables, cargo run -q -- routes, then the Definition of Done on SQLite and Postgres, and the part's README ("After installing", "Adapting it") and CHECKS.md.

Layout

template/
  app/                 the base app: kernel, layout, error pages, assets, bin/, migration crate, tests/app, CI, Docker
  parts/<name>/        sample.toml (crates, dependencies, wiring), README.md, CHECKS.md, files/ (at app paths)
  parts/SOURCE         the samples commit the parts were synced from (tools/sync-parts.sh)
  snippets/<name>/     snippet.toml (a part's format + `as`, `snippets`), README.md, files/ (at app paths)
tools/offrails-gen/    new, add-part (add), list-parts (parts), update-part, generate (g), destroy (d), snippets, snippet, vendor, verify
                       (Rust, std only; its own workspace; src/generate/ holds the generators, testdata/golden/ their golden files)
tools/new-app.sh       sh shorthand for `new`
tools/ci/test-template.sh   generate bare / all parts / parts added one by one / every generator / every snippet, then the Definition of Done on both databases
tools/ci/memory-budget.sh   boot a generated app (release), serve 1000 requests, fail over an RSS budget

What template/app/ contains. Everything new lists: config.rs (typed env, APP_BIND 127.0.0.1 in development, boot_warnings), error.rs (AppError, shape_errors, Retry-After), the kernel (security.rs header matrix and CSP, hosts.rs development Host allowlist, rate_limit.rs, flash.rs, cookie.rs, redact.rs log and error-text redaction, db.rs SQLite + Postgres pools and the Postgres test template and slots, cache.rs, assets.rs digests, css.rs theme guards, introspect.rs, theme.rs System/Light/Dark switch, tls.rs direct TLS, ui.rs kernel chrome), CSRF (tower-http's layer in routes.rs), /dev tools with the pending-migrations guard, /up, robots.txt, sitemap.xml, the htmx layout with meta and social cards, Tailwind (bin/setup-tailwind, pinned), bin/setup, bin/dev, bin/dev-cert, the migration crate (one table: cache_entries), tests/app/ (TestApp, the Postgres test template and reset slots, and the kernel suites: smoke, headers, security, errors, logs, theme, theme switch, kernel, kernel web), deny.toml, .gitleaks.toml, .zap/rules.tsv, .github/workflows/ci.yml, Dockerfile, compose.yaml, .env.example, docs/decisions.md (the template's own decisions, inherited by every app), .gitattributes, and .cargo/config.toml (the cargo offrails alias). The look: brand.toml (rendered by cargo offrails brand into assets/brand.css, src/brand.rs and the favicon set and social card), five layouts in templates/layouts/ (application = top nav and the shell, sidebar, marketing, auth, docs), the component kit templates/components/ui.html with static/components.js, and /dev/components + /dev/layouts/* showing all of it (views). Every shared file carries the samples: <anchor> comments parts wire into (samples).

Tokens. The template is a real crate named offrails_app, shown as Offrails App, so it builds and its tests run as is. new replaces, in every text file: offrails_app → the crate name, offrails-app → its kebab form (compose project, image), Offrails App → the display name, and, with --pitch, A new Rust off Rails app. (the description) and the home page's sentence → the pitch as a sentence, then A new Rust off Rails app (brand.toml's tagline, the one tagline: config::APP_TAGLINE is brand::TAGLINE) → the pitch capitalized, without its period. Part files use offrails_host (the samples' host crate); add-part replaces that and offrails_app with the app's crate name. To rename by hand (any OS), run the generator; never sed -i (it differs between macOS and Linux and doesn't exist on Windows).

Memory. The base is lean by default: two Tokio workers (APP_WORKER_THREADS), a blocking pool capped at 16 (APP_BLOCKING_THREADS), a SQLite pool of 4 with a 1 MiB page cache per connection and idle connections closed after a minute (database). tools/ci/memory-budget.sh measures it: on macOS (Apple silicon, 2026-10-17) the bare app in release is about 14 MiB after boot and 15 MiB after 1000 requests (debug: 16 / 17 MiB); the app with every part is about 18 MiB after boot and 20 MiB after 1000 requests (debug: 24 / 26 MiB); the budget is 24 MiB bare and 32 MiB with every part.

Choosing parts

The agent decides which parts the app needs from what the human said and the specs in product/, proposes the list with a one-line reason each, and asks the human only to confirm or adjust (interview). The default is the template plus the parts that fit; the human can opt out of any. Record the answer in docs/decisions.md (new and add-part log each part). Use this table so every agent decides the same way:

Signal in the request or a specPart(s)Why
Every app (development only; a read-only subset for admins in production)dev-dashboardLive charts, request traces, SQL with N+1 detection, logs, schema: how agents and humans see the running app
Sends any email: sign-in links, notifications, digests, receipts, a contact formmailer + dev-mailboxTemplated HTML + text mail with SMTP in production, and every email visible at /dev/mailbox in development
Anything slow, retried, scheduled or sent later: emails in the background, imports, digests, reminders, cleanups, webhooks outjobs + jobs-dashboardA database-backed queue with retries, cron and an /admin/jobs console; no extra infrastructure
People sign in (any ACCOUNTS but none)auth-core + one methodUsers, database sessions, the account security page; brings mailer and dev-mailbox
ACCOUNTS: magic-link (or magic-link invite)auth-magic-linkEmailed link plus a 6-digit code
ACCOUNTS: password (or password invite)auth-passwordargon2id, verification, reset
Two-factor, admin accounts that guard money or personal dataauth-totpAuthenticator-app codes with recovery codes
Chat, presence ("who's online"), typing indicators, multiplayer, live updates pushed to other usersrealtimeChannels over WebSocket with SSE and long-poll fallbacks
None of the above (a content site, an internal read-only tool)dev-dashboard onlyThe base already has /dev, the cache, security headers and the theme switch

When the human wants to see what a part is before deciding, point them at https://github.com/rust-off-rails/samples: its README has screenshots of every screen, light and dark. When the product later grows a need, add the part then: cargo offrails add-part <name>. Before hand-writing a mailer, a queue, sessions or a socket layer, run cargo offrails list-parts: if a part does it, add the part (workflow).

Other modes

The template is FRONTEND_MODE: htmx. For the others, new --mode <mode> generates the htmx template (whose kernel, /dev, tests and CI every mode shares), keeps the header at htmx, and logs the requested mode; then convert:

  • api: follow new api: remove templates/home/, templates/shared/, assets/, the Tailwind lines of bin/setup, bin/dev and the Dockerfile, and tests/app/theme_test.rs; replace controllers/home.rs with root.rs (GET / as a JSON index) and make every error JSON outside /dev (api). Set FRONTEND_MODE: api and API: rest once cargo nextest run is green.
  • inertia / spa: add web/ per frontend-inertia or frontend-react / frontend-svelte, replace the page templates with pages, then set the header.

Full templates for these modes are not shipped yet (0.7.1).

Windows

offrails-gen (new, add, parts, update-part, generate, destroy) is plain Rust and runs the same on Windows. The app builds and its SQLite suite runs there (CI matrix: Linux, macOS, Windows). .gitattributes keeps every source file LF, so rustfmt, the anchors and the tests agree across systems. The bin/ scripts are POSIX shell: on Windows use Git Bash or WSL, or the plain commands they wrap:

copy .env.example .env
# Tailwind: download tailwindcss-windows-x64.exe for the TAILWIND_VERSION pinned in bin/setup-tailwind
#   (https://github.com/tailwindlabs/tailwindcss/releases) to bin\tailwindcss.exe, then:
bin\tailwindcss.exe -i assets/app.css -o static/app.css --minify
cargo run -- db setup
cargo run -- serve                                          # one terminal (no auto-rebuild; cargo watch -x "run -- serve" adds it)
bin\tailwindcss.exe -i assets/app.css -o static/app.css --watch   # another

Known gaps on Windows: bin/setup, bin/dev and bin/dev-cert (sh), sh .offrails/tools/offrails-check (sh: run it in Git Bash), tools/ci/memory-budget.sh (uses ps), and the puppeteer browser smokes (run.sh; smoke-boosted.mjs is plain Node and finds Chrome or Edge under Program Files). Postgres is proven on Linux.

Keeping the template, the parts and the guides in sync

  • The guides are the spec (what each part must do, and why). The template is that spec built and tested. Where a guide used to carry a whole file the template now ships, it points at the template file instead and keeps only the excerpts that teach something, so the two can't drift.
  • Parts come from the samples repo, one way: tools/sync-parts.sh <samples-checkout> <ref> copies them verbatim and records the commit in parts/SOURCE. Never edit parts/ here; fix the sample upstream, then sync.
  • The base app is the framework's. template/app/ started as the samples repo's host/ (2026-10-17, samples f5541e9); from 0.7.1 on, host/ is generated from it (new … offrails_host "Offrails Host"), so parts are proven against exactly what apps get.
  • CI proves it: tools/ci/test-template.sh generates the bare app, the app with every part, and a bare app with each part added afterwards (then added again, which must change nothing), and runs the Definition of Done on both databases plus the memory budget.

Decisions

  • 2026-10-17: the framework ships its app skeleton as real, tested files that agents copy (template/), instead of Markdown code they retype (because a skeleton built from the guides cost an agent 50+ minutes and still drifted; the human decided: "add the full skeleton as templates")
  • 2026-10-17: optional kernel parts are the samples, vendored, and added with one command (cargo offrails add-part), by the same anchor/wire mechanism as the samples' install.sh; the base is lean (no jobs, no mailer) and the agent proposes the parts the product needs (because a blog without background jobs shouldn't carry a job queue, and a part added later must be as easy as one added at generation)
  • 2026-10-17: Rails-style generators (cargo offrails generate) write feature code and its wiring instead of agents copying recipes (the human: "can we add actual code generators similar to rails? so the agent can invoke those instead of just copying"); each records what it wrote in docs/generated.toml so destroy and --force touch only unchanged files
  • 2026-10-17: the generators are a std-only Rust tool run through cargo, not shell (because apps are built on Linux, macOS and Windows, and sed -i differs or doesn't exist)