action cable & websocket patterns

Action Cable Patterns

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "action cable & websocket patterns" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-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

  1. 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

  1. 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 patterns

  • presence-tracking.md

  • Complete presence implementation with Redis

  • deployment.md

  • Nginx, scaling, production configuration

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review