The scenario
We're building an app for a café. Baristas add orders and mark them ready from a bar screen, and customers watch a pickup board for their name. Both screens should update live, over Turbo Stream broadcasts.
The burn
turbo-rails gives us a one-line macro, so order.rb says broadcasts inserts_by: :prepend. The pickup board only shows orders that are ready, and no macro can express that, so we hand-roll an after_update_commit callback that prepends to a "pickup" stream when an order becomes ready. The views subscribe with turbo_stream_from "orders" and turbo_stream_from "pickup". With one café, it works.
Then two things happen. A second café signs up, and every order added or marked ready at one café appears on the other's screens as well. The stream names are global, so every page that subscribes hears everything. And product asks for a bulk import of mobile orders. order_imports_controller.rb creates thirty orders, the callback fires thirty times, and every connected screen receives thirty separate broadcasts for one click.
There's a quieter cost too. broadcasts sends only creates to the named stream; updates go to each record's own stream, so _order.html.erb has to subscribe every row to itself.
The fix
Answer "who gets what, and when?" deliberately.
Who: stream names carry the café. The bar subscribes to [cafe, :bar] and the pickup board to [cafe, :pickup], and the targets use dom_target to match.
When: the model stops deciding. The refactored order.rb has no callbacks. It keeps composer methods, broadcast_created and broadcast_ready, that know what each update looks like, and the controller calls the one it means. mark_ready only changes state, returning false if the order was already ready, so nothing is announced twice. The import knows it's one batch, so it calls Cafe#broadcast_new_orders once, sending one message with thirty rows instead of thirty messages with one.
Why it matters
A callback fires from every write path: imports, backfills, the console. It can't tell them apart, for the same reason we hesitate to send email from after_create. The split is the same as a mailer's: the model composes the message, and the caller decides whether and when to send it.
Scoping matters for more than noise. turbo_stream_from signs the stream name, so a page can only subscribe to streams the server rendered for it. Rendering the tag is the authorization step, and a bare name like "orders" grants access to everyone's orders.
We could reach for broadcasts_refreshes_to :cafe instead, and it would fix both symptoms. But each refresh becomes one full page request per connected client, which is fine for a few screens and expensive for many.
Read More
- Turbo Handbook: Streams, on stream actions and how they reach the page.
Turbo::Broadcastablein turbo-rails, where thebroadcastsmacros and their defaults are defined.