Real-Time Messaging with Rails and ActionCable

Ilya Bodrov-Krukowski

In the previous article I showed how to create a custom messaging application allowing one-to-one chats between users. We’ve integrated Devise, set up proper associations, added support for Emoji, and made everything work in a synchronous fashion. However, I would really like this app to be more modern. Therefore today we will enhance it further with ActionCable, making it real-time.

By the end of the article you will know how to:

  • Write code client-side to subscribe to a web socket/channel
  • Write code server-side to broadcast messages
  • Secure ActionCable
  • Display notifications about new messages
  • Dynamically update conversations
  • Implement an “is online” feature using Redis
  • Deploy your application to Heroku

If you haven’t read the previous article, you may use this code to get started. This branch contains the final version of the application with all the real-time features.

A working demo is available at

Integrating ActionCable

To make our messaging system work in real time, we will require ActionCable, which I have already covered in one of the previous articles. The first step is mounting it to some path, usually just /cable:


mount ActionCable.server => '/cable'

Next, we need to take care of both client and server side.

Client Side

Inside the javascripts directory there should be a file called cable.js with the following contents:


//= require action_cable
//= require_self
//= require_tree ./channels

(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer();


We need to require this file in our javascript manifest:


//= require cable

Then, create a new channel that will be called Conversations. Let’s also move all code from the into it:


jQuery(document).on 'turbolinks:load', ->
  messages_to_bottom = -> messages.scrollTop(messages.prop("scrollHeight"))
  messages = $('#conversation-body')

  if messages.length > 0

This code simply scrolls the conversations box to the very bottom as the newest messages are displayed. The javascripts/ can be removed.

Now, we need to subscribe to the Conversations channel. However, we only want to do this when a user is signed in, so let’s just add an empty block to the page if that’s true:


<% if user_signed_in? %>
  <div id="current-user"></div>
        <!-- ... -->

Now the CoffeeScript:

jQuery(document).on 'turbolinks:load', ->
  messages_to_bottom = -> messages.scrollTop(messages.prop("scrollHeight"))
  messages = $('#conversation-body')

  if $('#current-user').size() > 0
    App.personal_chat = App.cable.subscriptions.create {
      channel: "NotificationsChannel"
    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) ->
      # Called when data is received
  if messages.length > 0

I’ve received many questions about ActionCable and often readers experience problems with CoffeeScript by getting errors like “Unexpected indentation”. Therefore, note that CoffeeScript relies heavily on indentations and whitespace, so double check your code and compare it to the source published on GitHub.

The next step we should do is listen to the submit event on our form and prevent it from happening. Instead, we are going to send the message via our Notifications channel while providing the conversation id:

# ...
if messages.length > 0
  $('#new_personal_message').submit (e) ->
    $this = $(this)
    textarea = $this.find('#personal_message_body')
    if $.trim(textarea.val()).length > 1
      App.personal_chat.send_message textarea.val(), $this.find('#conversation_id').val()
    return false

Things are pretty simple here. Check that the message contains at least 2 characters and, if true, invoke the send_message method that will be coded in a moment. Next, clear the textarea indicating to the user that the message was consumed and prevent the default action from happening. Note, that even if our JavaScript fails to run or a user has disabled JavaScript for a website, messages will still be sent in a synchronous fashion.

Here is the send_message method:

# ...
App.personal_chat = App.cable.subscriptions.create {
  channel: "NotificationsChannel"
send_message: (message, conversation_id) ->
  @perform 'send_message', message: message, conversation_id: conversation_id
# ...

@perform 'send_message' means that we are invoking a method with this name on the server side while passing an object containing the message and conversation id. Let’s write this method now.

Server Side

Rails 5 applications have a new directory called app/channels. Create a new file inside that directory with the following code:


class NotificationsChannel < ApplicationCable::Channel
  def subscribed

  def unsubscribed

  def send_message(data)
    conversation = Conversation.find_by(id: data['conversation_id'])
    if conversation && conversation.participates?(current_user)
      personal_message ={body: data['message']})
      personal_message.conversation = conversation!

subscribed and unsubscribed are hooks that run when someone starts or stops listening to a socket.

send_message is the method we are calling from the client side. That method checks whether a conversation exists and a user has rights to access it. This is a very important step, because otherwise, anyone may write to any conversation. If it is true, just save the new message. Note that when you make any changes to the files inside the channels directory, you must reboot web server (even in development).

Another thing that needs to be tackled is authentication to prevent any unauthorized user from subscribing to a socket. Unfortunately, we don’t have access to Devise’s current_user method inside channels, so add it now:


module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
      logger.add_tags 'ActionCable',


    def find_verified_user # this checks whether a user is authenticated with devise
      verified_user = env['warden'].user
      if verified_user

identified_by :current_user allows the current_user method to be used by the channels. connect is being triggered automatically when someone tries to connect to a socket. Here, we set the current user only if they are authenticated. As long as Devise uses Warden for authentication purposes, we can use env['warden'].user to return an ActiveRecord object. If the user is not authenticated simply call reject_unauthorized_connection.

logger.add_tags 'ActionCable', prints out information about a user who has subscribed to a channel into the terminal.

So far so good, but after the message is saved, it should be sent back to the participants of a conversation and be rendered on the page. One of the ways to achieve this is by using a callback and a background job:


# ...
after_create_commit do

after_create_commit is called only after a record was saved and committed.

Here is the background job to broadcast a new message:


class NotificationBroadcastJob < ApplicationJob
    queue_as :default

    def perform(personal_message)
      message = render_message(personal_message)
      ActionCable.server.broadcast "notifications_#{}_channel",
                                   message: message,

      ActionCable.server.broadcast "notifications_#{}_channel",
                             notification: render_notification(personal_message),
                             message: message,


    def render_notification(message)
      NotificationsController.render partial: 'notifications/notification', locals: {message: message}

    def render_message(message)
      PersonalMessagesController.render partial: 'personal_messages/personal_message',
                                        locals: {personal_message: message}

ActionCable.server.broadcast sends data via a channel. Since, inside notifications_channel.rb, we said stream_from("notifications_#{}_channel"), the same name will be used in the background job.

Here we actually send two broadcasts, one containing the actual message to render and another with the notification. The idea is simple: if a user is currently browsing the conversation to which the message belongs, simply append a new message to the bottom. If, however, they are on some other page, display a small notification message at the bottom right corner saying “You’ve got a new message from…”.

Create a new controller to render the notification partial:


class NotificationsController < ApplicationController

And the partial itself:


<div id="notification">
  <div class="close">close me</div>
    <%= message.body %><br>
    from <%= %>
  <p><%= link_to 'Take me there now!', conversation_path(message.conversation) %></p>

Great! The next thing we need to do is actually render the markup, so return to the client side.

Back to the Client Side

Tweak the received callback to render the message or a notification:

received: (data) ->
  if messages.size() > 0 &&'conversation-id') is data['conversation_id']
    messages.append data['message']
    $('body').append(data['notification']) if data['notification']

The conditional inside this callback relies on the data-conversation-id attribute being set for the #conversation-body, so add it now:


<div id="conversation-body" data-conversation-id="<%= %>">

We are nearly done here. Let’s also make the “close me” link inside the notification to work. As long as a notification is a dynamically added content, we can’t bind a click event handler directly to it. Instead, rely on event bubbling:

$(document).on 'click', '#notification .close', ->

Also, style the notification to make it always display at the bottom right corner:


#notification {
  position: fixed;
  right: 10px;
  bottom: 10px;
  width: 300px;
  padding: 10px;
  border: 1px solid black;
  background-color: #fff;
  .close {cursor: pointer}

Now you can boot the server, open two separate browser windows (or maybe use two different browsers) and test the result! Talking to yourself might not be very fun, though…

Note that in order to receive updates in real time you need to visit the same address in all browsers, either localhost:3000 or

Updating Conversations

Another useful and pretty easy to implement feature is the dynamic updating of conversations. Currently, we sort conversations by updated_at, but we don’t really set this column with a new value when a message is saved. Also, it would be nice to re-render conversations sorted in a new order dynamically if a user is browsing the main page.

To make it work, firstly modify the callback:


after_create_commit do

touch will simply set the updated_at column with the current date and time.

Tweak the received callback:

received: (data) ->
  if messages.size() > 0 &&'conversation-id') is data['conversation_id']
    messages.append data['message']
    $.getScript('/conversations') if $('#conversations').size() > 0
    $('body').append(data['notification']) if data['notification']

So if a user is not browsing a particular conversation but, rather, is located on the conversations page, request the JavaScript to re-render conversations in which they participate. Instead, you may request a JSON array with all the conversations and render them from the callback.

Make the index action respond to requests in HTML and JS formats:


def index
  @conversations = Conversation.participating(current_user).order('updated_at DESC')
  respond_to do |format|

Lastly, the .js.erb view:


$('#conversations').replaceWith('<%= j render @conversations %>');

That’s it!

“Is Online?” Feature

“Who is online” is a very common feature for messaging systems. We will implement it in a pretty simple format by styling the user’s name in either red or green marking whether they are online or not.

Setting the “online” flag for specific users can be done from the controller, but we are going to employ another channel for that (that’ll be also used to notify when someone goes online or offline). We are also going to utilize Redis so install it to see the result in action.

First of all, add the redis-rb gem


gem 'redis'

and install it

$ bundle install

Next, create the new Appearances channel:


jQuery(document).on 'turbolinks:load', ->
  App.personal_chat = App.cable.subscriptions.create {
    channel: "AppearancesChannel"
  connected: ->

  disconnected: ->

  received: (data) ->

Basically, when a user subscribes to this channel, they are online.

Here is the server-side implementation:


class AppearancesChannel < ApplicationCable::Channel
  def subscribed
    redis.set("user_#{}_online", "1")
    ActionCable.server.broadcast "appearances_channel",
                                 online: true

  def unsubscribed
    ActionCable.server.broadcast "appearances_channel",
                                 online: false


  def redis

Once someone is subscribed, set “user_SOME_ID_online” to “1” in Redis. Then start streaming and broadcast a message saying that a user is now online.

When a user unsubscribes, we delete the “user_SOME_ID_online” key and once again broadcast a message. Pretty simple, really.

While we are coding the server-side, introduce a new online? instance method for the User class:


def online?

The next step is to wrap all user names with a span tag and give it a proper class. To make this process easier, add the following helper method:


def online_status(user)
  content_tag :span,,
              class: "user-#{} online_status #{'online' if}"

Now this helper can be used in any view. I’ll utilize it inside the _conversation partial:


Chatting with <%= online_status conversation.with(current_user) %>

Now code the received callback. It should simply add or remove the online class:

received: (data) ->
  user = $(".user-#{data['user_id']}")
  user.toggleClass 'online', data['online']

Either add or remove the class based on the value of data['online'].

Of course, some styling is needed:

.online_status {
  color: red;
  transition: color 1s ease;
  &.online {
    color: green;

I’ve added a transition to make things a bit more smooth. Of course, instead of styling names with some color, you may, for example, add an icon to them using :after or :before pseudo-selectors.

Publishing to Heroku

Of course, at some point in time you might want to share your application with the world. I will explain how to do it using Heroku as an example.

First of all, you need Redis installed. Heroku provides a bunch of addons (or elements as they are now called) adding Redis support. I am going to use Redis Cloud. Install its free version by typing:

$ heroku addons:create rediscloud

Now tell ActionCable to use Redis Cloud:


  adapter: redis
  url: <%= ENV["REDISCLOUD_URL"] %>

Another thing to note is that when you say, redis-rb will try to connect to localhost:6379 (6379 is the default port). That’s okay for development, but not particularly great for production. Therefore I’ve created this simple initializer:



By default, redis-rb first tries to connect to the URL set in the ENV['REDIS_URL'] and uses localhost:6379 as as fallback.

Another thing that needs to be done is the creation of the Procfile telling Heroku to use Puma as a web server:


web: bundle exec puma -C config/puma.rb

The configuration docs for Puma can be found here.

The last step is setting the ActionCable URL and adding allowed origins for the production environment. For my demo application, the configuration is the following:


config.action_cable.allowed_request_origins = ['',

config.action_cable.url = "wss://"

Your URL will be different.

That’s all! Once these steps are done, you can deploy your application to Heroku and test it out.


That was a long road. In these two articles, we’ve created a messaging application, allowing one-to-one conversations. We then made it work in real-time with the help of ActionCable and Redis. Of course, this code can be further developed and many new features can be introduced. If you create something interesting based on my code, do share it!

This concludes the article about messaging and ActionCable. As always I thank you for staying with me until the end and see you soon!

CSS Master, 3rd Edition