Guide

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
end

Pick 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).

config/cable.yml
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).
No Redis, no Action Cable. One Postgres — and your phlex-reactive code is identical.

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's reactive_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