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.
/plugin marketplace add sjnims/rails-expert/plugin install rails-expert@rails-expert-marketplaceThis 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.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.