Action Cable & WebSocket Patterns
Action Cable Patterns
Real-time WebSocket features for Rails applications.
Real-Time Feature Decision Tree
What real-time feature?
│
├─ User notifications
│ └─ Personal stream: stream_from "notifications_#{current_user.id}"
│
├─ Chat room messages
│ └─ Group stream: stream_from "chat_room_#{room.id}"
│
├─ Model updates (live editing)
│ └─ Model stream: stream_for @post (with broadcast_to)
│
├─ Presence tracking (who's online)
│ └─ Presence stream + Redis: stream_from "presence_room_#{room.id}"
│
└─ Dashboard/analytics
└─ Scoped stream: stream_from "dashboard_#{account.id}"
Core Principles (CRITICAL)
1. Authorization First
# WRONG - Security vulnerability!
def subscribed
stream_from "private_data" # Anyone can subscribe!
end
# RIGHT - Explicit authorization
def subscribed
reject unless current_user
reject unless current_user.can_access?(params[:resource_id])
stream_from "private_#{params[:resource_id]}"
end
2. Persist First, Broadcast Second
# WRONG - Data lost if client offline
def speak(data)
ActionCable.server.broadcast("chat", message: data['text'])
end
# RIGHT - Persist then broadcast
def speak(data)
message = Message.create!(user: current_user, text: data['text'])
ActionCable.server.broadcast("chat", message: message)
end
3. Use stream_for for Models
# WRONG - Manual naming (error-prone)
stream_from "posts:#{params[:id]}"
ActionCable.server.broadcast("posts:#{@post.id}", data)
# RIGHT - Type-safe model broadcasting
stream_for @post
PostChannel.broadcast_to(@post, data)
NEVER Do This
NEVER skip authorization:
# Every channel MUST have: reject unless current_user
# Plus resource-specific authorization
NEVER broadcast before commit:
# WRONG
post.save
ActionCable.server.broadcast(...) # Transaction may rollback!
# RIGHT - Use after_commit callback
after_create_commit { broadcast_creation }
NEVER broadcast full objects:
# WRONG - Leaks data, slow
ActionCable.server.broadcast("posts", post: @post)
# RIGHT - Only needed fields
ActionCable.server.broadcast("posts", post: @post.as_json(only: [:id, :title]))
NEVER create subscriptions without cleanup (JavaScript):
// WRONG - Memory leak
consumer.subscriptions.create("ChatChannel", { ... })
// RIGHT - Cleanup on unmount
useEffect(() => {
const sub = consumer.subscriptions.create(...)
return () => sub.unsubscribe()
}, [])
Channel Template
class NotificationsChannel < ApplicationCable::Channel
def subscribed
# 1. Authorization (REQUIRED)
reject unless current_user
# 2. Subscribe to stream
stream_from "notifications_#{current_user.id}"
end
def unsubscribed
# Cleanup (optional)
end
# Client action: channel.perform('mark_as_read', {id: 123})
def mark_as_read(data)
notification = current_user.notifications.find(data['id'])
notification.mark_as_read!
ActionCable.server.broadcast(
"notifications_#{current_user.id}",
action: 'count_updated',
unread_count: current_user.notifications.unread.count
)
end
end
Stream Patterns Quick Reference
| Pattern | Use Case | Code |
|---|---|---|
| Personal | Notifications | stream_from "user_#{current_user.id}" |
| Model | Live updates | stream_for @post → PostChannel.broadcast_to(@post, data) |
| Group | Chat rooms | stream_from "room_#{room.id}" |
| Presence | Who's online | stream_from "presence_#{room.id}" + Redis |
Broadcasting Patterns
From Model (Recommended)
class Post < ApplicationRecord
after_create_commit { broadcast_creation }
after_update_commit { broadcast_update }
private
def broadcast_creation
PostChannel.broadcast_to(self, action: 'created', post: as_json(only: [:id, :title]))
end
end
From Controller
def create
@comment = @post.comments.create!(comment_params)
CommentsChannel.broadcast_to(@post, action: 'created', comment: @comment.as_json)
end
From Background Job
class BroadcastJob < ApplicationJob
def perform(channel_name, data)
ActionCable.server.broadcast(channel_name, data)
end
end
Connection Authentication
# 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
# Cookie auth (default Rails)
if user = User.find_by(id: cookies.encrypted[:user_id])
user
# Token auth (API clients)
elsif user = find_user_from_token
user
else
reject_unauthorized_connection
end
end
def find_user_from_token
token = request.params[:token]
return nil unless token
payload = JWT.decode(token, Rails.application.secret_key_base).first
User.find_by(id: payload['user_id'])
rescue JWT::DecodeError
nil
end
end
end
Testing Quick Reference
# spec/channels/notifications_channel_spec.rb
RSpec.describe NotificationsChannel, type: :channel do
let(:user) { create(:user) }
before { stub_connection(current_user: user) }
it 'subscribes to user stream' do
subscribe
expect(subscription).to be_confirmed
expect(subscription).to have_stream_from("notifications_#{user.id}")
end
it 'rejects unauthenticated users' do
stub_connection(current_user: nil)
subscribe
expect(subscription).to be_rejected
end
it 'broadcasts on action' do
subscribe
expect {
perform :mark_as_read, id: notification.id
}.to have_broadcasted_to("notifications_#{user.id}")
end
end
Production Config
# config/cable.yml
production:
adapter: redis
url: <%= ENV['REDIS_URL'] %>
channel_prefix: myapp_production
# config/environments/production.rb
config.action_cable.url = ENV['ACTION_CABLE_URL']
config.action_cable.allowed_request_origins = ['https://example.com']
References
Detailed examples in references/:
javascript-consumers.md- Client-side subscription patternspresence-tracking.md- Complete presence implementation with Redisdeployment.md- Nginx, scaling, production configuration
More from kaakati/rails-enterprise-dev
flutter conventions & best practices
Dart 3.x and Flutter 3.x conventions, naming patterns, code organization, null safety, and async/await best practices
55getx state management patterns
GetX controllers, reactive state, dependency injection, bindings, navigation, and best practices
52tailadmin ui patterns
TailAdmin dashboard UI framework patterns and Tailwind CSS classes. ALWAYS use this skill when: (1) Building any dashboard or admin panel interface, (2) Creating data tables, cards, charts, or metrics displays, (3) Implementing forms, buttons, alerts, or modals, (4) Building navigation (sidebar, header, breadcrumbs), (5) Any UI work that should follow TailAdmin design. This skill REQUIRES fetching from the official GitHub repository to ensure accurate class usage - NEVER invent classes.
39mvvm-architecture
Expert MVVM decisions for iOS/tvOS: choosing between ViewModel patterns (state enum vs published properties vs Combine), service layer boundaries, dependency injection strategies, and testing approaches. Use when designing ViewModel architecture, debugging data flow issues, or deciding where business logic belongs. Trigger keywords: MVVM, ViewModel, ObservableObject, @StateObject, service layer, dependency injection, unit test, mock, architecture
36rails localization (i18n) - english & arabic
Comprehensive internationalization skill for Ruby on Rails applications with proper English and Arabic translations, RTL support, pluralization rules, date/time formatting, and culturally appropriate content adaptation.
34rspec testing patterns
Complete guide to testing Ruby on Rails applications with RSpec. Use this skill when writing unit tests, integration tests, system tests, or when setting up test infrastructure including factories, shared examples, and mocking strategies.
31