jonnifyelixir
F6 / F6.09 / multi-client

F6.09 · part 3 of 3

Many clients, live

A dashboard is most useful when a whole team watches it, and that is where the work from F6.07 pays off completely. Because a broadcast on "events" fans out to every subscribed process, every open dashboard receives the same event and folds it independently — ten browsers, ten mailboxes, ten diffs, all from one publish. No dashboard knows about the others; the runtime delivers to all of them. To show how many people are looking, the dashboard uses Presence: it tracks itself on a "dashboard" topic at mount, and a "presence_diff" message — arriving in handle_info/2 like any other — lets it recompute the viewer count from Presence.list/1. The whole thing stays read-only: the dashboard seeds from the contexts, folds broadcasts, and reports presence, but it never writes — it is a projection, not a participant. And because all of this rides on the BEAM’s distribution, clustering from F6.08 makes it correct across nodes for free: a viewer on one server and a write on another still meet, because PubSub and Presence span the cluster. One publish, every screen, an honest count — and the same code on one node or ten.

Four parts: the fan-out and what is read-only, how one event reaches every dashboard across a cluster, tracking viewers with Presence, and the read-only wiring that ties the whole course together.

Fan-out, Presence, read-only

Three ideas finish the lab. Fan-out means one broadcast reaches every dashboard. Presence reports who is watching. Read-only is the discipline: the dashboard observes, never writes. Select one.

Many clients · select one

FAN-OUT one broadcast reaches every connected dashboard at once delivery PRESENCE Presence.list("dashboard") — a live count of viewers who READ-ONLY the dashboard reads and folds — it never writes discipline

piece: fan-out

is: one broadcast, every dashboard

Read-only is not a limitation here — it is what makes the dashboard safe to open anywhere by anyone with access. It cannot corrupt state because it has no path to write; it can only show what the domain already decided.

One event, every dashboard

The fan-out is the F6.07 picture, now with dashboards on the receiving end. One context broadcast goes to PubSub, which delivers it to every subscribed dashboard; each folds the event into its own read model and diffs to its own browser. Clustered across nodes, a write on one server reaches a viewer on another, because PubSub and Presence span the whole cluster.

one publish → every dashboard, across nodes

context broadcast "events" PubSub spans the cluster NODE 1 dashboard A fold → diff dashboard B fold → diff NODE 2 dashboard C same event, same result one publish, every dashboard on every node — the same code whether you run one server or ten

Scaling the number of viewers or the number of servers does not change the publishing code at all. That invariance — one node or a cluster, one viewer or a thousand, the same broadcast — is the payoff of building on the BEAM from F5 onward.

Counting the viewers

The viewer count uses Presence exactly as in F6.07.3. At the connected mount the dashboard subscribes to the event topic and tracks itself on a "dashboard" topic. When anyone joins or leaves, Presence broadcasts a "presence_diff", which the dashboard handles by recomputing map_size(Presence.list(...)) into the viewers assign.

def mount(_params, _session, socket) do
  if connected?(socket) do
    Portal.subscribe("events")
    PortalWeb.Presence.track(self(), "dashboard", socket.id, %{})
  end

  {:ok, seed(socket)}
end

# Presence broadcasts a diff whenever a viewer joins or leaves
def handle_info(%{event: "presence_diff"}, socket) do
  {:noreply, assign(socket, viewers: map_size(PortalWeb.Presence.list("dashboard")))}
end

The viewer count and the metrics arrive through the same door — handle_info/2 — because both are process messages. Presence is not a special case in the dashboard; it is one more event kind folded into the read model.

The read-only wiring

The dashboard is routed like any LiveView, protected by the F6.08 auth, and everything it does is reading. The recap below is the whole lab in one view: seed from the contexts, fold broadcasts, report presence, and never write. That single read-only LiveView is where the engine, the contexts, the templates, LiveView, PubSub, and the deploy all meet.

# routed like any LiveView, protected by the F6.08 auth
live_session :authenticated,
  on_mount: [{PortalWeb.UserAuth, :ensure_authenticated}] do
  live "/dashboard", DashboardLive
end

# the dashboard only reads:
#   mount seeds counts from the contexts (F6.04)
#   handle_info folds broadcasts into assigns + a stream (F6.09.2)
#   Presence reports viewers; clustering (F6.08) spans nodes
# it never writes — a projection over the event stream

one publish

Fans out to every dashboard, on every node, and Presence counts them.

the whole course, live

Engine, contexts, LiveView, PubSub, and the deploy, in one read-only screen.

That completes F6.09 — and the course. From the F5 engine and its supervision tree, through contexts, routing, Ecto, templates, LiveView, PubSub, and deployment, to this dashboard that watches it all in real time, you have built one coherent system on the BEAM. Back to the module overview, the F6 chapter, or the course contents.