# Elements vs Ruby on Rails
Ruby on Rails is an open-source web app framework written in Ruby. David Heinemeier Hansson created it and first released it in 2004, and the Rails core team maintains it. Rails is free. Its community page credits its progress to "the work of thousands of volunteers" ([community](https://rubyonrails.org/community)), and the Rails Foundation, funded by member companies, works on its documentation, education, marketing and events ([foundation](https://rubyonrails.org/foundation)). Rails is organized around convention over configuration: it decides where code goes and how the pieces connect. It ships libraries for each part of an app: Active Record for database models and migrations, Action Controller and Action View for handling requests and rendering ERB templates, Hotwire (Turbo and Stimulus) for interactive pages, Action Cable for realtime updates, Active Job for background work, Action Mailer for email, and Active Storage for file uploads. New Rails 8 apps come configured to deploy with Kamal, which runs the app in Docker containers on servers you control.
Elements is an integrated app environment for building web apps, built for people and their agents: a project server that runs while you work, a package installer, a test runner, a bundled Postgres database with migrations, and a deploy command for any Ubuntu server you reach over SSH. It ships with a default framework that just works, with routing, server rendering, typed server functions, sessions, file uploads, realtime data, background jobs and email. Its pages are written in Elements HTML: standard HTML with reactive expressions. Elements apps are written in TypeScript and run on Node.js. The project server holds the build graph and build state in memory, so it builds in milliseconds and answers agents and humans in microseconds.
Elements is ready the moment you create a project. A new Rails 8.1 app is assembled from 23 gems before you write a line ([Gemfile template](https://github.com/rails/rails/blob/v8.1.0/railties/lib/rails/generators/rails/app/templates/Gemfile.tt)): Puma to serve it, Propshaft for assets, importmap for browser code, Turbo and Stimulus for interactivity, Solid Queue, Solid Cache and Solid Cable for jobs, caching and realtime, Kamal and Thruster to deploy, and Capybara and Selenium to test. Each is its own project with its own configuration, commands and docs, and every seam between them is a place where a person or an agent gets stuck. Over the parts sit conventions the agent has to remember, and nothing checks the agent's guesses until that line runs. Elements is one coherent system and stays close to the metal: HTML, CSS, TypeScript, SQL and function calls, with a framework vocabulary of `app.route()`, `session.login()`, `sql()`, `@rpc` and tests. One build reports every error together, type checking, tests, migrations and program analysis, so each mistake reaches you or your agent with a file and a line moments after you save.
| | Elements | Ruby on Rails 8.1 |
|---|---|---|
| Business model | Sells the tooling, per machine, with nothing metered ([pricing](/pricing)); the app framework is MIT licensed ([licenses](/learn/man/licenses)) | Free; credited to volunteers, with a foundation funded by member companies for docs, education, marketing and events ([foundation](https://rubyonrails.org/foundation)) |
| What a new app is made of | One coherent system: project server, default framework, Postgres, tests and deploy | 23 gems in the generated Gemfile, each its own project |
| Server and browser logic | TypeScript for both | Ruby on the server, JavaScript in the browser |
| Build errors | [One build](/learn/man/build) reports type checking, tests, migrations and program analysis, as you work | No type checking by default; tests and migrations are separate commands |
| What runs in development | A project server that runs while you work, builds in milliseconds and serves the app | `bin/rails server` starts the app |
| Default data access | Plain SQL through `sql()`, safe from SQL injection by construction | Active Record ORM |
| Migrations | SQL files, applied when you save one | Ruby migration files, applied with `bin/rails db:migrate` |
| Server call from the browser | A typed function call (`@rpc`), checked at build time | A url, parameters and JSON, checked when the request runs |
| Interactive UI | One reactive html language, server rendered and live in the browser | ERB with Turbo, plus Stimulus controllers in JavaScript |
| Live updates | Changes appear at once for the user who made them and in every open browser | Turbo broadcasts: after a model commits, the server renders HTML and sends it to open pages |
| Sessions | One API for route handlers, server calls and templates; the page updates on sign-in and sign-out | `session` in controllers; sign-in generated by `bin/rails generate authentication` |
| Background jobs (production default) | Saved in your transaction, so a job commits with your data or not at all | Solid Queue, in a separate database; by default a job still runs if your transaction rolls back |
| Tests | Part of the build, and a gate on every deploy | `bin/rails test`, `bin/ci`, or a CI workflow after you push |
| Deploy | Peer to peer over SSH, changed files only, often under a second after the first deploy | Kamal builds a Docker image, pushes it to a registry, pulls it on each server |
| Automatic TLS | Let's Encrypt for every domain, on every machine | Let's Encrypt on a single server |
## Rails Runs Your App. Elements Also Runs Your Build.
While you work on a Rails app, `bin/rails server` starts [Puma, the Rails web server](https://guides.rubyonrails.org/getting_started.html), which serves your pages and reloads changed code on the next request. When a migration is pending, Rails tells you, and you run it yourself. Nothing checks the code you saved, runs the tests it affects, or applies a migration when you save one: migrating, testing and deploying are separate commands, each started by hand. Choose one of the four bundlers `rails new` offers in place of import maps ([app generator](https://github.com/rails/rails/blob/v8.1.0/railties/lib/rails/generators/rails/app/app_generator.rb)), and development becomes two processes, the server and a build watcher, started together by `bin/dev` ([jsbundling-rails](https://github.com/rails/jsbundling-rails)). The development server also holds on to memory: "[ActionView leaks memory in dev](https://github.com/rails/rails/issues/35032)" has been open since January 2019, with 56 reactions.
Elements runs a project server: a server that runs while you work. It holds the build graph, meaning every file and what depends on it, and the build state in memory. On every save, the build state is updated almost instantly, because the server redoes only what the change touched. A single-file save often builds and hot reloads in a few milliseconds: the new code is swapped into the running app without a restart ([build](/learn/man/build)). A build in Elements gets the app ready for release, and it reports every type error, failing test and failed migration.
Rails went the other way on builds: a new app has no JavaScript build step, and import maps load browser code [without bundling or Node.js](https://guides.rubyonrails.org/asset_pipeline.html). Elements keeps the build, because a build that finishes in milliseconds is where mistakes are caught, with a file and a line.
## For an Agent, the Value Is in Correction
Rails now pitches convention to AI coding agents: its home page says ["Accelerate your agents with convention over configuration"](https://rubyonrails.org/) and promises "token-efficient code that's easy for agents to write." For an agent, the value is not in conventions and endless framework abstractions, or in a gem for each job. It is in correction: finding out what it got wrong the moment it gets it wrong.
Rails gives an agent little of that. A convention is a rule the agent has to remember, and nothing enforces it. Ruby has no type checker by default, so a misremembered method name, instance variable or file location surfaces only when that line runs, if a test happens to reach it. Gems add to what the agent has to remember, starting with the 23 a new app is generated with. A gem is a shortcut when it does something the agent cannot write quickly from standards, and that list is short: an agent would rather write HTML and CSS directly than learn a view component gem, it does not need a dependency for what a few lines of code do, and every gem past that list brings its own configuration, its own API to look up and one more seam to get wrong.
On a line from an agent writing machine code, which nobody does, to a package that does subtraction, the right amount of framework sits at the plumbing every app needs. Elements ships that plumbing: routing, typed server calls with `@rpc`, sessions, `sql()` and the database driver. Past that plumbing comes a point of diminishing returns, where each further abstraction costs an agent more to learn and get right than it saves. So Elements stops there, keeps abstractions to a minimum and puts its engineering into correction. A page is HTML and CSS, a server call is a TypeScript function marked `@rpc`, and data goes through SQL in `sql()`. The vocabulary on top is `app.route()`, `session.login()`, `sql()`, `@rpc` and tests. `elements create` scaffolds pages, templates, migrations, jobs and email the way Rails generators do, and what an agent writes into them is plain TypeScript, HTML, CSS and SQL ([cli](/learn/man/cli)).
One coherent system checks every page, function, test and migration the agent writes. An agent runs `elements build -json` and gets its answer in microseconds, because the project server already holds the build state ([build](/learn/man/build)). A failing test, a broken migration, a template reading a field that does not exist or a wrong argument type comes back as a diagnostic: an error message with a file and a line, reported before anything runs.
The same server lets several agents work at once. Your terminal, your editor and any number of agents connect to the one project server, so they see the same build state, and a single build loop orders their changes so two agents never build over each other.
## One Language for Server and Browser, Checked End to End
A Rails app uses Ruby for models and controllers, ERB for views and JavaScript for browser behavior ([Getting Started](https://guides.rubyonrails.org/getting_started.html)). Nothing checks them together by default. An instance variable the controller never set is `nil` in the view, so the page renders blank or raises, and only when someone visits it.
An Elements app uses TypeScript for server and browser code alike, and the project server checks every part of it in one build: type checking, tests, migrations and program analysis ([build](/learn/man/build)). Templates are written in Elements HTML and are part of the same build. A template declares typed attributes the way a function declares parameters, so a page that passes a template the wrong value is a build error, and so is a typo such as `{comment.txet}`.
## Server Calls Are Typed Function Calls
Every web app has a boundary between code that runs on the server, where the database and secrets live, and code that runs in the browser. In Rails, each interaction crosses that boundary by hand: a route, a controller action, [strong parameters](https://guides.rubyonrails.org/action_controller_overview.html) to filter the input, and a form or JavaScript that calls a url. Path helpers catch a wrong route name when the line runs, but nothing checks at build time that the url, the parameters and the response agree. A renamed parameter is dropped without an error by strong parameters, and a changed JSON shape breaks the browser code that reads it. Both show up only at runtime.
Elements crosses the boundary with `@rpc` functions. An `@rpc` is a server function, marked with an `@rpc` tag in its doc comment, that the browser calls directly with typed parameters and a typed return value ([rpc](/learn/man/rpc)). You call it from an event handler like any other function. The compiler turns the browser call into a network request and keeps the function body, and everything it depends on, out of the browser bundle.
Because server and browser code can sit side by side in one file, the compiler also guards the boundary. Calling server-only code such as `sql`, `tx`, `session.login` or `email` from code that can run in the browser is a compile error at the call site, so a query or a secret cannot reach the browser by accident:
```ehtml
```
File uploads go through the same typed call. Rails handles uploads with [Active Storage](https://guides.rubyonrails.org/active_storage_overview.html), which attaches files to models and stores them on disk or with a cloud storage service. In Elements, files are a `File[]` field on the form object an `@rpc` receives: bind `` ([file upload](/learn/man/recipes/file-upload)). There is no separate upload endpoint to wire up, and the files arrive typed alongside the rest of the form. What you do with them next is ordinary server code: the [database assets](/learn/man/recipes/database-assets) recipe keeps them in Postgres, and any npm storage client works in the same `@rpc`.
## SQL Is the Default, and a Migration Applies When You Save It
A Rails app reaches its database through Active Record, an ORM that maps tables to Ruby classes. It is a layer to learn on top of SQL: a query interface of its own, associations, callbacks and validations that run in Ruby rather than in the database ([querying](https://guides.rubyonrails.org/active_record_querying.html), [validations](https://guides.rubyonrails.org/active_record_validations.html)). The layer leaks. "[accepts_nested_attributes_for breaks the uniqueness with scope validation](https://github.com/rails/rails/issues/20676)", a uniqueness check that lets duplicates through when records are saved together, has been open since June 2015, with 67 reactions and 76 comments.
Elements makes SQL the default, because SQL is the query language every agent and every database tool already speaks. `sql()` takes a plain SQL template, and the build turns every `${value}` in it into a separate query parameter at compile time, so an interpolated value cannot inject SQL ([database/sql](/learn/man/database/sql)).
Migrations are SQL too. Rails migrations are Ruby files [applied by running `bin/rails db:migrate`](https://guides.rubyonrails.org/active_record_migrations.html), each in its own transaction. An Elements migration is a SQL file, and saving it applies it ([migrations](/learn/man/migrations)). Edit an applied migration in development and Elements rolls it back and applies the new version. Pending migrations run together in one Postgres transaction, against the test database first, so a batch applies in full or not at all.
## One Reactive HTML Language, Rendered on the Server and Live in the Browser
Both frameworks render pages on the server and then make them interactive in the browser. Hotwire cuts the custom JavaScript a Rails screen needs by sending HTML from the server: Turbo swaps in new pages and page fragments, and [Turbo Streams](https://turbo.hotwired.dev/handbook/streams) append, replace or remove elements by id. Each interaction is still a round trip that renders HTML on the server. Anything that should happen locally, such as an input that filters a list as you type, needs a [Stimulus](https://stimulus.hotwired.dev/handbook/introduction) controller written in JavaScript, so one screen is split across a Ruby controller, an ERB view and a JavaScript file, joined by `data-` attribute names that nothing checks. The seams show in Hotwire's own trackers. "[Turbo frame history not navigating properly w/ back button after upgrading from 7 to 8](https://github.com/hotwired/turbo/issues/1241)" has been open since April 2024, with 25 reactions and 31 comments, and "[Turbo new install - forms do not redirect](https://github.com/hotwired/turbo-rails/issues/122)" has been open since February 2021, with 76 comments.
In Elements, the same template runs on the server and in the browser. Elements HTML is standard HTML plus a few attributes, such as `e:if`, `e:for`, `e:switch` and `group`, and reactive expressions in braces ([html](/learn/man/html)). A page renders fully on the server, so search engines and first-time visitors get the complete page. The browser then picks up the server-rendered page and makes it interactive, and when data changes, the runtime updates only the elements that depend on it. Local interactions run locally: an input bound to a field updates the field as the user types, and the field updates the input, with no extra code. Event handlers are TypeScript in the same file as the markup, and they call `@rpc` functions directly.
## Live Updates Paint Instantly and Stay in Sync
Live updates push changes to browsers that are already open, such as a new comment or an edited row. Rails does this with Turbo broadcasts: a model declares `broadcasts_to` or `broadcasts_refreshes`, and after each commit Rails [renders HTML in a background job and sends it over Action Cable](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb), or tells each browser to refresh the page. In production, a new Rails 8 app carries those messages through [Solid Cable](https://github.com/rails/solid_cable), which stores them in a database table and polls it for new ones. Every update, including the user's own, waits for a round trip to the server before it appears.
Elements builds live updates on Postgres notifications (`LISTEN`/`NOTIFY`): every app server listens and forwards each message to its connected browsers as it arrives ([channel](/learn/man/channel)). On top of that sits LiveTable, a live, ordered set of typed rows that the page reads and writes directly ([livetable](/learn/man/livetable)). A LiveTable change uses optimistic UI: it paints in the browser that made it at once, while the server confirms it, and every other watching browser patches the affected row in place.
## Sessions Are Live Across HTTP, WebSockets and Templates
Rails has a session in every controller, kept in an encrypted cookie, and Rails 8 generates authentication code on top of it. [`bin/rails generate authentication`](https://guides.rubyonrails.org/security.html) writes `User` and `Session` models, controllers, an `Authentication` concern, password reset mail and migrations into your app, and you maintain that code from then on. It does not generate a sign-up flow.
Elements sessions are one API across the app: route handlers, `@rpc` functions and templates read the signed-in user the same way, over http and WebSockets ([session](/learn/man/session)). A template that reads the session updates the moment a user signs in or out, with no page reload. What your app writes is the sign-in check itself, a short `@rpc` that verifies a password hash and calls `session.login()`; the [authentication recipe](/learn/man/recipes/authentication) covers sign-in and sign-up.
## Email Templates Are Checked Like Pages
Rails sends mail with Action Mailer, whose views are ERB templates, so a missing variable can show up as a blank in an email that has already been sent. Elements email templates are written in Elements HTML, type checked like any page, and the compiler inlines their stylesheets into one `