The time has come! You are going to build a real-time web application from scratch with Rails 8. No React, no Vue, no frontend framework at all. Just Rails, the Hotwire stack that ships with it, and a little bit of Stimulus when you need client-side behavior.
First things first, though. We need to set up your development environment and generate the application.
Why Real-Time, Why Rails?
For years, "real-time" on the web meant polling. Your browser would fire off AJAX requests every few seconds, asking "anything new?" and the server would usually say no. It works, and plenty of apps still do, but users can still feel the delay. In a chat app, if someone sends you a message and it takes 5 or 10 seconds to show up, that is a problem.
WebSockets solved the underlying problem. Instead of the browser pestering the server, you open a persistent connection, and the server pushes data to the client whenever something happens. But if you have ever tried to wire up WebSockets by hand, you know how much plumbing is involved. Connection management, serialization, reconnection logic, and then you still have to figure out how to take whatever data comes back and actually update the DOM with it.
Rails 8 handles all of that for you. Turbo sends HTML fragments (not JSON) over the wire, Action Cable manages the WebSocket connections, and Stimulus gives you just enough client-side JavaScript when you need it. The Solid trifecta (Solid Queue, Solid Cache, Solid Cable) means you do not even need Redis to get started. Everything runs on SQLite.
Luckily for us and all Rails developers, rails new in Rails 8 is basically the real-time starter kit. In a JavaScript-heavy stack, you would be piecing together a frontend framework, an API layer, a state management library, a WebSocket client, probably Redis for pub/sub, and a bunch of glue code to hold it all together. Rails 8 ships with all of that out of the box.
The Hotwire Philosophy
Hotwire stands for "HTML Over The Wire." The idea is that your server renders HTML and sends it to the browser, same as Rails has always done, but now it can also push HTML fragments over a WebSocket connection to update parts of a page without a full reload.
Hotwire is really three tools working together:
- Turbo Drive intercepts link clicks and form submissions and fetches the response behind the scenes. It swaps the
<body>content so the user gets single-page-application-style navigation without you having to set up a client-side router or manage any state on the frontend. - Turbo Frames let you break a page into independent sections. Each section loads and updates independently. If you click a link inside a frame, only that frame's content gets replaced, and the rest of the page stays put.
- Turbo Streams are where real-time comes in. A stream delivers a targeted DOM operation (append, prepend, replace, remove, etc.) either as part of an HTTP response or via a WebSocket broadcast to every connected client.
Stimulus is the companion library for client-side behavior. It is not a framework, and you will not be building components or managing state with it. Stimulus gives you small controllers that attach to DOM elements and respond to events, things like toggling a dropdown or copying text to the clipboard.
Action Cable is the WebSocket layer underneath Turbo Streams. When your code broadcasts a Turbo Stream update, Action Cable is what actually delivers it. You can also use Action Cable directly for things that Turbo Streams cannot express, like a typing indicator or live cursor positions.
We will go over all of these in detail throughout the book. For now, just know that they layer on top of each other and each one builds on what the one below it does. Drive is the foundation, Frames and Streams get more targeted from there, and Stimulus is for the small bits of client-side JavaScript that the other tools do not cover.
Introducing Switchboard
The app we are building is called Switchboard. It is a team chat application, think Slack but much simpler, with rooms (you might call them channels), direct messages, and live presence indicators. Chat is a great fit for real-time learning because everything about it is inherently live. Messages should appear the moment they are sent, typing indicators should appear while someone is composing, and user presence (online, away, offline) should update automatically. If any of those things require a page refresh, something is wrong.
Over the course of this book, you will build:
- Real-time messaging with Turbo Streams broadcasting
- Rooms and direct message threads
- A typing indicator powered by Action Cable
- Live online/offline presence
- Unread message badges that update without refreshing
- File attachments and link previews
- Notifications delivered over WebSockets
Each chapter adds a feature, and by the end, Switchboard will be a fully functional real-time chat app.
Prerequisites and Environment Setup
Let's get your machine ready. You need Ruby, the Rails gem, SQLite (which your system almost certainly already has), and a text editor or IDE. That is it. Rails 8 uses Importmaps for JavaScript, so there is no Node.js, no Yarn, no npm, and no JS build step to deal with.
Installing Ruby
You need Ruby 3.4 or newer. Throughout the book, we will use 3.4.9, the latest patch release at the time of writing.
I recommend mise for managing Ruby versions. It handles Ruby, Node, Python, and just about everything else with one tool. I switched to it from asdf a while back and have not looked back.
# Install mise (macOS/Linux)
curl https://mise.run | sh
# Install Ruby
mise use --global ruby@3.4.9
# Verify
ruby --versionIf you already have a preferred version manager (rbenv, asdf, chruby), use that instead. The important thing is that ruby --version reports 3.4.x or higher.
Installing Rails 8
With Ruby in place, install the Rails gem:
gem install rails
rails --versionYou should see something like Rails 8.1.x. If you see a version older than 8.0, make sure your Ruby is on the correct version and try gem install rails again.
It may not be all smooth sailing when installing Rails. I have personally run into OpenSSL issues on a clean system, and the native extensions for the sqlite3 gem can sometimes be finicky depending on your OS version. Predicting the issues you may hit across different machines, operating systems, and library versions is impossible. If you run into something, Google the error message plus your operating system. You will usually find an answer in the first StackOverflow link.
SQLite
Rails 8 defaults to SQLite for development (and honestly, for production too, if your app fits the scale). On macOS, SQLite comes preinstalled. On Ubuntu/Debian:
sudo apt-get install libsqlite3-devYou likely already have it. Run sqlite3 --version to check.
Generating the Switchboard App
Alright! Let's create the application.
rails new switchboard --css=tailwind
cd switchboardRails 8 uses Propshaft as the default asset pipeline (Sprockets is gone) and Importmaps as the default JavaScript approach. The only flag we need is --css=tailwind to pull in the tailwindcss-rails gem. Everything else comes automatically: Propshaft, Importmaps, Solid Queue, Solid Cache, Solid Cable.
Take a minute to look at what Rails generated. If you are coming from Rails 7, a few things have changed.
config/database.yml defaults to SQLite for development, test, and production. Nothing extra to install or configure.
Gemfile includes solid_queue, solid_cache, and solid_cable out of the box. These are the Solid trifecta, replacing the Redis dependencies you might remember from older Rails versions. Background jobs, caching, and WebSocket pub/sub now run on SQLite adapters, so if you have ever spent time getting Redis running in development just to work on a feature, that is no longer necessary.
If you were to open config/cable.yml, you would see it defaults to the Solid Cable adapter. WebSockets work without any external services running.
config/deploy.yml is a Kamal 2 deployment configuration. We will not touch deployment in this book, but Rails 8 ships with a production deployment story built in, which is nice.
The Procfile.dev has two entries: the Rails server and the Tailwind CSS watcher. We will use bin/dev instead of bin/rails server throughout the book so that Tailwind's watcher is always running and your styles compile as you make changes.
Setting Up the Database
bin/rails db:prepareThis creates the SQLite database files and runs any pending migrations. We just generated the app, so there are no migrations yet, but it is a good habit to run db:prepare early. It is idempotent, meaning you can run it whenever you want without worrying about it doing something twice.
Running the App for the First Time
bin/devOpen http://localhost:3000, and you should see the Rails boot screen with the Rails logo and your Ruby and Rails version numbers. Nothing exciting yet, but everything is wired up.
Easy peasy!
Hit Ctrl+C to stop the server when you are done looking around.
Seeding Users
Switchboard is a chat app, so we need users. Now, building a full authentication system with sign-up pages, password resets, and email confirmation would eat a whole chapter, and you would not learn anything about real-time features from it. That is not what this book is about.
Instead, we are going to take a shortcut. We will create a simple User model, seed a few users, and wire up a current_user helper that pulls the active user from the session. In the next chapter, we will add a dev-only dropdown that lets you switch between users with a single click. For now, let's just get the data in place.
The User Model
Generate the model:
bin/rails generate model User name:string email:string
bin/rails db:migrateThat gives you a users table with name and email columns plus the standard created_at and updated_at timestamps.
Open the model and add some validations:
class User < ApplicationRecord
validates :name, presence: true
validates :email, presence: true, uniqueness: true
endNothing fancy. We just want to make sure every user has a name and a unique email address.
Seed Data
Open db/seeds.rb:
users = [
{ name: "Miles", email: "miles@example.com" },
{ name: "Gwen", email: "gwen@example.com" },
{ name: "Peter", email: "peter@example.com" }
]
users.each do |attrs|
User.find_or_create_by!(email: attrs[:email]) do |user|
user.name = attrs[:name]
end
end
puts "Seeded #{User.count} users"We use find_or_create_by! so you can re-run seeds without creating duplicates. Run it:
bin/rails db:seedYou should see Seeded 3 users in the output. You can verify in the console:
bin/rails consoleUser.pluck(:name)
# => ["Miles", "Gwen", "Peter"]Good. Three users, ready to chat. Type exit to leave the console.
The current_user Helper
We need a way for controllers and views to know which user is "logged in." Since we don't have real authentication, we will use the session to store a user ID and fall back to Miles if nothing is set.
Create a concern:
module SetCurrentUser
extend ActiveSupport::Concern
included do
helper_method :current_user
before_action :set_current_user
end
private
def current_user
@current_user
end
def set_current_user
@current_user = User.find_by(id: session[:user_id]) || User.first
end
endInclude it in ApplicationController:
class ApplicationController < ActionController::Base
include SetCurrentUser
endNow every request has a current_user. If there is a user ID in the session, it loads that user; otherwise, it falls back to User.first, which is Miles if you ran the seeds in the order we wrote them. The helper_method call at the top makes current_user available in your views, too, which we will need once we start building the layout.
We will add a user-switcher dropdown in the next chapter so you can switch users without having to touch the console. For now, you are always Miles.
A Tour of the Realtime Stack
Before we start building features, let's take a look at what Turbo Drive is already doing in your app. You did not install it separately. It shipped with rails new.
Start the server if it is not running:
bin/devSeeing Turbo Drive in Action
Let's create a quick scratch page.
Generate a controller:
bin/rails generate controller Pages home aboutTwo actions, two views. Now open config/routes.rb:
Rails.application.routes.draw do
get "home", to: "pages#home"
get "about", to: "pages#about"
root "pages#home"
endAdd some content to the views:
<div class="p-8">
<h1 class="text-3xl font-bold mb-4">Switchboard</h1>
<p class="mb-4">Welcome to Switchboard, your real-time team chat app.</p>
<%= link_to "About this app", about_path, class: "text-blue-600 underline" %>
</div><div class="p-8">
<h1 class="text-3xl font-bold mb-4">About Switchboard</h1>
<p class="mb-4">Switchboard is the demo application for Realtime Rails.</p>
<%= link_to "Back to home", root_path, class: "text-blue-600 underline" %>
</div>Visit http://localhost:3000 in your browser. Click the "About this app" link. Click "Back to home." Go back and forth a few times.
You will notice the page transitions are fast. There is no full-page flash of white between pages, and the URL bar updates normally, but the browser is not actually doing a traditional page load. If you open your dev tools and watch the Network tab while you click around, you will see fetch requests instead of full document loads.
That is Turbo Drive doing its thing. It intercepted those link clicks, fetched the page via fetch() behind the scenes, and swapped the <body> content into the existing document without doing a full reload. You did not write any JavaScript or configure anything. It is just on by default.
A little side note, though: Turbo Drive isn't doing anything magical with your data. It is navigation optimization, nothing more. Your server still renders complete HTML pages just like it always has, and your controllers do not know Turbo exists. Every Rails app gets this behavior for free the moment the turbo-rails gem is included, which rails new does automatically.
We will cover Turbo Frames and Turbo Streams in later chapters for more targeted updates, but you just saw the base layer working without any configuration.
Version Control
Now would be a good time to commit your work. We will follow a commit-per-chapter workflow throughout the book, so you always have a snapshot to come back to if something breaks.
git add -A
git commit -m "Chapter 1: Initial Switchboard application with users and Turbo Drive demo"If you run into issues with git, make sure you are inside the switchboard directory. Rails generates a .gitignore that handles the common exclusions (log files, tmp, SQLite databases in development), so you should not have to worry about committing files you do not want.
What did you learn in this chapter?
- What Hotwire is and how Turbo Drive, Turbo Frames, Turbo Streams, Stimulus, and Action Cable fit together
- Generated the Switchboard Rails 8 application with Tailwind CSS
- The Solid trifecta (Solid Queue, Solid Cache, Solid Cable) and why Redis is no longer required
- Created a User model with seed data (Miles, Gwen, Peter)
- You learned how
current_userworks with a session-based concern - Turbo Drive gives you SPA-like navigation with zero configuration
- How to use
bin/devto run the Rails server with the Tailwind watcher
Continue with the complete book when you’re ready.