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.
/plugin marketplace add bastos/rails-plugin/plugin install bastos-ruby-on-rails@bastos/rails-pluginThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Comprehensive guide to real-time WebSocket communication in Rails.
# 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
rails generate channel Chat speak
Creates:
app/channels/chat_channel.rbapp/javascript/channels/chat_channel.js# 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
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!" })
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
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
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
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
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
// 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 })
}
}
)
// 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()}`)
// 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" })
# 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
# config/routes.rb
Rails.application.routes.draw do
mount ActionCable.server => "/cable"
end
# 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
# Gemfile
gem "redis"
# config/cable.yml
production:
adapter: redis
url: <%= ENV["REDIS_URL"] %>
channel_prefix: myapp_production
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
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
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
# 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
# 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
# test/models/message_test.rb
require "test_helper"
class MessageTest < ActiveSupport::TestCase
test "broadcasts on create" do
room = rooms(:one)
assert_broadcast_on("chat_#{room.id}", hash_including(message: "Hello")) do
Message.create!(room: room, user: users(:one), content: "Hello")
end
end
end
# spec/channels/chat_channel_spec.rb
require "rails_helper"
RSpec.describe ChatChannel, type: :channel do
let(:user) { create(:user) }
let(:room) { create(:room) }
before do
stub_connection current_user: user
end
it "subscribes to room stream" do
subscribe(room_id: room.id)
expect(subscription).to be_confirmed
expect(subscription).to have_stream_for(room)
end
it "broadcasts messages" do
subscribe(room_id: room.id)
expect {
perform :speak, message: "Hello"
}.to have_broadcasted_to(room).with(hash_including(type: "message"))
end
end
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 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 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.