DEV Community

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

Posted on

1 1 1

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

Heroku

Built for developers, by developers.

Whether you're building a simple prototype or a business-critical product, Heroku's fully-managed platform gives you the simplest path to delivering apps quickly — using the tools and languages you already love!

Learn More

Top comments (0)

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

👋 Kindness is contagious

If this post resonated with you, feel free to hit ❤️ or leave a quick comment to share your thoughts!

Okay