.offrails/RAILS_MAP.md

Rails → Off Rails Map

When a human (or your training data) says "the Rails way", look it up here.

Frameworks

RailsOff RailsGuide
Active RecordSeaORM entities + query fns in src/models/models
database.yml / adaptersDATABASE_URL: SQLite or Postgres, same codedatabase
Active Record Migrationsmigration/ crate (sea-orm-migration DSL), embedded in the binarymigrations
Active Model validations<Model>Params::validate() -> Result<New<Model>, Errors>models
Action Controlleraxum handlers in src/controllers/controllers
Routing (routes.rb)src/routes.rs (one file)routing
Action View / ERBAskama templates + view structsviews
Hotwire (Turbo + Stimulus)htmx mode: htmx 2.x + htmx-ext-sse (+ static/app.js for small behavior)frontend-htmx
Propshaft / importmapstatic/ + kernel/assets.rs digests, vendored htmxfrontend-htmx
tailwindcss-railsTailwind v4 standalone CLI pinned in bin/setup-tailwind (htmx, islands) / @tailwindcss/vite (inertia, spa)frontend-htmx
rails new --apiFRONTEND_MODE: api: no UI, REST /api/v1 and/or GraphQL, bearer tokensapi
graphql-ruby (+ graphiql-rails, graphql-batch)async-graphql: src/graphql/, /dev/graphiql, a DataLoader per associationgraphql
rswag / OpenAPI docsutoipa annotations → /api/openapi.json, committed docs/openapi.json, Scalar at /dev/api-docsapi
has_secure_token API tokens / Doorkeeperapi_tokens (SHA-256 digests, scopes, expiry), ApiUser extractorapi
rack-corstower-http CorsLayer from APP_CORS_ORIGINS (exact origins)api
PumaThe 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, maildeployment
favicon.ico, meta-tags gembrand.toml + cargo offrails brand + Ctx meta (title, description, canonical, OG, Twitter) from newviews
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 reviewBASE=… cargo offrails prove → docs/proof/PROOF.mdtesting
inertia-railsinertia mode: generated kernel/inertia.rs (Inertia v3 protocol) + @inertiajs/reactfrontend-inertia
jsbundling / vite_railsVite 8 in web/ (inertia, spa, islands), manifest read by kernel/assets.rsfrontend-inertia
Jbuilder / serializers#[derive(Serialize, TS)] structs in src/views/frontend-react
Active Job + Solid Queuekernel/jobs/: Oban-style DB-backed queue, app workerjobs
Mission Control – Jobs/admin/jobs dashboard + app jobs …jobs-dashboard
Action Mailerlettre + Askama, delivered via jobsmailers
letter_opener(_web), mailer previews/dev/mailbox (Mailer::Local), with mailer previews in its Previews tabmailers, dev-tools
Action MailboxNot included. Webhook controller if a spec needs it
Action Cable + Solid CableSSE + broadcasts table (kernel/pubsub.rs)realtime
Action Cable channels (two-way)One WebSocket, topics, room actors (kernel/hub.rs, kernel/channels.rs)channels
Active Storageobject_store + attachments tablestorage
Action TextMarkdown column → pulldown-cmark → ammoniaviews
Solid Cache / Rails.cachekernel/cache.rs: moka L1 + cache_entries tablecaching
Active Supportstd + chrono. No monkey patching exists to port
rails generate authenticationgenerators/authentication.mdauth
Credentials (credentials.yml.enc)Env vars; secrets from the deploy platformconfig
Environments (dev/test/prod)APP_ENV + Configconfig
KamalKamal 2, Fly.io, Render or Railway: the same Docker image everywheredeployment
Rails consoleNo REPL. Write an app task, or use psql via app db consolecli
Minitest / system testsTestApp integration tests via oneshot, on SQLite + Postgrestesting
Fixtures / FactoryBottests/app/factories.rstesting
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 snippetgenerators
a Rails guide's code sample, copiedcargo 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 changedgenerators
Engines / gems that add featuresparts: cargo offrails parts, add <part>, update-part <part>template
bin/rails commandsthe app binary + cargocli
Rack middlewaretower layers in routes.rsrouting
I18nEnglish-only by default. If a spec requires locales, add rust-i18n and log the decision

Active Record concepts

RailsOff Rails
Post.find(id)post::find(&db, id).await? → AppError::NotFound on miss
Post.where(...), scopesNamed query fns: post::published(&db, page), post::by_author(&db, id)
post.savepost::create(&db, author_id, new) / post::update(&db, post, new)
has_many :commentsRelation::Comments + comment::for_post(&db, post_id)
belongs_to :authorauthor_id: i64 + FK + Relation::Author; post.find_related(user::Entity)
has_many :throughJoin entity + Related impl with via()
includes(:comments) (N+1 fix)find_also_related (1:1) or LoaderTrait::load_many (1:N)
ValidationsParams::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
ConcernsTraits only when ≥3 models share behavior; otherwise duplicate
STIA kind column + DeriveActiveEnum
Polymorphic associationsAvoid. Separate nullable FKs with a CHECK that exactly one is set
enum status: {...}Text column + CHECK constraint + DeriveActiveEnum
counter_cacheCompute with COUNT(*); denormalize only for a measured reason
dependent: :destroyon_delete(Cascade) on the FK in the migration
Soft delete (paranoia)discarded_at TIMESTAMPTZ + queries filter it
touch: true, updated_atSet updated_at = Set(Utc::now()) in every update fn
Transactionslet txn = kernel::db::begin(db).await?; … txn.commit().await? (BEGIN IMMEDIATE on SQLite)
find_eachKeyset 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

RailsOff Rails
before_action :authenticateRequireUser 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_entityRe-render form view with errors, status 422
respond_to { format.json }Separate API handlers under /api/v1; no content negotiation
rescue_fromAppError → IntoResponse in error.rs
helper_method :current_userCurrentUser extractor passed into the view struct
Strong parametersTyped Deserialize structs; unknown fields ignored
posts_path, post_path(@p)paths::posts(), paths::post(id) functions in routes.rs
Pundit / CanCanCanpolicies/<resource>.rs pure fns: can_edit(user, post) -> bool

Borrowed from Phoenix / Elixir

Rails isn't the only source. Where Elixir did it better, we copied it:

Phoenix / ElixirOff RailsGuide
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 pluginsrc/jobs/recurring.rs + recurring_executionsjobs
Oban Lifeline / Pruner / Stager pluginsKernel plugins run by every workerjobs
Oban.Testing (assert_enqueued, perform_job, drain_queue)TestApp helpersjobs
Swoosh local adapter + /dev/mailboxMailer::Local + /dev/mailboxmailers
Phoenix LiveDashboard/dev (modest: mailbox, jobs, sockets, routes, system)dev-tools
Phoenix.Channel + phoenix.jsInspired by Phoenix Channels; our own protocol, offrails.v1: /socket (+ SSE and long-polling fallbacks), typed Room actors, seq resume, static/channels.jschannels
Phoenix.PresenceHub::track/untrack, presence frames (here, joins/leaves); people, not tabs; minimal metadatachannels
Phoenix.PubSubper-topic in-process broadcast + Postgres NOTIFY between nodeschannels
libcluster / Hordekernel/cluster.rs: cluster_nodes heartbeats, rendezvous ownership, failover rebuilt from the DBchannels
Phoenix / Rails pending-migrations page (ActiveRecord::PendingMigrationError)Dev-only page with a Run migrations button; /up 503 in production while pendingdev-tools
Ecto changesets<Model>Params::validate() -> Result<New<Model>, Errors>models
mix phx.routesapp routes / /dev/routescli