Building a Ruby on Rails Chat Application with ActionCable and Heroku

In this guide we’ll create a real-time chat application using Rails 8.0.1 and Ruby 3.4.2.
Project Setup
source "https://rubygems.org"
ruby "3.4.2"
gem "rails", "~> 8.0.1"
gem "turbo-rails"
Prerequisites
To get started, ensure you have:
- Proper Action Cable configuration in cable.yml
- Working user authentication system
- Turbo Rails properly installed and configured
This implementation provides a robust foundation for a real-time chat application, leveraging Rails 8’s modern features for seamless real-time updates with minimal JavaScript.
Key Technical Aspects
Turbo Streams and Broadcasting
- Turbo Streams: Handles real-time updates through WebSocket connections
- Action Cable: Powers the WebSocket functionality (built into Rails)
- Scoped Broadcasting: Messages only broadcast to specific room subscribers
- Partial Rendering: Keeps code DRY and maintains consistent UI updates
Let’s break down the key broadcasting mechanisms:
Room Broadcasting:
broadcasts_to ->(room) { room }
This establishes the room as a broadcast target, allowing Turbo to track changes to the room itself.
Message Broadcasting:
after_create_commit -> { broadcast_append_to room }
This ensures new messages are automatically broadcast to all room subscribers.
JavaScript Integration
- Stimulus: Manages form behavior and DOM interactions
- Minimal JavaScript: Most real-time functionality handled by Turbo
- Automatic DOM Updates: No manual DOM manipulation required
Models
Room Model
class Room < ApplicationRecord
has_many :messages, dependent: :destroy
validates :name, presence: true, uniqueness: true
broadcasts_to ->(room) { room }
end
Message Model
class Message < ApplicationRecord
belongs_to :room
belongs_to :user
validates :content, presence: true
after_create_commit -> { broadcast_append_to room }
end
Controllers
Rooms Controller
class RoomsController < ApplicationController
def index
@rooms = Room.all
end
def show
@room = Room.find(params[:id])
@messages = @room.messages.includes(:user)
@message = Message.new
end
def create
@room = Room.create!(room_params)
redirect_to @room
end
private
def room_params
params.require(:room).permit(:name)
end
end
Messages Controller
class MessagesController < ApplicationController
def create
@message = Message.new(message_params)
@message.user_id = session[:user_id] || create_anonymous_user.id
@message.save!
respond_to do |format|
format.turbo_stream
end
end
private
def message_params
params.require(:message).permit(:content, :room_id)
end
def create_anonymous_user
random_id = SecureRandom.hex(4)
user = User.create!(
nickname: "Anonymous_#{random_id}",
email: "new-email-#{random_id}@test.com",
)
session[:user_id] = user.id
user
end
end
Views
Room Index
Chat Rooms
<%= form_with(model: Room.new, class: "flex gap-2") do |f| %>
<%= f.text_field :name, class: "rounded border px-2 py-1" %>
<%= f.submit "Create Room", class: "bg-blue-500 text-white px-4 py-1 rounded" %>
<% end %>
<%= turbo_frame_tag "rooms" do %>
<%= render @rooms %>
<% end %>
Room Partial
<%= link_to room_path(room),
class: "block p-4 border rounded hover:bg-gray-50",
data: { turbo_frame: "_top" } do %>
<%= room.name %>
<% end %>
Room Show (Chat Interface)
<%= @room.name %>
<%= turbo_stream_from @room %>
<%= turbo_frame_tag "messages",
class: "block mb-4 h-96 overflow-y-auto border rounded p-4",
data: { reset_form_target: "messages" } do %>
<%= render @messages %>
<% end %>
<%= turbo_frame_tag "new_message", target: "_top" do %>
<%= form_with(model: [@room, @message],
class: "flex gap-2",
data: { action: "turbo:submit-end->reset-form#reset" }) do |f| %>
<%= f.hidden_field :room_id, value: @room.id %>
<%= f.text_field :content,
class: "flex-1 rounded border px-2 py-1",
data: { reset_form_target: "content" } %>
<%= f.submit "Send", class: "bg-blue-500 text-white px-4 py-1 rounded" %>
<% end %>
<% end %>
Message Partial
<%= message.user.email %>:
<%= content_tag :span, message.content, class: "break-words" %>
JavaScript
Reset Form Controller
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["content", "messages"]
connect() {
this.scrollToBottom()
}
reset() {
this.contentTarget.value = ""
this.scrollToBottom()
}
scrollToBottom() {
this.messagesTarget.scrollTop = this.messagesTarget.scrollHeight
}
}
Routes
resources :rooms do
resources :messages, only: [:create]
end
root 'rooms#index'
How It All Works Together
-
Room Creation and Listing - Users can view available rooms on the index page - Each room is rendered using the
_room.html.erb
partial - Entering a Chat Room
- Clicking a room link takes users to the show page
- The show page establishes a Turbo Stream connection
- Existing messages are loaded and displayed
- Real-time Message Broadcasting
- When a message is created:
- The form submits to MessagesController#create
- Message is saved to the database
- after_create_commit triggers broadcasting
- All room subscribers receive the update
- New message appears instantly for all users
- When a message is created:
- Form Handling
- The Stimulus controller manages form behavior
- After successful submission, the form is cleared
- The UI remains responsive throughout
Code In Action
You should see something like this in your browser:
The Chat room should be like this:
Deploying the application on Heroku
To deployment on Heroku platform is pretty straighfoward. The prerequisites are:
- Heroku CLI installed
- Git repository initialized
After covering all the prerequisites, let’s dive into the steps to the deployment:
- Create a new Heroku application
heroku create your-chat-app-name
- Add the necessary Add-ons
# Add Redis add-on
heroku addons:create heroku-redis:hobby-dev
# Add PostgreSQL add-on
heroku addons:create heroku-postgresql:mini
- Configure RAILS_MASTER_KEY ENV variable
heroku config:set RAILS_MASTER_KEY=$(cat config/master.key)
- Deploy the application
# Push to Heroku
git push heroku main
# Run database migrations
heroku run rails db:migrate
- Request a Web Dyno
heroku ps:scale web=1
- Verify the deployment
Check the logs from the deployment process and open the application:
# Open the application
heroku open
# Monitor logs
heroku logs --tail
Ps. if you have any questions
Ask here