Action Cable
๐ก Action Cable
express2-6 covered Socket.IO for exactly this in Express; Action Cable is Rails' own built-in answer to the same problem.
๐ Connections and Channels
# app/channels/application_cable/connection.rb module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_user def connect self.current_user = find_verified_user end private def find_verified_user if (user = User.find_by(id: cookies.encrypted[:user_id])) user else reject_unauthorized_connection end end end end
A Connection is established once per browser tab, the moment the WebSocket handshake happens โ it's where authentication lives, reusing the same encrypted session cookie rails2-1 already set during a normal HTTP login. identified_by :current_user declares that every message flowing through this connection knows which user it belongs to, and reject_unauthorized_connection refuses the handshake entirely for anyone who fails the check โ the WebSocket equivalent of rails2-1's require_login before_action.
A single Connection can host many Channels โ one per feature that needs real-time updates:
# app/channels/comments_channel.rb class CommentsChannel < ApplicationCable::Channel def subscribed post = Post.find(params[:post_id]) stream_for post end end
stream_for post subscribes this client to a stream scoped to one specific post โ Action Cable derives a unique stream identifier from the object automatically, so every browser tab currently viewing that post's comments joins the same stream, and tabs viewing a different post don't.
๐ข Broadcasting
# app/models/comment.rb class Comment < ApplicationRecord belongs_to :post after_create_commit :broadcast_comment private def broadcast_comment CommentsChannel.broadcast_to(post, CommentSerializer.new(self).as_json) end end
after_create_commit is a callback (from Course 1's validations/callbacks chapter, rails1-7) that fires only after the database transaction actually commits โ broadcasting before that point risks telling clients about a comment that a later rollback then undoes. broadcast_to(post, ...) pushes the payload to every client streaming that specific post, reusing the exact same CommentSerializer Chapter 5 wrote for the JSON API โ one source of truth for "what a comment looks like," whether it arrives via a REST request or a live broadcast.
๐ป The Client Side
// app/javascript/channels/comments_channel.js import consumer from "./consumer" consumer.subscriptions.create( { channel: "CommentsChannel", post_id: postId }, { received(data) { appendCommentToPage(data) } } )
The client subscribes with the same post_id param the server's subscribed method reads, and received fires every time a broadcast arrives on that stream โ no polling, no manually re-fetching the comments list.
๐ Action Cable vs Socket.IO
Rails Action Cable vs Express + Socket.IO (express2-6)
| Concern | Express + Socket.IO | Rails Action Cable |
|---|---|---|
| Connection auth | io.use() middleware | Connection#connect, identified_by |
| Grouping clients | Rooms (socket.join(room)) | Streams (stream_for / stream_from) |
| Sending a message | io.to(room).emit(event, data) | Channel.broadcast_to(object, data) |
| Feature organization | Event names on one connection | Separate channel class per feature |
| Triggering a broadcast | Called explicitly wherever needed | Often a model callback (after_create_commit) |
The concepts map closely โ a Socket.IO "room" and an Action Cable "stream" solve the same "only these clients get this message" problem. The organizational difference is Rails' usual one: a dedicated channel class per feature (CommentsChannel, NotificationsChannel) instead of a flat set of event name strings on one shared connection.
๐ป Coding Challenges
Challenge 1: A Notifications Channel
Write a NotificationsChannel where subscribed streams for the current connection's current_user (not a param โ the user identified on the connection itself).
Goal: Practice streaming scoped to the authenticated user rather than a URL parameter.
Challenge 2: Broadcasting on Create
Add an after_create_commit callback to a Notification model that broadcasts the new notification (serialized) to the NotificationsChannel stream for its owning user.
Goal: Practice triggering a broadcast from a model callback, tied to the right stream.
Challenge 3: Client Subscription
Write the JavaScript client code subscribing to NotificationsChannel and appending each received notification to a #notifications element on the page.
Goal: Practice wiring the client side of a channel subscription to a broadcast.
Action Cable is the right tool when data genuinely needs to appear without the user doing anything โ a live chat, a notification badge. If a page only needs "show me the latest data when I next visit or refresh," a plain request (Chapter 5's JSON API, or a normal page load) is simpler, has one less moving part (no persistent connection to manage), and is usually the better default.
๐ฏ What's Next
Next chapter: Hotwire โ Turbo & Stimulus โ how modern Rails apps add interactivity to server-rendered HTML without reaching for a full frontend framework.