Action Cable integrates WebSockets with Rails, enabling real-time features like chat, notifications, and live updates. It provides both server-side Ruby and client-side JavaScript frameworks that work together seamlessly.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
examples/chat-channel.rbexamples/collaborative-editing.rbexamples/dashboard-channel.rbexamples/live-feed.rbexamples/multi-room-chat.rbexamples/notifications-channel.rbexamples/presence-channel.rbreferences/action-cable-patterns.mdreferences/solid-cable.mdAction Cable integrates WebSockets with Rails, enabling real-time features like chat, notifications, and live updates. It provides both server-side Ruby and client-side JavaScript frameworks that work together seamlessly.
Action Cable enables:
Rails 8 introduces Solid Cable, which replaces Redis with database-backed pub/sub, simplifying deployment.
HTTP (Request-Response):
Client → Request → Server
Client ← Response ← Server
[Connection closes]
WebSocket (Persistent Connection):
Client ↔ Persistent Connection ↔ Server
[Messages flow both directions]
[Connection stays open]
Benefits:
Browser (Consumer) → WebSocket → Connection → Channels → Broadcasters
Connection: WebSocket connection (one per browser tab) Channel: Logical grouping (like a controller) Subscription: Consumer subscribed to a channel Broadcasting: Message sent to all channel subscribers
Channels are like controllers for WebSockets.
rails generate channel Chat
Generates:
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
# Called when consumer subscribes
stream_from "chat_#{params[:room_id]}"
end
def unsubscribed
# Called when consumer unsubscribes (cleanup)
end
def speak(data)
# Called when consumer sends message
Message.create!(
content: data['message'],
user: current_user,
room_id: params[:room_id]
)
end
end
// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create(
{ channel: "ChatChannel", room_id: 123 },
{
connected() {
console.log("Connected to chat")
},
disconnected() {
console.log("Disconnected from chat")
},
received(data) {
// Handle broadcasted message
const messagesContainer = document.getElementById("messages")
messagesContainer.insertAdjacentHTML("beforeend", data.html)
},
speak(message) {
this.perform("speak", { message: message })
}
}
)
Subscribe to broadcasts:
class ChatChannel < ApplicationCable::Channel
def subscribed
# Stream from named channel
stream_from "chat_room_#{params[:room_id]}"
# Stream for current user only
stream_for current_user
# Stop streaming
stop_all_streams
end
end
Channels support lifecycle callbacks and exception handling:
class ChatChannel < ApplicationCable::Channel
before_subscribe :verify_access
after_subscribe :log_subscription
rescue_from UnauthorizedError, with: :handle_unauthorized
def subscribed
stream_from "chat_#{params[:room_id]}"
end
private
def verify_access
reject unless current_user.can_access?(params[:room_id])
end
def log_subscription
Rails.logger.info "User #{current_user.id} subscribed to chat"
end
def handle_unauthorized(exception)
# Handle error, optionally broadcast error message
transmit(error: "Unauthorized access")
end
end
Available callbacks: before_subscribe, after_subscribe, before_unsubscribe, after_unsubscribe.
Send messages to channel subscribers:
class Message < ApplicationRecord
belongs_to :room
belongs_to :user
after_create_commit :broadcast_message
private
def broadcast_message
ActionCable.server.broadcast(
"chat_room_#{room_id}",
{
html: ApplicationController.render(
partial: 'messages/message',
locals: { message: self }
),
user: user.name
}
)
end
end
class MessagesController < ApplicationController
def create
@message = Message.new(message_params)
if @message.save
# Broadcast happens in model callback
head :ok
else
render json: { errors: @message.errors }, status: :unprocessable_entity
end
end
end
class NotificationBroadcastJob < ApplicationJob
queue_as :default
def perform(notification)
ActionCable.server.broadcast(
"notifications_#{notification.user_id}",
{ html: render_notification(notification) }
)
end
private
def render_notification(notification)
ApplicationController.render(
partial: 'notifications/notification',
locals: { notification: notification }
)
end
end
Authenticate WebSocket connections:
# 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 verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end
Now current_user is available in all channels.
Solid Cable replaces Redis with database-backed pub/sub.
# config/cable.yml
production:
adapter: solid_cable
polling_interval: 0.1 # 100ms
message_retention: 1.day
# No Redis needed!
# Solid Cable stores messages in database
# Polls for new messages every 100ms
rails solid_cable:install
rails db:migrate
Creates solid_cable_messages table.
Solid Cable:
Redis:
For most apps, Solid Cable is simpler and sufficient.
See references/solid-cable.md for details.
# Channel
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room_id]}"
end
def speak(data)
Message.create!(
room_id: params[:room_id],
user: current_user,
content: data['message']
)
end
end
# Model
class Message < ApplicationRecord
after_create_commit -> {
broadcast_append_to "chat_#{room_id}",
target: "messages",
partial: "messages/message",
locals: { message: self }
}
end
class NotificationChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
end
# Broadcast to specific user
NotificationChannel.broadcast_to(user, {
html: render_notification(notification)
})
class AppearanceChannel < ApplicationCable::Channel
def subscribed
stream_from "appearances"
broadcast_appearance("online")
end
def unsubscribed
broadcast_appearance("offline")
end
def appear
broadcast_appearance("online")
end
def away
broadcast_appearance("away")
end
private
def broadcast_appearance(status)
ActionCable.server.broadcast("appearances", {
user_id: current_user.id,
username: current_user.name,
status: status
})
end
end
See references/action-cable-patterns.md for more examples.
require "test_helper"
class ChatChannelTest < ActionCable::Channel::TestCase
test "subscribes to stream" do
subscribe room_id: 42
assert subscription.confirmed?
assert_has_stream "chat_42"
end
test "receives broadcasts" do
subscribe room_id: 42
perform :speak, message: "Hello!"
assert_broadcast_on("chat_42", message: "Hello!")
end
end
test "broadcasts message to chat" do
room = rooms(:general)
assert_broadcasts("chat_#{room.id}", 1) do
Message.create!(room: room, user: users(:alice), content: "Hello!")
end
end
For high-traffic apps, run Action Cable on separate servers:
# config/cable.yml
production:
adapter: solid_cable
url: wss://cable.example.com
Action Cable scales horizontally:
Track connection count, message throughput, latency, and errors.
One-way server → client:
def stream
response.headers['Content-Type'] = 'text/event-stream'
response.headers['Cache-Control'] = 'no-cache'
sse = SSE.new(response.stream)
sse.write({ message: "Hello" })
ensure
sse.close
end
Use when: Only server → client needed (notifications, live feeds)
Regular HTTP requests:
setInterval(() => {
fetch('/api/notifications/latest')
.then(r => r.json())
.then(data => updateUI(data))
}, 5000)
Use when: Simple updates, low frequency, broad browser support needed
For deeper exploration:
references/action-cable-patterns.md: Chat, notifications, presence patternsreferences/solid-cable.md: Database-backed pub/sub in Rails 8For code examples (in examples/):
chat-channel.rb: Real-time chat with typing indicatorsnotifications-channel.rb: User-specific push notificationspresence-channel.rb: Online status trackingdashboard-channel.rb: Admin dashboard with live statsmulti-room-chat.rb: Multiple rooms with private messagescollaborative-editing.rb: Document editing with cursorslive-feed.rb: Real-time feed updatesAction Cable provides:
Master Action Cable and you'll build real-time features that feel magical.