Rails → Off Rails
Coming from Rails?
Every Rails concept and its Rust off Rails equivalent. When a human, or your training data, says "the Rails way", look it up here.
95 entries
Frameworks
Rails
Off Rails
Guide
Active Record Migrations
migration/ crate (sea-orm-migration DSL), embedded in the binary
migrations
Hotwire (Turbo + Stimulus)
htmx mode: htmx 2.x + htmx-ext-sse (+ static/app.js for small behavior)
frontend-htmx
tailwindcss-rails
Tailwind v4 standalone CLI pinned in
bin/setup-tailwind (htmx, islands) / @tailwindcss/vite (inertia, spa)
frontend-htmx
graphql-ruby (+ graphiql-rails, graphql-batch)
async-graphql:
src/graphql/, /dev/graphiql, a DataLoader per association
graphql
rswag / OpenAPI docs
utoipa annotations →
/api/openapi.json, committed docs/openapi.json, Scalar at /dev/api-docs
api
has_secure_token API tokens / Doorkeeper
api_tokens (SHA-256 digests, scopes, expiry), ApiUser extractor
api
docker compose up (Rails 7.1+ devcontainer)
compose.yaml from new: the production image + Postgres, profiles worker, mail
deployment
favicon.ico, meta-tags gem
brand.toml + cargo offrails brand + Ctx meta (title, description, canonical, OG, Twitter) from new
views
ViewComponent, a UI kit,
layout "admin"
{% import "components/ui.html" as ui %} macros; {% extends "layouts/sidebar.html" %} (application, sidebar, marketing, auth, docs)
views
Capybara screenshots, a Percy-style visual review
BASE=… cargo offrails prove → docs/proof/PROOF.md
testing
inertia-rails
inertia mode: generated kernel/inertia.rs (Inertia v3 protocol) + @inertiajs/react
frontend-inertia
jsbundling / vite_rails
Vite 8 in
web/ (inertia, spa, islands), manifest read by kernel/assets.rs
frontend-inertia
letter_opener(_web), mailer previews
/dev/mailbox (Mailer::Local), with mailer previews in its Previews tab
mailers, dev-tools
Action Mailbox
Not included. Webhook controller if a spec needs it
Action Cable channels (two-way)
One WebSocket, topics, room actors (
kernel/hub.rs, kernel/channels.rs)
channels
Active Support
std +
chrono. No monkey patching exists to port
rails new (+ its flags)
interview.md → generators/new.md
rails generate <kind> …
cargo offrails generate <kind> … (g): migration, model, scaffold (resource), controller, page, api, job, mailer, channel, test; optional, like a snippet
generators
a Rails guide's code sample, copied
cargo offrails snippets, snippet show <name>, snippet add <name> [--as <model>] (feed, slugs, Markdown, pager, theme toggle, backdated seeds, public page by slug)
snippets
rails destroy <kind> …
cargo offrails destroy <kind> <name> (d): only files nobody changed
generators
Engines / gems that add features
parts:
cargo offrails parts, add <part>, update-part <part>
template
I18n
English-only by default. If a spec requires locales, add
rust-i18n and log the decision
Active Record concepts
Rails
Off Rails
Guide
Post.find(id)
post::find(&db, id).await? → AppError::NotFound on miss
Post.where(...), scopes
Named query fns: post::published(&db, page), post::by_author(&db, id)
post.save
post::create(&db, author_id, new) / post::update(&db, post, new)
has_many :comments
Relation::Comments + comment::for_post(&db, post_id)
belongs_to :author
author_id: i64 + FK + Relation::Author; post.find_related(user::Entity)
has_many :through
Join entity + Related impl with via()
includes(:comments) (N+1 fix)
find_also_related (1:1) or LoaderTrait::load_many (1:N)
Validations
Params::validate(), plus DB constraints as the backstop
validates :email, uniqueness:
UNIQUE index + map the unique violation to a field error
Callbacks (
after_create)
Banned. Call the side effect explicitly in the model fn
Concerns
Traits only when ≥3 models share behavior; otherwise duplicate
STI
A
kind column + DeriveActiveEnum
Polymorphic associations
Avoid. Separate nullable FKs with a
CHECK that exactly one is set
enum status: {...}
Text column + CHECK constraint + DeriveActiveEnum
counter_cache
Compute with COUNT(*); denormalize only for a measured reason
dependent: :destroy
on_delete(Cascade) on the FK in the migration
Soft delete (
paranoia)
discarded_at TIMESTAMPTZ + queries filter it
touch: true, updated_at
Set updated_at = Set(Utc::now()) in every update fn
Transactions
let txn = kernel::db::begin(db).await?; … txn.commit().await? (BEGIN IMMEDIATE on SQLite)
find_each
Keyset pagination loop (WHERE id > $1 ORDER BY id LIMIT 1000)
Pagination (pagy/kaminari)
Page struct in models/mod.rs → .limit().offset()
Search (pg_search/ransack)
Portable
LIKE; full-text via the escape hatch (tsvector / FTS5)
Action Controller concepts
Rails
Off Rails
Guide
before_action :authenticate
RequireUser extractor as a handler argument
params.require(:post).permit(...)
Form<PostParams>. The struct is the permit list
redirect_to posts_path, notice:
Flash::notice(jar, "…") + Redirect::to(&paths::posts())
render :new, status: :unprocessable_entity
Re-render form view with errors, status 422
respond_to { format.json }
Separate API handlers under /api/v1; no content negotiation
rescue_from
AppError → IntoResponse in error.rs
helper_method :current_user
CurrentUser extractor passed into the view struct
Strong parameters
Typed
Deserialize structs; unknown fields ignored
posts_path, post_path(@p)
paths::posts(), paths::post(id) functions in routes.rs
Pundit / CanCanCan
policies/<resource>.rs pure fns: can_edit(user, post) -> bool
Borrowed from Phoenix / Elixir
Phoenix / Elixir
Off Rails
Guide
Oban (jobs as rows with a state machine, kept history, queue limits, pause/resume, snooze/cancel, unique jobs)
kernel/jobs/
jobs
Phoenix.Channel + phoenix.js
Inspired by Phoenix Channels; our own protocol, offrails.v1:
/socket (+ SSE and long-polling fallbacks), typed Room actors, seq resume, static/channels.js
channels
Phoenix.Presence
Hub::track/untrack, presence frames (here, joins/leaves); people, not tabs; minimal metadata
channels
libcluster / Horde
kernel/cluster.rs: cluster_nodes heartbeats, rendezvous ownership, failover rebuilt from the DB
channels
Phoenix / Rails pending-migrations page (
ActiveRecord::PendingMigrationError)
Dev-only page with a Run migrations button; /up 503 in production while pending
dev-tools