Transport: pgbus vs Action Cable
phlex-reactive's client → server half is a plain HTTP POST. The server → client half (broadcasts) rides on whatever transport turbo-rails is configured with.
Drop-in: nothing in your code changes#
With pgbus installed, broadcast_*_to AND turbo_stream_from route over Postgres SSE automatically — with zero code changes. Both transports drive the SAME broadcast API: the gem's broadcast_replace_to/broadcast_append_to call ::Turbo::StreamsChannel.* unchanged, so swapping the transport is a Gemfile decision, not a rewrite.
A record opts into durable broadcasting the same way it always would — broadcasts_to, now with durable: true:
class Message < ApplicationRecord
broadcasts_to ->(m) { [m.room, :messages] }, durable: true
endPick a transport#
You have two choices. Toggle below to see the setup for each — the toggle itself is a reactive component (dogfooding).
Works out of the box. Broadcasts ride over WebSocket via Action Cable (needs Redis or solid_cable).
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") %>pgbus vs Action Cable#
Action Cable is the default turbo-rails transport. It works, but for reactive broadcasts it carries four tradeoffs pgbus does not:
- Needs Redis (or solid_cable). Action Cable requires a separate pub/sub backend. pgbus needs one Postgres — the database you already run.
- Non-transactional. An Action Cable broadcast fires immediately, even if the surrounding transaction later rolls back — a phantom UI update for a change that never landed. pgbus defers to
after_commit: a rolled-back transaction emits nothing. - Races on subscribe. A message broadcast between render and subscribe is lost on Action Cable. pgbus watermarks and replays it.
- No reconnect replay. A tab that dropped its Action Cable connection misses whatever aired while it was gone. pgbus replays from the PGMQ archive on reconnect (
Last-Event-ID).
pgbus-only broadcast options (exclude: / visible_to:)#
Because both transports share the identical ::Turbo::StreamsChannel API, extra transport keywords are simply forwarded to the stream. On Action Cable they are accepted and ignored (it has no per-connection routing); with pgbus they reach the dispatcher:
exclude:— the actor'sreactive_connection_id. Skips delivery to that one connection so the actor (who already got the action's HTTP response) never gets a duplicate echo.visible_to:— scopes a broadcast to a subset of subscribers on the stream.
# Under pgbus these reach the dispatcher; under Action Cable they no-op.
Todos::Item.broadcast_append_to(
@list, :todos,
target: dom_id(@list, :todos),
model: todo,
exclude: reactive_connection_id # actor already has the HTTP response
)Same observability on either transport#
Each broadcast_*_to wraps its body in a broadcast.phlex_reactive notification that fires on both Action Cable AND pgbus — the event wraps the class-method body, which is identical on either transport. So an APM sees the same broadcast fan-out (component name, stream action, streamables count — never the model or state) no matter which transport you run.
ActiveSupport::Notifications.subscribe("broadcast.phlex_reactive") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
MyAPM.record("reactive.broadcast", event.duration,
component: event.payload[:component],
stream_action: event.payload[:stream_action])
end