action-cable-realtime
npx machina-cli add skill aehkyrr/rails-expert/action-cable-realtime --openclawAction Cable & Real-Time: WebSockets in Rails
Overview
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.
Action Cable enables:
- Real-time chat and messaging
- Live notifications
- Presence indicators (who's online)
- Collaborative editing
- Live dashboard updates
- Real-time feeds
Rails 8 introduces Solid Cable, which replaces Redis with database-backed pub/sub, simplifying deployment.
Core Concepts
WebSockets vs HTTP
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:
- Bi-directional communication
- Low latency (no connection overhead)
- Server can push to clients
- Efficient for real-time features
Action Cable Architecture
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
Channels are like controllers for WebSockets.
Creating a Channel
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 })
}
}
)
Streaming
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
Channel Callbacks
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.
Broadcasting
Send messages to channel subscribers:
From Models
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
From Controllers
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
From Jobs
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
Authentication
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.
Rails 8: Solid Cable
Solid Cable replaces Redis with database-backed pub/sub.
Configuration
# 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
Migration
rails solid_cable:install
rails db:migrate
Creates solid_cable_messages table.
Trade-offs
Solid Cable:
- Simpler deployment (no Redis)
- One database to manage
- ~100-150ms latency
- Sufficient for chat, notifications, updates
Redis:
- Lower latency (<50ms)
- Higher throughput
- Better for millions of connections
For most apps, Solid Cable is simpler and sufficient.
See references/solid-cable.md for details.
Common Patterns
Chat Application
# 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
Live Notifications
class NotificationChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
end
# Broadcast to specific user
NotificationChannel.broadcast_to(user, {
html: render_notification(notification)
})
Presence (Who's Online)
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.
Testing
Channel Tests
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
Integration Tests
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
Deployment Considerations
Standalone Server
For high-traffic apps, run Action Cable on separate servers:
# config/cable.yml
production:
adapter: solid_cable
url: wss://cable.example.com
Scaling
Action Cable scales horizontally:
- Multiple app servers
- Shared pub/sub (Solid Cable database or Redis)
- Load balancer with WebSocket support
Monitoring
Track connection count, message throughput, latency, and errors.
Alternatives to Action Cable
Server-Sent Events (SSE)
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)
Polling
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
Further Reading
For deeper exploration:
references/action-cable-patterns.md: Chat, notifications, presence patternsreferences/solid-cable.md: Database-backed pub/sub in Rails 8
For 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 updates
Summary
Action Cable provides:
- WebSocket integration with Rails
- Channels for logical grouping
- Broadcasting to connected clients
- Authentication via connection identification
- Solid Cable for database-backed pub/sub (Rails 8)
- Turbo Streams integration for HTML updates
Master Action Cable and you'll build real-time features that feel magical.
Source
git clone https://github.com/aehkyrr/rails-expert/blob/main/plugins/rails-expert/skills/action-cable-realtime/SKILL.mdView on GitHub Overview
Action Cable wires WebSockets into Rails to power real-time features like chat, notifications, and live updates. It pairs server-side Ruby channels with client-side JavaScript, enabling streaming messages, presence, and collaboration. Rails 8 introduces Solid Cable, a database-backed pub/sub option that simplifies deployment by replacing Redis.
How This Skill Works
A persistent WebSocket connection is opened per browser tab and routed through a Connection to logical Channels. Clients subscribe via Subscriptions, and Broadcasters push updates to all subscribers, triggering client-side UI updates. Solid Cable in Rails 8 swaps Redis with a database-backed pub/sub, influencing deployment choices and scalability.
When to Use It
- Building a real-time chat feature with channels and broadcasting
- Pushing live notifications to users without page refreshes
- Showing presence indicators (online/offline) across rooms or spaces
- Collaborative editing or shared workspaces with live updates
- Streaming dashboards or feeds with continuous updates (e.g., metrics, events)
Quick Start
- Step 1: Generate a channel (e.g., rails generate channel Chat) and implement stream_from in ChatChannel
- Step 2: Create a JavaScript consumer to subscribe to the channel and handle received data, plus a speak action to broadcast
- Step 3: Broadcast from the server (e.g., Message.create! or ActionCable.server.broadcast) and test in the browser
Best Practices
- Create separate channels per feature or room (e.g., ChatChannel, PresenceChannel) to keep concerns isolated
- Authenticate subscriptions and validate params to prevent unauthorized broadcasts
- Choose Solid Cable (Rails 8) for simpler deployment, or Redis for heavy pub/sub workloads
- Implement robust connect/disconnect handling and client reconnection logic
- Minimize payloads and use streaming of HTML or small JSON fragments rather than large payloads
Example Use Cases
- A multi-room chat app where each room has its own ChatChannel and room_id stream
- Live user notifications triggered by events (mentions, follows, or alerts)
- Presence indicators showing who’s online in a workspace or channel
- Collaborative document editing with real-time updates propagated to all collaborators
- Real-time dashboard displaying streaming metrics and activity feeds