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 SeaORM entities + query fns in src/models/ models
database.yml / adapters DATABASE_URL: SQLite or Postgres, same code database
Active Record Migrations migration/ crate (sea-orm-migration DSL), embedded in the binary migrations
Active Model validations <Model>Params::validate() -> Result<New<Model>, Errors> models
Action Controller axum handlers in src/controllers/ controllers
Routing (routes.rb) src/routes.rs (one file) routing
Action View / ERB Askama templates + view structs views
Hotwire (Turbo + Stimulus) htmx mode: htmx 2.x + htmx-ext-sse (+ static/app.js for small behavior) frontend-htmx
Propshaft / importmap static/ + kernel/assets.rs digests, vendored htmx frontend-htmx
tailwindcss-rails Tailwind v4 standalone CLI pinned in bin/setup-tailwind (htmx, islands) / @tailwindcss/vite (inertia, spa) frontend-htmx
rails new --api FRONTEND_MODE: api: no UI, REST /api/v1 and/or GraphQL, bearer tokens api
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
rack-cors tower-http CorsLayer from APP_CORS_ORIGINS (exact origins) api
Puma The binary itself: axum on hyper (HTTP/1.1 + h2); optional direct TLS via APP_TLS_* deployment
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
Jbuilder / serializers #[derive(Serialize, TS)] structs in src/views/ frontend-react
Active Job + Solid Queue kernel/jobs/: Oban-style DB-backed queue, app worker jobs
Mission Control – Jobs /admin/jobs dashboard + app jobs … jobs-dashboard
Action Mailer lettre + Askama, delivered via jobs mailers
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 + Solid Cable SSE + broadcasts table (kernel/pubsub.rs) realtime
Action Cable channels (two-way) One WebSocket, topics, room actors (kernel/hub.rs, kernel/channels.rs) channels
Active Storage object_store + attachments table storage
Action Text Markdown column → pulldown-cmark → ammonia views
Solid Cache / Rails.cache kernel/cache.rs: moka L1 + cache_entries table caching
Active Support std + chrono. No monkey patching exists to port
rails generate authentication generators/authentication.md auth
Credentials (credentials.yml.enc) Env vars; secrets from the deploy platform config
Environments (dev/test/prod) APP_ENV + Config config
Kamal Kamal 2, Fly.io, Render or Railway: the same Docker image everywhere deployment
Rails console No REPL. Write an app task, or use psql via app db console cli
Minitest / system tests TestApp integration tests via oneshot, on SQLite + Postgres testing
Fixtures / FactoryBot tests/app/factories.rs testing
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
bin/rails commands the app binary + cargo cli
Rack middleware tower layers in routes.rs routing
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
Oban Web /admin/jobs (live, htmx-polled) jobs-dashboard
Oban Cron plugin src/jobs/recurring.rs + recurring_executions jobs
Oban Lifeline / Pruner / Stager plugins Kernel plugins run by every worker jobs
Oban.Testing (assert_enqueued, perform_job, drain_queue) TestApp helpers jobs
Swoosh local adapter + /dev/mailbox Mailer::Local + /dev/mailbox mailers
Phoenix LiveDashboard /dev (modest: mailbox, jobs, sockets, routes, system) dev-tools
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
Phoenix.PubSub per-topic in-process broadcast + Postgres NOTIFY between nodes 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
Ecto changesets <Model>Params::validate() -> Result<New<Model>, Errors> models
mix phx.routes app routes / /dev/routes cli