DEV Community

Cover image for Building a Ruby on Rails Chat Application with ActionCable and Heroku
reinteractive
reinteractive

Posted on

Building a Ruby on Rails Chat Application with ActionCable and Heroku

Credited to: Rodrigo Souza

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" 
Enter fullscreen mode Exit fullscreen mode

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 } 
Enter fullscreen mode Exit fullscreen mode

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 } 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode
Message Model
class Message < ApplicationRecord belongs_to :room belongs_to :user validates :content, presence: true after_create_commit -> { broadcast_append_to room } end 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode
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 
Enter fullscreen mode Exit fullscreen mode

Views

Room Index
<div class="container mx-auto px-4"> <h1 class="text-2xl font-bold mb-4">Chat Rooms</h1> <div class="mb-4"> <%= 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 %> </div> <%= turbo_frame_tag "rooms" do %> <div class="grid gap-4"> <%= render @rooms %> </div> <% end %> </div> 
Enter fullscreen mode Exit fullscreen mode
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 %> 
Enter fullscreen mode Exit fullscreen mode
Room Show (Chat Interface)
<div class="container mx-auto px-4" data-controller="reset-form"> <h1 class="text-2xl font-bold mb-4"><%= @room.name %></h1> <%= 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 %> </div> 
Enter fullscreen mode Exit fullscreen mode
Message Partial
<div class="message mb-3"> <strong><%= message.user.email %>:</strong> <%= content_tag :span, message.content, class: "break-words" %> </div> 
Enter fullscreen mode Exit fullscreen mode

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 } } 
Enter fullscreen mode Exit fullscreen mode

Routes

 resources :rooms do resources :messages, only: [:create] end root 'rooms#index' 
Enter fullscreen mode Exit fullscreen mode

How It All Works Together

  1. Room Creation and Listing

    • Users can view available rooms on the index page
    • Each room is rendered using the _room.html.erb partial
  2. 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
  3. 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
  4. 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:

image

The Chat room should be like this:

image

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:

  1. Create a new Heroku application
heroku create your-chat-app-name 
Enter fullscreen mode Exit fullscreen mode
  1. 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 
Enter fullscreen mode Exit fullscreen mode
  1. Configure RAILS_MASTER_KEY ENV variable
heroku config:set RAILS_MASTER_KEY=$(cat config/master.key) 
Enter fullscreen mode Exit fullscreen mode
  1. Deploy the application
# Push to Heroku git push heroku main # Run database migrations heroku run rails db:migrate 
Enter fullscreen mode Exit fullscreen mode
  1. Request a Web Dyno
heroku ps:scale web=1 
Enter fullscreen mode Exit fullscreen mode
  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 
Enter fullscreen mode Exit fullscreen mode

Top comments (0)