From ruby-on-rails
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.
npx claudepluginhub bastos/ruby-plugin-marketplace --plugin ruby-on-railsThis skill uses the workspace's default tool permissions.
Comprehensive guide to real-time WebSocket communication in Rails.
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
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