Key Takeaways
- Rails 5 introduces ActionCable, a feature that seamlessly integrates WebSockets for real-time communication in your Rails application, enhancing interaction in applications like chat systems.
- Utilize Devise for user authentication in your Rails app, ensuring that access to chat features is secure and managed efficiently.
- Create and manage multiple chat rooms using Rails models and controllers, providing a structured way to organize conversations and maintain data integrity.
- Employ client-side JavaScript to enable users to send and receive messages in real-time without needing to refresh their browser, leveraging ActionCable’s subscription mechanisms.
- Integrate background jobs for broadcasting messages, which helps in managing the load and improving the performance of the chat application.
- Prepare and deploy your chat application to Heroku, including setting up necessary add-ons like Redis for managing live chat data and configuring environment variables for secure and scalable production use.
Rails 5 has introduced a bunch of new great features, but one of the most anticipated ones is of course ActionCable. ActionCable seamlessly integrates WebSockets into your application and offers both client-side JS and server-side Ruby frameworks. This way, you can write real-time features in the same styles as the rest your application, which is really cool.
Learn more on ruby with our tutorial Simulate User Behavior and Test Your Ruby App on SitePoint.
Some months ago I wrote a series of articles describing how to build mini-chat with Rails using AJAX, WebSockets powered by Faye and Server-sent events. Those articles garnered some attention, so I decided to pen a new part of this series instructing how to use ActionCable to achieve the same goal.
This time, however, we will face a bit more complicated task and discuss the following topics:
- Preparing application and integrating Devise
- Introducing chat rooms
- Setting up ActionCable
- Coding client-side
- Coding server-side with the help of background jobs
- Introducing basic authorization for ActionCable
- Preparing application to be deployed to Heroku
The source code can be found at GitHub.
The working demo is available at sitepoint-actioncable.herokuapp.com.
Preparing the Application
Start off by creating a new application. Support for ActionCable was added only in Rails 5, so you will have to use this version (currently 5.0.0.rc1 is the latest one):
$ rails new CableChat -T
Now add a couple of gems:
Gemfile
[...]
gem 'devise'
gem 'bootstrap', '~> 4.0.0.alpha3'
[...]
Devise will be used for authentication and authorization (you may read this article to learn more) and Bootstrap 4 – for styling.
Run
$ bundle install
Add Bootstrap’s styles:
stylesheets/application.scss
@import "bootstrap";
Run the following commands to install Devise, generate a new User
model, and copy views for further customization:
$ rails generate devise:install
$ rails generate devise User
$ rails generate devise:views
$ rails db:migrate
Now restrict access to all pages of the site to authenticated users only:
application_controller.rb
[...]
before_action :authenticate_user!
[...]
Chat Rooms
The next step is to add support for chat rooms, so generate the following model:
$ rails g model ChatRoom title:string user:references
$ rails db:migrate
A chat room should have a creator, so make sure you establish a one-to-many relation between chat_rooms
and users
:
models/chat_room.rb
[...]
belongs_to :user
[...]
models/users.rb
[...]
has_many :chat_rooms, dependent: :destroy
[...]
Code a controller to list and create chat rooms:
chat_rooms_controller.rb
class ChatRoomsController < ApplicationController
def index
@chat_rooms = ChatRoom.all
end
def new
@chat_room = ChatRoom.new
end
def create
@chat_room = current_user.chat_rooms.build(chat_room_params)
if @chat_room.save
flash[:success] = 'Chat room added!'
redirect_to chat_rooms_path
else
render 'new'
end
end
private
def chat_room_params
params.require(:chat_room).permit(:title)
end
end
Now a bunch of really simple views:
views/chat_rooms/index.html.erb
<h1>Chat rooms</h1>
<p class="lead"><%= link_to 'New chat room', new_chat_room_path, class: 'btn btn-primary' %></p>
<ul>
<%= render @chat_rooms %>
</ul>
views/chat_rooms/_chat_room.html.erb
<li><%= link_to "Enter #{chat_room.title}", chat_room_path(chat_room) %></li>
views/chat_rooms/new.html.erb
<h1>Add chat room</h1>
<%= form_for @chat_room do |f| %>
<div class="form-group">
<%= f.label :title %>
<%= f.text_field :title, autofocus: true, class: 'form-control' %>
</div>
<%= f.submit "Add!", class: 'btn btn-primary' %>
<% end %>
Messages
The main star of our app is, of course, a chat message. It should belong to both a user and a chat room. To get there, run the following:
$ rails g model Message body:text user:references chat_room:references
$ rails db:migrate
Make sure to establish the proper relations:
models/chat_room.rb
[...]
belongs_to :user
has_many :messages, dependent: :destroy
[...]
models/users.rb
[...]
has_many :chat_rooms, dependent: :destroy
has_many :messages, dependent: :destroy
[...]
models/message.rb
[...]
belongs_to :user
belongs_to :chat_room
[...]
So far, so good. Messages should be displayed when a user enters a chat room, so create a new show
action:
chat_rooms_controller.rb
[...]
def show
@chat_room = ChatRoom.includes(:messages).find_by(id: params[:id])
end
[...]
Note the includes method here used for eager loading.
Now the views:
views/chat_rooms/show.html.erb
<h1><%= @chat_room.title %></h1>
<div id="messages">
<%= render @chat_room.messages %>
</div>
views/messages/_message.html.erb
<div class="card">
<div class="card-block">
<div class="row">
<div class="col-md-1">
<%= gravatar_for message.user %>
</div>
<div class="col-md-11">
<p class="card-text">
<span class="text-muted"><%= message.user.name %> at <%= message.timestamp %> says</span><br>
<%= message.body %>
</p>
</div>
</div>
</div>
</div>
In this partial three new methods are employed: user.name
, message.timestamp
and gravatar_for
. To construct a name, let’s simply strip off the domain part from the user’s email (of course, in a real app you’d want to allow them entering a name upon registration or at the “Edit profile” page):
models/user.rb
[...]
def name
email.split('@')[0]
end
[...]
timestamp
relies on strftime
to present message’s creation date in a user-friendly format:
models/message.rb
[...]
def timestamp
created_at.strftime('%H:%M:%S %d %B %Y')
end
[...]
gravatar_for
is a helper to display user’s gravatar:
application_helper.rb
module ApplicationHelper
def gravatar_for(user, opts = {})
opts[:alt] = user.name
image_tag "https://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(user.email)}?s=#{opts.delete(:size) { 40 }}",
opts
end
end
The last two things to do here is to style the messages container a bit:
#messages {
max-height: 450px;
overflow-y: auto;
.avatar {
margin: 0.5rem;
}
}
Add routes:
config/routes.rb
[...]
resources :chat_rooms, only: [:new, :create, :show, :index]
root 'chat_rooms#index'
[...]
Finally, preparations are done and we can proceed to coding the core functionality of our chat.
Adding ActionCable
Client Side
Before proceeding, install Redis on your machine if you do not already have it. Redis is available for nix, via Homebrew and for Windows, as well.
Next, tweak the Gemfile:
Gemfile
[...]
gem 'redis', '~> 3.2'
[...]
and run
$ bundle install
Now you may modify the config/cable.yml file to use Redis as an adapter:
config/cable.yml
[...]
adapter: redis
url: YOUR_URL
[...]
Or simply use adapter: async
(the default value).
Also, modify your routes.rb to mount ActionCable on some URL:
config/routes.rb
[...]
mount ActionCable.server => '/cable'
[...]
Check that inside the javascripts directory there is a cable.js file with the contents like:
javascripts/cable.js
//= require action_cable
//= require_self
//= require_tree ./channels
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);
This file must be required inside application.js:
javascripts/application.js
[...]
//= require cable
[...]
Consumer
is a client of a web socket connection that can subscribe to one or multiple channels. Each ActionCable server may handle multiple connections. Channel
is similar to an MVC controller and is used for streaming. You may read more about ActionCable’s terminology here.
So, let’s create a new channel:
javascripts/channels/rooms.coffee
App.global_chat = App.cable.subscriptions.create {
channel: "ChatRoomsChannel"
chat_room_id: ''
},
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
# Data received
send_message: (message, chat_room_id) ->
@perform 'send_message', message: message, chat_room_id: chat_room_id
Here, we basically subscribe a consumer to the ChatRoomsChannel
and pass the current room’s id (at this point we do not really pass anything, but that’ll be fixed soon). The subscription has a number of self-explaining callbacks: connected
, disconnected
, and received
. Also, the subscription defines the main function (send_message
) that invokes the method with the same name of the server and passes the necessary data to it.
Of course, we need a form to allow users to send their messages:
views/chat_rooms/show.html.erb
<%= form_for @message, url: '#' do |f| %>
<div class="form-group">
<%= f.label :body %>
<%= f.text_area :body, class: 'form-control' %>
<small class="text-muted">From 2 to 1000 characters</small>
</div>
<%= f.submit "Post", class: 'btn btn-primary btn-lg' %>
<% end %>
The @message
instance variable should be set inside the controller:
chat_rooms_controller.rb
[...]
def show
@chat_room = ChatRoom.includes(:messages).find_by(id: params[:id])
@message = Message.new
end
[...]
Of course, you might use the basic form
tag instead of relying on the Rails form builder, but this allows us to take advantage of things like I18n translations later.
Let’s also add some validations for messages:
models/message.rb
[...]
validates :body, presence: true, length: {minimum: 2, maximum: 1000}
[...]
Another problem to tackle here is providing our script with the room’s id. Let’s solve it with the help of HTML data-
attribute:
views/chat_rooms/show.html.erb
[...]
<div id="messages" data-chat-room-id="<%= @chat_room.id %>">
<%= render @chat_room.messages %>
</div>
[...]
Having this in place, we can use room’s id in the script:
javascripts/channels/rooms.coffee
jQuery(document).on 'turbolinks:load', ->
messages = $('#messages')
if $('#messages').length > 0
App.global_chat = App.cable.subscriptions.create {
channel: "ChatRoomsChannel"
chat_room_id: messages.data('chat-room-id')
},
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
# Data received
send_message: (message, chat_room_id) ->
@perform 'send_message', message: message, chat_room_id: chat_room_id
Note the jQuery(document).on 'turbolinks:load'
part. This should be done only if you are using Turbolinks 5 that supports this new event. You might think about usng jquery-turbolinks to bring the default jQuery events back, but unfortunately it is not compatible with Turbolinks 5.
The logic of the script is pretty simple: check if there is a #messages
block on the page and, if yes, subscribe to the channel while providing the room’s id. The next step is to listen for the form’s submit
event:
javascripts/channels/rooms.coffee
jQuery(document).on 'turbolinks:load', ->
messages = $('#messages')
if $('#messages').length > 0
App.global_chat = App.cable.subscriptions.create
# ...
$('#new_message').submit (e) ->
$this = $(this)
textarea = $this.find('#message_body')
if $.trim(textarea.val()).length > 1
App.global_chat.send_message textarea.val(), messages.data('chat-room-id')
textarea.val('')
e.preventDefault()
return false
When the form is submitted, take the message’s body, check that its length is at least two and then call the send_message
function to broadcast the new message to all visitors of the chat room. Next, clear the textarea and prevent form submission.
Server Side
Our next task will be to introduce a channel on our server. In Rails 5, there is a new directory called channels to host them, so create a chat_rooms_channel.rb file there:
channels/chat_rooms_channel.rb
class ChatRoomsChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_rooms_#{params['chat_room_id']}_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def send_message(data)
# process data sent from the page
end
end
subscribed
is a special method to start streaming from a channel with a given name. As long as we have multiple rooms, the channel’s name will vary. Remember, we provided chat_room_id: messages.data('chat-room-id')
when subscribing to a channel in our script? Thanks to it, chat_room_id
can be fetched inside the subscribed
method by calling params['chat_room_id']
.
unsubscribed
is a callback that fires when the streaming is stopped, but we won’t use it in this demo.
The last method – send_message
– fires when we run @perform 'send_message', message: message, chat_room_id: chat_room_id
from our script. The data
variable contains a hash of sent data, so, for example, to access the message you would type data['message']
.
There are multiple ways to broadcast the received message, but I am going to show you a very neat solution based on the demo provided by DHH (I’ve also found this article with a slightly different approach).
First of all, modify the send_message
method:
channels/chat_rooms_channel.rb
[...]
def send_message(data)
current_user.messages.create!(body: data['message'], chat_room_id: data['chat_room_id'])
end
[...]
Once we receive a message, save it to the database. You don’t even need to check whether the provided chat room exists – by default, in Rails 5, a record’s parent must exist in order to save it. This behavior can be changed by setting optional: true
for the belongs_to
relation (read about other changes in Rails 5 here).
There is a problem though – Devise’s current_user
method is not available for us here. To fix that, modify the connection.rb file inside the application_cable directory:
channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
logger.add_tags 'ActionCable', current_user.email
end
protected
def find_verified_user # this checks whether a user is authenticated with devise
if verified_user = env['warden'].user
verified_user
else
reject_unauthorized_connection
end
end
end
end
Having this in place, we achieve even two goals at once: the current_user
method is now available for the channel and unauthenticated users are not able to broadcast their messages.
The call to logger.add_tags 'ActionCable', current_user.email
is used to display debugging information in the console, so you will see output similar to this:
[ActionCable] [test@example.com] Registered connection (Z2lkOi8vY2FibGUtY2hhdC9Vc2VyLzE)
[ActionCable] [test@example.com] ChatRoomsChannel is transmitting the subscription confirmation
[ActionCable] [test@example.com] ChatRoomsChannel is streaming from chat_rooms_1_channel
Under the hood Devise uses Warden for authentication, so env['warden'].user
tries to fetch the currently logged-in user. If it is not found, reject_unauthorized_connection
forbids broadcasting.
Now, let’s add a callback that fires after the message is actually saved to the database to schedule a background job:
models/message.rb
[...]
after_create_commit { MessageBroadcastJob.perform_later(self) }
[...]
In this callback self
is a saved message, so we basically pass it to the job. Write the job now:
jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
queue_as :default
def perform(message)
ActionCable.server.broadcast "chat_rooms_#{message.chat_room.id}_channel",
message: 'MESSAGE_HTML'
end
end
The perform
method does the actual broadcasting, but what about the data we want to broadcast? Once again, there are a couple of ways to solve this problem. You may send JSON with the message data and then on the client side use a templating engine like Handlebars. In this demo, however, let’s send HTML markup from the messages/_message.html.erb partial we created earlier. This partial can be rendered with the help of a controller:
jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
queue_as :default
def perform(message)
ActionCable.server.broadcast "chat_rooms_#{message.chat_room.id}_channel",
message: render_message(message)
end
private
def render_message(message)
MessagesController.render partial: 'messages/message', locals: {message: message}
end
end
In order for this to work, you’ll have to create an empty MessagesController
:
messages_controller.rb
class MessagesController < ApplicationController
end
Back to the Client Side
Great, now the server side is ready and we can finalize our script. As long as we broadcast HTML markup, it can be simply placed right onto the page without any further manipulations:
javascripts/channels/rooms.coffee
[...]
App.global_chat = App.cable.subscriptions.create {
channel: "ChatRoomsChannel"
chat_room_id: messages.data('chat-room-id')
},
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
messages.append data['message']
send_message: (message, chat_room_id) ->
@perform 'send_message', message: message, chat_room_id: chat_room_id
[...]
The only thing I don’t really like here is that, by default, the user sees old messages, whereas the newer ones are being placed at the bottom. You could either use the order
method to sort them properly and replace append
with prepend
inside the received
callback, but I’d like to make our chat behave like Slack. In Slack, newer messages are also placed at the bottom, but the chat window automatically scrolls to them. That’s easy to achieve with the following function that is called once the page is loaded:
javascripts/channels/rooms.coffee
jQuery(document).on 'turbolinks:load', ->
messages = $('#messages')
if $('#messages').length > 0
messages_to_bottom = -> messages.scrollTop(messages.prop("scrollHeight"))
messages_to_bottom()
App.global_chat = App.cable.subscriptions.create
# ...
Let’s also scroll to the bottom once a new message has arrived (because by default it won’t be focused):
javascripts/channels/rooms.coffee
[...]
received: (data) ->
messages.append data['message']
messages_to_bottom()
[...]
Great! Check out the resulting script on GitHub.
Pushing to Heroku
If you wish to push your new shiny chat to Heroku, some additional actions have to be taken. First of all, you will have install a Redis addon. There are many addons to choose from: for example, you could use Rediscloud. When the addon is installed, tweak cable.yml to provide the proper Redis URL. For Rediscloud it is stored inside the ENV["REDISCLOUD_URL"]
environment variable:
config/cable.yml
production:
adapter: redis
url: <%= ENV["REDISCLOUD_URL"] %>
[...]
The next step is to list the allowed origins to subscribe to the channels:
config/environments/production.rb
[...]
config.action_cable.allowed_request_origins = ['https://your_app.herokuapp.com',
'http://your_app.herokuapp.com']
[...]
Lastly, you have to provide the ActionCable URL. As long as our routes.rb has mount ActionCable.server => '/cable'
, the corresponding setting should be:
config/environments/production.rb
[...]
config.action_cable.url = "wss://sitepoint-actioncable.herokuapp.com/cable"
[...]
Having this in place, you can push your code to Heroku and observe the result. Yay!
Conclusion
In this article we’ve discussed how to set up ActionCable and code a mini-chat app with support for multiple rooms. The app includes both the client and server sides, while providing a basic authorization mechanism. We also employed a background job to power up the broadcasting process and discussed steps required to deploy the application to Heroku.
Hopefully, you found this article useful and interesting. Have you already tried using ActionCable? Did you like it? Share your opinion in the comments. Follow me on Twitter to be the first one to know about my articles, and see you soon!
Learn more on ruby with our tutorial Simulate User Behavior and Test Your Ruby App on SitePoint.
Frequently Asked Questions (FAQs) about Creating a Chat App with Rails 5, ActionCable, and Devise
How can I integrate live chat functionality into my Rails application?
Integrating live chat functionality into your Rails application involves several steps. First, you need to set up your Rails application and install the necessary gems, including ActionCable and Devise. ActionCable is a Rails framework that seamlessly integrates WebSockets with the rest of your Rails application. Devise is a flexible authentication solution for Rails based on Warden. After setting up, you need to create your chat models and controllers, and set up your routes. Then, you can create your chat views and use ActionCable to set up the live chat functionality. Finally, you can use Devise to handle user authentication.
Can I create a chat app with Rails 5, ActionCable, and Devise for mobile platforms?
Yes, you can create a chat app with Rails 5, ActionCable, and Devise for mobile platforms. However, you will need to use a tool like React Native or Flutter to create the mobile app interface. These tools allow you to write mobile applications in JavaScript or Dart, respectively, which can then be compiled into native code for both iOS and Android. The backend of your app, including the chat functionality, can be handled by your Rails application.
How can I handle user authentication in my chat app?
User authentication in your chat app can be handled using Devise. Devise is a flexible authentication solution for Rails that is based on Warden. It provides a full-featured authentication solution that includes features like user registration, session management, password recovery, and more. To use Devise, you need to add it to your Gemfile and run the bundle install command. Then, you can generate the Devise installation with the rails generate devise:install command and create your User model with the rails generate devise User command.
How can I ensure the security of my chat app?
Ensuring the security of your chat app involves several steps. First, you need to handle user authentication securely. This can be done using Devise, which provides a secure and flexible authentication solution. Additionally, you should use secure WebSocket connections for your live chat functionality. This can be done using ActionCable, which supports secure WebSocket connections out of the box. Finally, you should follow best practices for secure coding and regularly update your dependencies to ensure that they do not contain any known security vulnerabilities.
How can I scale my chat app to support more users?
Scaling your chat app to support more users can be done in several ways. One way is to use a more powerful server or multiple servers to handle more connections. Another way is to use a load balancer to distribute the load among multiple servers. Additionally, you can optimize your code to handle more connections more efficiently. This can involve things like optimizing your database queries, using caching, and more. Finally, you can use a service like Redis or Memcached to store session data, which can help to reduce the load on your database.
How can I customize the look and feel of my chat app?
Customizing the look and feel of your chat app can be done using CSS and JavaScript. CSS allows you to style your app, including things like colors, fonts, and layout. JavaScript allows you to add interactivity to your app, such as animations and transitions. Additionally, you can use a front-end framework like Bootstrap or Material-UI to quickly create a professional-looking interface.
Can I add additional features to my chat app, like private messaging or group chats?
Yes, you can add additional features to your chat app, like private messaging or group chats. This can be done by extending your chat models and controllers, and creating additional views for these features. For example, for private messaging, you could create a PrivateMessage model and controller, and a view for creating and viewing private messages. For group chats, you could create a Group model and controller, and a view for creating and joining groups.
How can I deploy my chat app to a live server?
Deploying your chat app to a live server can be done using a platform like Heroku or AWS. These platforms provide a simple way to deploy your Rails application to a live server, and they also provide additional services like database hosting, load balancing, and more. To deploy your app, you need to create an account on the platform, install their command-line tool, and follow their deployment instructions.
How can I test my chat app to ensure that it works correctly?
Testing your chat app to ensure that it works correctly can be done using Rails’ built-in testing framework, or a tool like RSpec. These tools allow you to write tests for your models, controllers, and views, and they provide a way to automatically run these tests to ensure that your app works as expected. Additionally, you can use a tool like Capybara to write integration tests that simulate user interactions with your app.
How can I handle errors and exceptions in my chat app?
Handling errors and exceptions in your chat app can be done using Rails’ built-in error handling features. For example, you can use the rescue_from method in your controllers to handle specific exceptions. Additionally, you can use a tool like Sentry or Bugsnag to automatically report exceptions to a central location, which can help you to quickly identify and fix issues.
Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.