Presence room
Open this page in multiple tabs to see visitors join, leave, and send collaborative room pings through Phoenix Presence and Dstar patches.
Demo
Lobby
Who is here right now?
Keep this tab open, then open another. Each tab becomes a distinct presence and receives live roster patches.
Online now
0
active browser tabs
How it works
- Phoenix Presence tracks each browser tab as a visitor.
- handle_connect/2 and handle_info/2 drive the room stream.
- Roster, online count, and activity are patched independently.
- Renaming yourself repaints every open tab's roster.
- The ping button broadcasts a tiny collaborative interaction.
handle_connect/2
subscribes to the room topic and tracks this tab with Phoenix.Presence, then patches the roster.
handle_info/2
reacts to presence_diff
broadcasts, and handle_disconnect/1
untracks on every exit from the receive loop — bookkeeping that a per-connection process
would do for you.
Phoenix.PubSub.subscribe(DstarDemo.PubSub, @topic)
remember_presence_instance(registry_tab, presence_instance)
{:ok, _} =
Presence.track(self(), @topic, tab_id, %{
name: name,
color: color,
status: "active",
presence_instance: presence_instance,
last_seen: System.system_time(:second)
})
If this were LiveView
Both sides track each tab with Phoenix.Presence,
react to presence_diff, and broadcast a room ping.
They differ in what has to happen when a visitor leaves.
Dstar
Phoenix.PubSub.subscribe(DstarDemo.PubSub, @topic)
remember_presence_instance(registry_tab, presence_instance)
{:ok, _} =
Presence.track(self(), @topic, tab_id, %{
name: name,
color: color,
status: "active",
presence_instance: presence_instance,
last_seen: System.system_time(:second)
})
LiveView
# Illustrative LiveView equivalent — not compiled or run.
def mount(_params, _session, socket) do
socket =
assign(socket,
visitor_name: random_name(),
visitor_color: random_color(),
activity: []
)
if connected?(socket) do
Phoenix.PubSub.subscribe(DstarDemo.PubSub, @topic)
{:ok, _} =
Presence.track(self(), @topic, socket.id, %{
name: socket.assigns.visitor_name,
color: socket.assigns.visitor_color,
status: "active",
last_seen: System.system_time(:second)
})
end
{:ok, assign(socket, roster: Presence.list(@topic))}
end
def handle_info(%Phoenix.Socket.Broadcast{event: "presence_diff"}, socket) do
{:noreply, assign(socket, roster: Presence.list(@topic))}
end
def handle_info({:ping, name}, socket) do
{:noreply, assign(socket, activity: [name | socket.assigns.activity])}
end
def handle_event("ping", _params, socket) do
Phoenix.PubSub.broadcast(
DstarDemo.PubSub,
@topic,
{:ping, socket.assigns.visitor_name}
)
{:noreply, socket}
end
# No untrack is written anywhere. Presence is bound to self(), and the
# LiveView process dying on disconnect removes the entry — including when
# it dies by crashing.