Super Solid Cable
Solid Cable
Solid Cable is a new adapter for Action Cable - now the default in Rails 8 - that brings the magic of web sockets and real time updates to your Rails app.
I’d never had chance to really use Action Cable before, but a colleague gave me a great suggestion to improve an internal tool I’ve been building, and that coincided with me having just reviewed some upcoming videos for the Rails Foundation - including solid cable - and a 90 minute train journey from Bristol to London!
How far could I get introducing real time updates to my app on a train to London I wondered? What could go wrong? Well, Solid Cable is so ridiculously simple that I’d finished before I even got to Paddington! Now en-route home on the same journey, I thought I would write up a blog post, as always, for future me. So, here we go.
Simple Demo App
Just to keep things really simple, lets create a new Rails 8 app that lets people chat. Here’s the simplest thing I could come up with but will be perfect to illustrate how to use Solid Cable.
First up you’ll need Ruby and Rails 8 on your machine. If you don’t yet, follow the steps in section 3.1 of the fantastic, newly polished and updated Rails Guides here
OK, lets go ahead and create our new Rails app:
rails new super-simple-chat -c tailwind
Next, we’re going to create a model, a controller and some views to play with. Initially we’ll use standard
rails and the app will be feature complete. First we’ll cd
into the app directory and generate
the model, controller and index view by running these commands on the console:
# change into the app directory
cd super-simple-chat
# generate the Chat model and controller
rails g model chat message:string
rails g controller chats index create
# migrate the database
rails db:migrate
We need to add a bit of code here, and I won’t explain this in detail since it’s very basic Rails. I’ll save the more detailed descriptions for when we add the real time Solid Cable magic coming up. So, open up each of these files and replace the existing code in each with the code below:
config/routes.rb
Rails.application.routes.draw do
resources :chats
root "chats#index"
end
app/controllers/chats_controller.rb
class ChatsController < ApplicationController
def index
@chats = Chat.all
end
def create
@chat = Chat.build(chat_params)
@chat.save
redirect_to chats_path
end
private
def chat_params
params.require(:chat).permit(:message)
end
end
app/views/chats/index.html.erb
<div class="w-1/2">
<h1 class="mb-6 text-slate-700 font-bold text-2xl">Chats</h1>
<div class="space-y-4 mb-8">
<div id="chats">
<%= render @chats %>
</div>
</div>
<%= render "form" %>
</div>
And then we need to create two partials, one for the input form, and one for the individual chat messages:
app/views/chats/_form.html.erb
<%= form_with model: Chat.new, local: false do |f| %>
<div class="flex">
<%= f.text_field :message,
autocomplete: :off,
class: "block w-2/3 rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400",
placeholder: "enter message..." %>
<%= f.submit "Send",
class: "inline-flex w-1/3 items-center justify-center rounded-md bg-slate-400 ml-3 px-3 py-2 text-sm font-semibold text-white shadow-sm" %>
</div>
<% end %>
app/views/chats/_chat.html.erb
<div>
<div class="flex justify-between items-start">
<div class="text-gray-700">
<%= chat.message %>
</div>
<div class="text-sm text-gray-400">
<%= time_ago_in_words(chat.created_at) %> ago
</div>
</div>
</div>
The Basic App
OK! If you followed the instructions above, you should be able to run the app with bin/dev
in the
console and visit http://localhost:3000. It should look like the
screenshot above, and you can type in chats and see them rendered above the input box. Try it out!
This actually works great, and shows the power of Rails out of the box. Whats happening is the text input is posted to the server and the page is reloaded rendering the list of messages.
If someone else sends a message from another server though, we won’t see it unless we reload the page. We want to see these messages in real time, that’s how we roll, so lets bring the magic with Solid Cable!
Introducing Solid Cable
We’ve built the basics, and this is essentially where I was with my app. To introduce Solid Cable we need to have the gem in our Gemfile and some minor configuration set up.
If you’re using Rails 8.0.0, you’re in luck. You don’t need to do anything! Solid Cable is available by default. If you don’t have Solid Cable for some reason, like an older version of Rails, you’ll need to add the gem and install as follows:
bundle add solid_cable
bin/rails solid_cable:install
Configuring Solid Cable
The main configuration file for Action Cable is config/cable.yml
and by default in “development” the
adapter is set to “async”. Since we want to enable Solid Cable on localhost, we need to update this
file, so open it up and change:
development:
adapter: async
to:
development:
adapter: solid_cable
connects_to:
database:
writing: cable
polling_interval: 0.1.seconds
message_retention: 1.day
Then lastly we need to set up a cable database for Solid Cable to store our messages. Unlike the
typical way Action Cable is used where events are published to Redis then immediately
broadcast back to Action Cable with a pub/sub strategy, with Solid Cable, messages are written to a database
and Solid Cable polls for new ones, broadcasting them back to clients when found. Solid Cable also cleans
up after itself, trimming data older than message_retention
above.
So we need to set up a new database to accept the events from Solid Cable. SQLite is perfect for this task
so similar to the cable config above, we need to set up the “cable” database in our development environment.
Open up the config/database.yml
file and change the following:
development:
<<: *default
database: storage/development.sqlite3
to
development:
primary:
<<: *default
database: storage/development.sqlite3
cable:
<<: *default
database: storage/development_cable.sqlite3
migrations_paths: db/cable_migrate
Now lets initialise the database with rails db:prepare
Unleash the Web Sockets
OK! Back to our app now. We’re going to add some real time web socket goodness with just a few lines of code!
Firstly we need to web socket enable our view so that newly broadcasted Chats can be rendered. Open
up the app/views/chats/index.html.erb
file and insert the turbo_stream_from
tag on line 4 as shown here
<div class="w-1/2">
<h1 class="mb-6 text-slate-700 font-bold text-2xl">Chats</h1>
<div class="space-y-4 mb-8">
<%= turbo_stream_from "chats" %>
<div id="chats">
<%= render @chats %>
</div>
</div>
<%= render "form" %>
</div>
And then we just need to update our Chat model to broadcast when a new Chat is created. Open up
app/models/chat.rb
and replace the code with the following:
class Chat < ApplicationRecord
after_create_commit :broadcast_chat
def broadcast_chat
broadcast_append_to(
"chats",
target: "chats",
partial: "chats/chat",
locals: { chat: self }
)
end
end
One last tidy up is we don’t need to redirect to the index page in the controller after a Chat is created.
We’ll be updating the page via websockets, so we can just return nothing. In the
app/controllers/chats_controller.rb
file “create” action, simple replace the line
redirect_to chats_path
with
render json: {}, status: :no_content
Now you may not believe this. But thats it. You’re done! Restart your server, then open up two browsers and behold the magic! When you send a chat in one, you should see it in the other in real time, no refresh required!
Under the Hood
If you take a look in your Rails log you should be able to see not only the Chat record being inserted when a chat message is sent, but also the event being inserted in the cable database. It will looks something like this:
SolidCable::Message Insert (0.5ms) INSERT INTO "solid_cable_messages" ("created_at","channel",
"payload","channel_hash") VALUES ('2024-11-26 06:23:40.882477', x'6368617473', x'225c753030336
3747572626f2d73747265616d20616374696f6e3d5c22617070656e645c22207461726765743d5c2263686174735c2
25c75303033655c753030336374656d706c6174655c75303033655c7530303363212d2d20424547494e206170702f7
6696577732f63686174732f5f636861742e68746d6c2e657262202d2d5c75303033655c75303033636469765c75303
033655c6e20205c753030336364697620636c6173733d5c22666c6578206a7573746966792d6265747765656e20697
f6469765c75303033655c6e202020205c753030336364697620636c6173733d5c22746578742d736d20746578742d6
77261792d3430305c225c75303033655c6e2020202020206c657373207468616e2061206d696e7574652061676f5c6
e202020205c75303033632f6469765c75303033655c6e20205c75303033632f6469765c75303033655c6e5c7530303
3632f6469765c75303033655c6e5c7530303363212d2d20454e44206170702f76696577732f63686174732f5f63686
1742e68746d6c2e657262202d2d5c75303033655c75303033632f74656d706c6174655c75303033655c75303033632
f747572626f2d73747265616d5c753030336522', -2034957492849835134) ON CONFLICT DO NOTHING RETURNING
"id" /*action='create',application='SuperSolidCable',controller='chats'*/
and then also you can see our partial being broadcast out to clients with the “append” action
Turbo::StreamsChannel transmitting "<turbo-stream action=\"append\" target=\"chats\"><template>
<!-- BEGIN app/views/chats/_chat.html.erb --><div>\n <div class=\"flex justify-between items-
start\">\n <div class=\"text-gray-700\">\n solid cable\n </div>\n <div class=\"te
xt-sm text-gray-400\">\n less than a minute... (via streamed from chats)
Pretty damn cool!
Wrapping Up
This is quite a long blog post to demonstrate how you can add web sockets and real time updates in your Rails apps with a few lines of code, I know, but hopefully its been fun.
As always, oh wait, not as always as I moved to Blue Sky mostly, but yes, don’t hesitate to drop me a note there at https://bsky.app/profile/mileswoodroffe.com or via any channels listed here