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](https://github.com/mhenrixon/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`:

```ruby
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).

Action Cable

pgbus

Works out of the box. Broadcasts ride over WebSocket via Action Cable (needs Redis or solid_cable).

```yaml
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`).

> **Tip:** 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.

```ruby
# 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.

```ruby
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
```