class Comment < ApplicationRecord
belongs_to :post
belongs_to :author, class_name: 'User'
# Automatically broadcast changes to all subscribers of this post
after_create_commit -> { broadcast_append_to post, target: "comments" }
after_update_commit -> { broadcast_replace_to post }
after_destroy_commit -> { broadcast_remove_to post }
validates :body, presence: true
end
<%= turbo_stream_from @post %>
<article class="post">
<h1><%= @post.title %></h1>
<%= simple_format @post.body %>
<div class="comments-section mt-8">
<h2 id="comment_count"><%= pluralize @post.comments.count, 'comment' %></h2>
<div id="comments" class="space-y-4 my-6">
<%= render @post.comments %>
</div>
<%= render "comments/form", post: @post, comment: Comment.new %>
</div>
</article>
<%= turbo_frame_tag dom_id(comment) do %>
<div class="comment p-4 bg-gray-50 rounded">
<div class="flex items-start gap-3">
<%= image_tag comment.author.avatar_url, class: "w-10 h-10 rounded-full" %>
<div class="flex-1">
<div class="flex items-baseline gap-2">
<span class="font-semibold"><%= comment.author.name %></span>
<span class="text-sm text-gray-500"><%= time_ago_in_words comment.created_at %> ago</span>
</div>
<p class="mt-1"><%= comment.body %></p>
</div>
</div>
</div>
<% end %>
Broadcasting Turbo Streams via Action Cable enables real-time collaborative features without polling. When a model is created, updated, or destroyed, I broadcast the change to all subscribed users using broadcasts_to or manual broadcast_* methods. Subscribers receive Turbo Stream fragments that update their DOM automatically. This pattern powers collaborative editing, live notifications, real-time dashboards, and multiplayer features. The key is scoping broadcasts appropriately—I use stream_for to create user-specific or resource-specific channels. I also include the current user's ID in the broadcast payload so their UI doesn't duplicate the update they just made. Proper authorization ensures users only subscribe to streams they're allowed to see.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
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
<form data-controller="query-sync" data-action="change->query-sync#apply">
<select name="status" class="rounded border p-2">
<option value="">Any</option>
<option value="open">Open</option>
<option value="closed">Closed</option>
</select>
Filter UI that syncs query params via Stimulus (no front-end router)
class TasksController < ApplicationController
def destroy
@task = Task.find(params[:id])
@task.destroy!
respond_to do |format|
Remove deleted items instantly with turbo_stream.remove
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
static values = { delay: { type: Number, default: 250 } }
Debounced live search with Stimulus + Turbo Streams
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_member
def connect
self.current_member = env['warden']&.user || reject_unauthorized_connection
Presence indicator with ActionCable + Turbo Streams
Share this code
Here's the card — post it anywhere.