module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
logger.add_tags "ActionCable", current_user.id
end
private
def find_verified_user
if (user = User.find_by(id: cookies.encrypted[:user_id]))
user
else
reject_unauthorized_connection
end
end
end
end
class NotificationsChannel < ApplicationCable::Channel
def subscribed
project = current_user.projects.find(params[:notifiable_id])
stream_for project
rescue ActiveRecord::RecordNotFound
reject
end
def unsubscribed
stop_all_streams
end
end
class Notification < ApplicationRecord
belongs_to :project
belongs_to :recipient, class_name: "User"
validates :body, presence: true
after_create_commit :broadcast_to_recipient
private
def broadcast_to_recipient
broadcast_prepend_later_to(
project,
channel: NotificationsChannel,
target: "notifications",
partial: "notifications/notification",
locals: { notification: self }
)
end
end
<li id="<%= dom_id(notification) %>" class="notification" data-unread="<%= notification.read_at.nil? %>">
<span class="notification__body"><%= notification.body %></span>
<time datetime="<%= notification.created_at.iso8601 %>">
<%= time_ago_in_words(notification.created_at) %> ago
</time>
<%= button_to "Dismiss",
notification_path(notification),
method: :delete,
form: { data: { turbo_confirm: nil } },
class: "notification__dismiss" %>
</li>
This snippet shows how a Rails app streams live UI updates to a single authenticated user over ActionCable while keeping the stream authorized, so one user can never subscribe to another user's channel. The pattern combines Turbo Streams (which render partials and wrap them in <turbo-stream> frames) with a hand-written ActionCable channel that verifies ownership before calling stream_for.
In Connection, the WebSocket connection is identified by current_user, resolved from an encrypted, signed cookie rather than a request parameter. Because ActionCable connections are long-lived and separate from the normal request cycle, find_verified_user rejects the connection with reject_unauthorized_connection when no valid session exists. This means authentication happens once at connect time, and every channel on that connection inherits the trusted current_user.
NotificationsChannel is where the real safety lives. The client sends a subscribed request with a notifiable_id, but the channel never trusts that id blindly — it scopes the lookup to current_user.projects, so a forged id for someone else's project raises ActiveRecord::RecordNotFound and the subscription is rejected. Only after the record is confirmed to belong to the connected user does stream_for project open the stream. Using stream_for (rather than a raw stream_from with a string key) lets Rails derive the broadcasting name from the object and the channel class, avoiding manually built, guessable channel names.
The broadcast itself is triggered from Notification model in an after_create_commit callback. broadcast_prepend_later_to enqueues the render on a background job so the transaction commits fast and the ERB partial is rendered off the request thread. It targets the DOM id notifications and renders the Notification partial.
That partial in Notification partial is deliberately plain: a turbo_frame-friendly <li> keyed by dom_id(notification), which Turbo uses to prepend without duplicates. The key trade-off is that authorization must be enforced at subscribe time because the socket bypasses controller filters; forgetting the scoped lookup would leak every project's notifications to every client. This approach fits dashboards, chat, and activity feeds where updates must reach exactly one user in real time.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.