erb ruby 71 lines · 3 tabs

Turbo Streams + authorization: signed per-user stream name

Shared by codesnips Jan 2026
3 tabs
<%# private stream: turbo signs the serialized record name %>
<%= turbo_stream_from current_user %>

<section class="notifications">
  <h1>Notifications</h1>

  <%= turbo_frame_tag "notifications" do %>
    <ul id="notifications">
      <% current_user.notifications.unread.recent.each do |notification| %>
        <%= render notification %>
      <% end %>
    </ul>
  <% end %>

  <% if current_user.notifications.unread.none? %>
    <p class="empty">You're all caught up.</p>
  <% end %>
</section>
3 files · erb, ruby Explain with highlit

This snippet shows how to keep Turbo Stream broadcasts private to a single user by combining a signed stream name with a server-side authorization check. By default turbo_stream_from accepts any string as a channel identifier, and because that identifier is serialized and signed into the page, a naive turbo_stream_from current_user still works — but only if the subscription is bound to the exact same signed token that the broadcast targets. The pattern here treats the stream name as a capability: the signed token embedded in the DOM is what proves the browser is allowed to listen.

In notifications.html.erb, the view calls turbo_stream_from with the current_user record itself rather than a raw id. Turbo signs the serialized name via Turbo::StreamsChannel, so the resulting data-signed-stream-name attribute is tamper-proof: a malicious client cannot swap in another user's id and receive their notifications. The rest of the template renders an notifications target that later appends will flow into.

Notification model produces the broadcasts. broadcasts_to scopes each stream to the owning user, meaning Notification.create! for a given user pushes a turbo_stream append only to that user's private channel. broadcast_prepend_later_to is used in notify! to push work onto a background job so the request isn't blocked on ActionCable delivery, which matters when a single action fans out many notifications.

TurboStreamsChannel is where authorization actually lives. Overriding subscribed and verifying the incoming signed_stream_name against current_user via Turbo::StreamsChannel.verified_stream_name closes the gap where a user with a valid session could hand-craft a subscription to someone else's stream. If verification fails or the record is not owned by current_user, the channel calls reject, which tears down the WebSocket subscription.

The trade-off is that signing binds a stream to a specific record identity, so streams cannot be shared ad hoc; that rigidity is exactly what makes them safe. A common pitfall is trusting the signature alone — signing proves the name was not altered, but the subscribed guard is still needed to enforce that the connection's authenticated identity matches the stream's owner. Reach for this when broadcasts carry per-user data and a shared or guessable channel name would leak information.


Related snips

Share this code

Here's the card — post it anywhere.

Turbo Streams + authorization: signed per-user stream name — share card
Link copied