action-cable
This skill should be used when the user asks about "WebSockets", "Action Cable", "real-time", "channels", "broadcasting", "streams", "subscriptions", "live updates", "push notifications", "chat features", or needs guidance on implementing real-time features in Rails applications.
What this skill does
# Action Cable
Comprehensive guide to real-time WebSocket communication in Rails.
## Server Setup
### Connection
```ruby
# 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
```
### Channel Generator
```bash
rails generate channel Chat speak
```
Creates:
- `app/channels/chat_channel.rb`
- `app/javascript/channels/chat_channel.js`
## Channels
### Basic Channel
```ruby
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room_id]}"
end
def unsubscribed
# Cleanup when channel is unsubscribed
end
def speak(data)
Message.create!(
content: data["message"],
user: current_user,
room_id: params[:room_id]
)
end
end
```
### Stream Methods
```ruby
class NotificationsChannel < ApplicationCable::Channel
def subscribed
# Stream from string identifier
stream_from "notifications_#{current_user.id}"
# Stream for model (uses GlobalID)
stream_for current_user
end
end
# Broadcasting to stream_for
NotificationsChannel.broadcast_to(user, { type: "alert", message: "Hello!" })
```
### Rejecting Subscriptions
```ruby
class ChatChannel < ApplicationCable::Channel
def subscribed
room = Room.find(params[:room_id])
if room.accessible_by?(current_user)
stream_for room
else
reject
end
end
end
```
## Broadcasting
### From Models
```ruby
class Message < ApplicationRecord
belongs_to :room
belongs_to :user
after_create_commit :broadcast_message
private
def broadcast_message
ActionCable.server.broadcast(
"chat_#{room_id}",
{ message: content, user: user.name, created_at: created_at }
)
end
end
```
### From Controllers
```ruby
class MessagesController < ApplicationController
def create
@message = current_user.messages.create!(message_params)
ActionCable.server.broadcast(
"chat_#{@message.room_id}",
render_message(@message)
)
head :ok
end
private
def render_message(message)
ApplicationController.renderer.render(
partial: "messages/message",
locals: { message: message }
)
end
end
```
### From Jobs
```ruby
class BroadcastMessageJob < ApplicationJob
queue_as :default
def perform(message)
ActionCable.server.broadcast(
"chat_#{message.room_id}",
message: render_message(message)
)
end
private
def render_message(message)
MessagesController.render(
partial: "messages/message",
locals: { message: message }
)
end
end
```
### With Turbo Streams
```ruby
class Message < ApplicationRecord
belongs_to :room
after_create_commit { broadcast_append_to room }
after_update_commit { broadcast_replace_to room }
after_destroy_commit { broadcast_remove_to room }
end
```
## Client-Side (JavaScript)
### Subscription
```javascript
// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create(
{ channel: "ChatChannel", room_id: roomId },
{
connected() {
console.log("Connected to chat")
},
disconnected() {
console.log("Disconnected from chat")
},
received(data) {
// Called when data is broadcast
const messagesContainer = document.getElementById("messages")
messagesContainer.insertAdjacentHTML("beforeend", data.message)
},
speak(message) {
this.perform("speak", { message: message })
}
}
)
```
### Consumer Setup
```javascript
// app/javascript/channels/consumer.js
import { createConsumer } from "@rails/actioncable"
export default createConsumer()
// With custom URL
export default createConsumer("/cable")
// With authentication token
export default createConsumer(`/cable?token=${getToken()}`)
```
### Dynamic Subscriptions
```javascript
// Subscribe dynamically
const subscription = consumer.subscriptions.create(
{ channel: "ChatChannel", room_id: 123 },
{
received(data) {
console.log(data)
}
}
)
// Unsubscribe
subscription.unsubscribe()
// Perform action
subscription.perform("speak", { message: "Hello" })
```
## Configuration
### Cable Configuration
```yaml
# config/cable.yml
development:
adapter: async
test:
adapter: test
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: myapp_production
```
### Routes
```ruby
# config/routes.rb
Rails.application.routes.draw do
mount ActionCable.server => "/cable"
end
```
### Allowed Origins
```ruby
# config/environments/production.rb
config.action_cable.allowed_request_origins = [
"https://example.com",
/https:\/\/.*\.example\.com/
]
# Or allow all (not recommended for production)
config.action_cable.disable_request_forgery_protection = true
```
### Redis Adapter
```ruby
# Gemfile
gem "redis"
# config/cable.yml
production:
adapter: redis
url: <%= ENV["REDIS_URL"] %>
channel_prefix: myapp_production
```
## Common Patterns
### Presence Tracking
```ruby
class AppearanceChannel < ApplicationCable::Channel
def subscribed
stream_from "appearance_channel"
current_user.appear
end
def unsubscribed
current_user.disappear
end
def appear(data)
current_user.appear(on: data["appearing_on"])
end
def away
current_user.away
end
end
```
### Typing Indicators
```ruby
class TypingChannel < ApplicationCable::Channel
def subscribed
stream_from "typing_#{params[:room_id]}"
end
def typing
ActionCable.server.broadcast(
"typing_#{params[:room_id]}",
{ user_id: current_user.id, name: current_user.name }
)
end
def stopped_typing
ActionCable.server.broadcast(
"typing_#{params[:room_id]}",
{ user_id: current_user.id, stopped: true }
)
end
end
```
### Room-Based Chat
```ruby
class RoomChannel < ApplicationCable::Channel
def subscribed
@room = Room.find(params[:room_id])
stream_for @room
# Notify others user joined
broadcast_user_joined
end
def unsubscribed
broadcast_user_left if @room
end
def speak(data)
message = @room.messages.create!(
user: current_user,
content: data["message"]
)
RoomChannel.broadcast_to(@room, {
type: "message",
message: render_message(message)
})
end
private
def broadcast_user_joined
RoomChannel.broadcast_to(@room, {
type: "user_joined",
user: { id: current_user.id, name: current_user.name }
})
end
def broadcast_user_left
RoomChannel.broadcast_to(@room, {
type: "user_left",
user: { id: current_user.id }
})
end
def render_message(message)
ApplicationController.renderer.render(
partial: "messages/message",
locals: { message: message }
)
end
end
```
## Testing
### Channel Tests
```ruby
# test/channels/chat_channel_test.rb
require "test_helper"
class ChatChannelTest < ActionCable::Channel::TestCase
test "subscribes to room stream" do
subscribe room_id: 1
assert subscription.confirmed?
assert_has_stream "chat_1"
end
test "rejects without room_id" do
subscribe
assert subscription.rejected?
end
end
```
### Connection Tests
```ruby
# test/channels/application_cable/connection_test.rb
require "test_helper"
class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
test "connects with valid user" do
user = users(:one)
cookies.encrypted[:user_id] = user.id
connect
assert_equal user, connection.current_user
end
test "rejects connection without user" do
assert_reject_connection { connect }
end
end
```
Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.