ActionCable + Heroku + Custom Domain

My last blog post I discuss how I set up realtime chatting with the new Rails5 ActionCable.

Then I went to launch on heroku — worked perfectly.

Then I pushed to a heroku app with a custom domain name, let’s call it example.com . It didn’t seeing anything subscribing on the custom domain, but if I went to the herokuapp URL, I could see it subscribing the user.

The problem is that the server is still listening on: app-name.herokuapp.com

I searched for days and days for a way to allow the server to listen to the custom URL’s connection, and after it all… it was just a couple simple lines:

heroku config:set RAILS_HOST "http://www.example.com"

And in production.rb

config.action_cable.url = "wss://#{ENV['RAILS_HOST']}/cable"

I hope this helps anyone else who is working with ActionCable on a Heroku custom domain, or white-labeled URL!

 

Advertisements

Instant Chat with ActionCable5 on Heroku

I started with a chat app, based completely off of this awesome post Gmail Like Chat App with Ruby on Rails by Joseph Ndungu. It’s amazing, and if you don’t already have the  basics of a chat app set up, I suggest starting there.

Then I wanted to launch it on Heroku without running another server. Here is how I did it with ActionCable5 . After so many issues, and not being able to find a lot of documentation on it, I wanted to provide what my set up looks like.

Set up:

Install the gems:

gem 'actioncable'
gem 'puma' 
gem 'redis', '~> 3.2'

 

Procfile:

web: bundle exec puma -p $PORT ./config.ru 
actioncable: bundle exec puma -p $PORT cable/config.ru 
redis: redis-server

 

Config/routes.rb

 mount ActionCable.server => '/cable'

 

config/cable.yml

local: &local 
 adapter: async
 :url: redis://localhost:5000
 :host: localhost
 :port: 6379
 :timeout: 1
 :inline: true
development: *local 
test: *local


production: &production
 adapter: redis
 :url: <%= ENV["REDISTOGO_URL"] %>

 

**The production RedisURL is stored in my heroku config. In my console, I created the add on like this:

heroku addons:create redistogo:nano

The just run

heroku config

and you will be able to see your redistogo url for future reference.

config/application.rb

 config.action_cable.mount_path = '/websocket'

 

config/redis/cable.yml

local: &local 
 :url: redis://localhost:6379
 :host: localhost
 :port: 6379
 :timeout: 1
 :inline: true
development: *local 
test: *local

 

config/environments/development.rb

 config.action_cable.allowed_request_origins = ['localhost:3000']
 config.action_cable.url = "ws://localhost:3000/cable"

 

config/environments/production.rb

config.action_cable.url = 'wss://your-app-name.herokuapp.com/cable'
config.action_cable.allowed_request_origins = [ 'https://your-app-name.herokuapp.com', /http:\/\/your-app-name.herokuapp.com.*/ ]

 

cable/config.ru

require ::File.expand_path('../../config/environment', __FILE__) 
Rails.application.eager_load!

ActionCable.server.config.allowed_request_origins = ["http://localhost:3000"] 
run ActionCable.server

 

Okay, so now is a little background on my chat structure. There are Convos, which have many Msgs.

models/convo.rb

class Convo < ApplicationRecord
 belongs_to :sender, :foreign_key => :sender_id, class_name: 'User'
 belongs_to :recipient, :foreign_key => :recipient_id, class_name: 'User'

has_many :msgs, dependent: :destroy

validates_uniqueness_of :sender_id, :scope => :recipient_id

scope :involving, -> (user) do
 where("convos.sender_id =? OR convos.recipient_id =?",user.id,user.id)
 end

scope :between, -> (sender_id,recipient_id) do
 where("(convos.sender_id = ? AND convos.recipient_id =?) OR (convos.sender_id = ? AND convos.recipient_id =?)", sender_id,recipient_id, recipient_id, sender_id)
 end
end

models/msg.rb

class Msg < ApplicationRecord
 belongs_to :convo
 belongs_to :user
end

 

Okay — let’s set up the channel for broadcasting. I called mine MsgsChannel:

So, in your app directory, add a ‘channels’ directory. In there you’re going to add 3 files:

app/channels/application/channel.rb

module ApplicationCable 
 class Channel < ActionCable::Channel::Base
 end
end

app/channels/application/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 cookies.signed['user.id']
         verified_user = User.find_by(id: cookies.signed['user.id'])
         verified_user
       else
         reject_unauthorized_connection
       end
   end
end

 

 

app/channels/msgs_channel

class MsgsChannel < ApplicationCable::Channel 
 def subscribed
  @convos = Convo.where("sender_id = ? OR recipient_id = ?", current_user, current_user)
  @convos.each do |convo|
  stream_from "msg_channel_#{convo.id}"
  end
 end

 def unsubscribed

 end

 def speak() 
  ActionCable.server.broadcast "msg_channel_#{convo_id}"
 end

end

 

And let’s connect it to the client. ActionCable is built with coffee, but I’m going to use javascript.

In order to open a channel, you want to tell it where to listen.

VERY IMPORTANT: This line changes depending on the environment. Note the commented out third line, and make sure to uncomment, and comment out the localhost line when you push to heroku.

javascript/channels/convos.js

this.App = {};

App.cable = ActionCable.createConsumer()

 

javascript/cable.js

(function() {
 this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);

 

And then, what do you want to do over the channel:

javascript/channels/msgs.js

App.msgs = App.cable.subscriptions.create('MsgsChannel', { 
  received: function(data) {
   var chatbox = "#chatbox_" + data.convo + " .chatboxcontent"
     return $.ajax({ 
      type: "GET",
      url: '/appendMsg',
      data: {
       id: data.msg.id
      },
     success: function(result) { 
       $(chatbox).append(result).scrollTop(1000000);
      }
     });
   },
 renderMsg: function(data) {
   }
});

 

 

 

 

And in my MsgsController:

def create
  if @msg.save!
    ActionCable.server.broadcast "msg_channel_#{@convo.id}",
      msg: @msg,
      user: User.find_by(id: @msg.user_id),
      convo: @msg.convo_id,
      receiver: @email_user.id
    head :ok
    msg = @msg
  end
end

 

 

 

So many files I feel like I missed something… let me know if you see something missing!